1/*
2 * Copyright (C) 2014 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 android.net.wifi;
18
19import android.Manifest.permission;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.content.Context;
23import android.net.INetworkScoreCache;
24import android.net.NetworkKey;
25import android.net.ScoredNetwork;
26import android.os.Handler;
27import android.os.Process;
28import android.util.Log;
29
30import com.android.internal.annotations.GuardedBy;
31import com.android.internal.util.Preconditions;
32
33import java.io.FileDescriptor;
34import java.io.PrintWriter;
35import java.util.HashMap;
36import java.util.List;
37import java.util.Map;
38
39/**
40 * {@link INetworkScoreCache} implementation for Wifi Networks.
41 *
42 * @hide
43 */
44public class WifiNetworkScoreCache extends INetworkScoreCache.Stub {
45    private static final String TAG = "WifiNetworkScoreCache";
46    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
47
48    // A Network scorer returns a score in the range [-128, +127]
49    // We treat the lowest possible score as though there were no score, effectively allowing the
50    // scorer to provide an RSSI threshold below which a network should not be used.
51    public static final int INVALID_NETWORK_SCORE = Byte.MIN_VALUE;
52
53    // See {@link #CacheListener}.
54    @Nullable
55    @GuardedBy("mCacheLock")
56    private CacheListener mListener;
57
58    private final Context mContext;
59    private final Object mCacheLock = new Object();
60
61    // The key is of the form "<ssid>"<bssid>
62    // TODO: What about SSIDs that can't be encoded as UTF-8?
63    private final Map<String, ScoredNetwork> mNetworkCache;
64
65
66    public WifiNetworkScoreCache(Context context) {
67        this(context, null /* listener */);
68    }
69
70    /**
71     * Instantiates a WifiNetworkScoreCache.
72     *
73     * @param context Application context
74     * @param listener CacheListener for cache updates
75     */
76    public WifiNetworkScoreCache(Context context, @Nullable CacheListener listener) {
77        mContext = context.getApplicationContext();
78        mListener = listener;
79        mNetworkCache = new HashMap<>();
80    }
81
82    @Override public final void updateScores(List<ScoredNetwork> networks) {
83        if (networks == null || networks.isEmpty()) {
84           return;
85        }
86        if (DBG) {
87            Log.d(TAG, "updateScores list size=" + networks.size());
88        }
89
90        boolean changed = false;
91
92        synchronized(mNetworkCache) {
93            for (ScoredNetwork network : networks) {
94                String networkKey = buildNetworkKey(network);
95                if (networkKey == null) {
96                    if (DBG) {
97                        Log.d(TAG, "Failed to build network key for ScoredNetwork" + network);
98                    }
99                    continue;
100                }
101                mNetworkCache.put(networkKey, network);
102                changed = true;
103            }
104        }
105
106        synchronized (mCacheLock) {
107            if (mListener != null && changed) {
108                mListener.post(networks);
109            }
110        }
111    }
112
113    @Override public final void clearScores() {
114        synchronized (mNetworkCache) {
115            mNetworkCache.clear();
116        }
117    }
118
119    /**
120     * Returns whether there is any score info for the given ScanResult.
121     *
122     * This includes null-score info, so it should only be used when determining whether to request
123     * scores from the network scorer.
124     */
125    public boolean isScoredNetwork(ScanResult result) {
126        return getScoredNetwork(result) != null;
127    }
128
129    /**
130     * Returns whether there is a non-null score curve for the given ScanResult.
131     *
132     * A null score curve has special meaning - we should never connect to an ephemeral network if
133     * the score curve is null.
134     */
135    public boolean hasScoreCurve(ScanResult result) {
136        ScoredNetwork network = getScoredNetwork(result);
137        return network != null && network.rssiCurve != null;
138    }
139
140    public int getNetworkScore(ScanResult result) {
141
142        int score = INVALID_NETWORK_SCORE;
143
144        ScoredNetwork network = getScoredNetwork(result);
145        if (network != null && network.rssiCurve != null) {
146            score = network.rssiCurve.lookupScore(result.level);
147            if (DBG) {
148                Log.d(TAG, "getNetworkScore found scored network " + network.networkKey
149                        + " score " + Integer.toString(score)
150                        + " RSSI " + result.level);
151            }
152        }
153        return score;
154    }
155
156    /**
157     * Returns the ScoredNetwork metered hint for a given ScanResult.
158     *
159     * If there is no ScoredNetwork associated with the ScanResult then false will be returned.
160     */
161    public boolean getMeteredHint(ScanResult result) {
162        ScoredNetwork network = getScoredNetwork(result);
163        return network != null && network.meteredHint;
164    }
165
166    public int getNetworkScore(ScanResult result, boolean isActiveNetwork) {
167
168        int score = INVALID_NETWORK_SCORE;
169
170        ScoredNetwork network = getScoredNetwork(result);
171        if (network != null && network.rssiCurve != null) {
172            score = network.rssiCurve.lookupScore(result.level, isActiveNetwork);
173            if (DBG) {
174                Log.d(TAG, "getNetworkScore found scored network " + network.networkKey
175                        + " score " + Integer.toString(score)
176                        + " RSSI " + result.level
177                        + " isActiveNetwork " + isActiveNetwork);
178            }
179        }
180        return score;
181    }
182
183    @Nullable
184    public ScoredNetwork getScoredNetwork(ScanResult result) {
185        String key = buildNetworkKey(result);
186        if (key == null) return null;
187
188        synchronized(mNetworkCache) {
189            ScoredNetwork network = mNetworkCache.get(key);
190            return network;
191        }
192    }
193
194    /** Returns the ScoredNetwork for the given key. */
195    @Nullable
196    public ScoredNetwork getScoredNetwork(NetworkKey networkKey) {
197        String key = buildNetworkKey(networkKey);
198        if (key == null) {
199            if (DBG) {
200                Log.d(TAG, "Could not build key string for Network Key: " + networkKey);
201            }
202            return null;
203        }
204        synchronized (mNetworkCache) {
205            return mNetworkCache.get(key);
206        }
207    }
208
209    private String buildNetworkKey(ScoredNetwork network) {
210        if (network == null) {
211            return null;
212        }
213        return buildNetworkKey(network.networkKey);
214    }
215
216    private String buildNetworkKey(NetworkKey networkKey) {
217        if (networkKey == null) {
218            return null;
219        }
220        if (networkKey.wifiKey == null) return null;
221        if (networkKey.type == NetworkKey.TYPE_WIFI) {
222            String key = networkKey.wifiKey.ssid;
223            if (key == null) return null;
224            if (networkKey.wifiKey.bssid != null) {
225                key = key + networkKey.wifiKey.bssid;
226            }
227            return key;
228        }
229        return null;
230    }
231
232    private String buildNetworkKey(ScanResult result) {
233        if (result == null || result.SSID == null) {
234            return null;
235        }
236        StringBuilder key = new StringBuilder("\"");
237        key.append(result.SSID);
238        key.append("\"");
239        if (result.BSSID != null) {
240            key.append(result.BSSID);
241        }
242        return key.toString();
243    }
244
245    @Override protected final void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
246        mContext.enforceCallingOrSelfPermission(permission.DUMP, TAG);
247        String header = String.format("WifiNetworkScoreCache (%s/%d)",
248                mContext.getPackageName(), Process.myUid());
249        writer.println(header);
250        writer.println("  All score curves:");
251        for (ScoredNetwork score : mNetworkCache.values()) {
252            writer.println("    " + score);
253        }
254        writer.println("  Current network scores:");
255        WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
256        for (ScanResult scanResult : wifiManager.getScanResults()) {
257            writer.println("    " + buildNetworkKey(scanResult) + ": " + getNetworkScore(scanResult));
258        }
259    }
260
261    /** Registers a CacheListener instance, replacing the previous listener if it existed. */
262    public void registerListener(CacheListener listener) {
263        synchronized (mCacheLock) {
264            mListener = listener;
265        }
266    }
267
268    /** Removes the registered CacheListener. */
269    public void unregisterListener() {
270        synchronized (mCacheLock) {
271            mListener = null;
272        }
273    }
274
275    /** Listener for updates to the cache inside WifiNetworkScoreCache. */
276    public abstract static class CacheListener {
277
278        private Handler mHandler;
279
280        /**
281         * Constructor for CacheListener.
282         *
283         * @param handler the Handler on which to invoke the {@link #networkCacheUpdated} method.
284         *          This cannot be null.
285         */
286        public CacheListener(@NonNull Handler handler) {
287            Preconditions.checkNotNull(handler);
288            mHandler = handler;
289        }
290
291        /** Invokes the {@link #networkCacheUpdated(List<ScoredNetwork>)} method on the handler. */
292        void post(List<ScoredNetwork> updatedNetworks) {
293            mHandler.post(new Runnable() {
294                @Override
295                public void run() {
296                    networkCacheUpdated(updatedNetworks);
297                }
298            });
299        }
300
301        /**
302         * Invoked whenever the cache is updated.
303         *
304         * <p>Clearing the cache does not invoke this method.
305         *
306         * @param updatedNetworks the networks that were updated
307         */
308        public abstract void networkCacheUpdated(List<ScoredNetwork> updatedNetworks);
309    }
310}
311