blob: 9218c25a9860e71e3e7fea2775e56cbb09394c25 [file] [log] [blame]
Jeff Davidson6a4b2202014-04-16 17:29:40 -07001/*
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 com.android.server;
18
Jeremy Joslin145c3432016-12-09 13:11:51 -080019import static android.net.NetworkRecommendationProvider.EXTRA_RECOMMENDATION_RESULT;
20import static android.net.NetworkRecommendationProvider.EXTRA_SEQUENCE;
21
Jeff Davidson6a4b2202014-04-16 17:29:40 -070022import android.Manifest.permission;
Jeremy Joslin145c3432016-12-09 13:11:51 -080023import android.annotation.Nullable;
Jeremy Joslin967b5812016-06-02 07:58:14 -070024import android.content.BroadcastReceiver;
Jeremy Joslindd251ef2016-03-14 11:17:41 -070025import android.content.ComponentName;
Jeff Davidson56f9f732014-08-14 16:47:23 -070026import android.content.ContentResolver;
Jeff Davidson6a4b2202014-04-16 17:29:40 -070027import android.content.Context;
Jeff Davidsonb096bdc2014-07-01 12:29:11 -070028import android.content.Intent;
Jeremy Joslin967b5812016-06-02 07:58:14 -070029import android.content.IntentFilter;
Jeremy Joslindd251ef2016-03-14 11:17:41 -070030import android.content.ServiceConnection;
Jeff Davidson6a4b2202014-04-16 17:29:40 -070031import android.content.pm.PackageManager;
Jeremy Joslinfa4f08e2016-12-06 07:42:38 -080032import android.database.ContentObserver;
Jeremy Joslin145c3432016-12-09 13:11:51 -080033import android.net.INetworkRecommendationProvider;
Jeff Davidson14f1ec02014-04-29 11:58:26 -070034import android.net.INetworkScoreCache;
Jeff Davidson6a4b2202014-04-16 17:29:40 -070035import android.net.INetworkScoreService;
Jeremy Joslinb2087a12016-12-13 16:11:51 -080036import android.net.NetworkKey;
Jeremy Joslin5519d7c2017-01-06 14:36:54 -080037import android.net.NetworkScoreManager;
Jeremy Joslinf621bc92017-02-16 11:11:57 -080038import android.net.NetworkScorerAppData;
Jeremy Joslind1daf6d2016-11-28 17:47:35 -080039import android.net.RecommendationRequest;
40import android.net.RecommendationResult;
Jeff Davidson6a4b2202014-04-16 17:29:40 -070041import android.net.ScoredNetwork;
Jeremy Joslinfa4f08e2016-12-06 07:42:38 -080042import android.net.Uri;
Jeremy Joslinba242732017-01-24 17:16:42 -080043import android.net.wifi.ScanResult;
44import android.net.wifi.WifiInfo;
45import android.net.wifi.WifiManager;
46import android.net.wifi.WifiScanner;
Jeremy Joslin8f5521a2016-12-20 14:36:20 -080047import android.os.Binder;
Jeremy Joslince73c6f2016-12-29 14:49:38 -080048import android.os.Build;
Jeremy Joslin145c3432016-12-09 13:11:51 -080049import android.os.Bundle;
Jeremy Joslince73c6f2016-12-29 14:49:38 -080050import android.os.Handler;
Jeremy Joslindd251ef2016-03-14 11:17:41 -070051import android.os.IBinder;
Jeremy Joslin145c3432016-12-09 13:11:51 -080052import android.os.IRemoteCallback;
Jeremy Joslince73c6f2016-12-29 14:49:38 -080053import android.os.Looper;
54import android.os.Message;
Jeremy Joslina5172f62017-02-02 14:27:05 -080055import android.os.Process;
Jeremy Joslin998d7ca2016-12-28 15:56:46 -080056import android.os.RemoteCallback;
Amin Shaikh972e2362016-12-07 14:08:09 -080057import android.os.RemoteCallbackList;
Jeff Davidson14f1ec02014-04-29 11:58:26 -070058import android.os.RemoteException;
Jeff Davidsonac7285d2014-08-08 15:12:47 -070059import android.os.UserHandle;
Jeremy Joslincb594f32017-01-03 17:31:23 -080060import android.provider.Settings;
Jeremy Joslinfa4f08e2016-12-06 07:42:38 -080061import android.provider.Settings.Global;
Amin Shaikh972e2362016-12-07 14:08:09 -080062import android.util.ArrayMap;
Jeremy Josline71fe2b2017-01-25 11:40:08 -080063import android.util.ArraySet;
Jeff Davidson14f1ec02014-04-29 11:58:26 -070064import android.util.Log;
Jeremy Joslince73c6f2016-12-29 14:49:38 -080065import android.util.Pair;
Jeremy Joslin145c3432016-12-09 13:11:51 -080066import android.util.TimedRemoteCaller;
67
Jeff Davidson7842f642014-11-23 13:48:12 -080068import com.android.internal.annotations.GuardedBy;
Amin Shaikhaa09aa02016-11-21 17:27:53 -080069import com.android.internal.annotations.VisibleForTesting;
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -070070import com.android.internal.content.PackageMonitor;
Jeff Sharkeyba6f8c82016-11-09 12:25:44 -070071import com.android.internal.os.TransferPipe;
Jeff Sharkeyfe9a53b2017-03-31 14:08:23 -060072import com.android.internal.util.DumpUtils;
Jeremy Joslin145c3432016-12-09 13:11:51 -080073
Jeff Davidson6a4b2202014-04-16 17:29:40 -070074import java.io.FileDescriptor;
Jeff Sharkeyba6f8c82016-11-09 12:25:44 -070075import java.io.IOException;
Jeff Davidson6a4b2202014-04-16 17:29:40 -070076import java.io.PrintWriter;
Jeff Davidson14f1ec02014-04-29 11:58:26 -070077import java.util.ArrayList;
Amin Shaikh972e2362016-12-07 14:08:09 -080078import java.util.Collection;
79import java.util.Collections;
Jeff Davidson14f1ec02014-04-29 11:58:26 -070080import java.util.List;
Jeff Davidson6a4b2202014-04-16 17:29:40 -070081import java.util.Map;
Jeremy Josline71fe2b2017-01-25 11:40:08 -080082import java.util.Set;
Jeremy Joslin145c3432016-12-09 13:11:51 -080083import java.util.concurrent.TimeoutException;
Jeremy Joslince73c6f2016-12-29 14:49:38 -080084import java.util.concurrent.atomic.AtomicBoolean;
Jeremy Joslin3452b692017-01-17 15:48:13 -080085import java.util.concurrent.atomic.AtomicReference;
Jeremy Joslinba242732017-01-24 17:16:42 -080086import java.util.function.BiConsumer;
Jeremy Joslinba242732017-01-24 17:16:42 -080087import java.util.function.Supplier;
Jeremy Josline71fe2b2017-01-25 11:40:08 -080088import java.util.function.UnaryOperator;
Jeff Davidson6a4b2202014-04-16 17:29:40 -070089
90/**
91 * Backing service for {@link android.net.NetworkScoreManager}.
92 * @hide
93 */
94public class NetworkScoreService extends INetworkScoreService.Stub {
95 private static final String TAG = "NetworkScoreService";
Jeremy Joslince73c6f2016-12-29 14:49:38 -080096 private static final boolean DBG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG);
Jeremy Josline71fe2b2017-01-25 11:40:08 -080097 private static final boolean VERBOSE = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.VERBOSE);
Jeff Davidson6a4b2202014-04-16 17:29:40 -070098
Jeff Davidson6a4b2202014-04-16 17:29:40 -070099 private final Context mContext;
Amin Shaikhaa09aa02016-11-21 17:27:53 -0800100 private final NetworkScorerAppManager mNetworkScorerAppManager;
Jeremy Joslin3452b692017-01-17 15:48:13 -0800101 private final AtomicReference<RequestRecommendationCaller> mReqRecommendationCallerRef;
Amin Shaikh972e2362016-12-07 14:08:09 -0800102 @GuardedBy("mScoreCaches")
103 private final Map<Integer, RemoteCallbackList<INetworkScoreCache>> mScoreCaches;
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -0700104 /** Lock used to update mPackageMonitor when scorer package changes occur. */
Jeremy Joslince73c6f2016-12-29 14:49:38 -0800105 private final Object mPackageMonitorLock = new Object();
106 private final Object mServiceConnectionLock = new Object();
107 private final Handler mHandler;
Jeremy Joslincb594f32017-01-03 17:31:23 -0800108 private final DispatchingContentObserver mContentObserver;
Jeff Davidson7842f642014-11-23 13:48:12 -0800109
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -0700110 @GuardedBy("mPackageMonitorLock")
111 private NetworkScorerPackageMonitor mPackageMonitor;
Jeremy Joslin145c3432016-12-09 13:11:51 -0800112 @GuardedBy("mServiceConnectionLock")
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700113 private ScoringServiceConnection mServiceConnection;
Jeremy Joslincb594f32017-01-03 17:31:23 -0800114 private volatile long mRecommendationRequestTimeoutMs;
Jeff Davidson7842f642014-11-23 13:48:12 -0800115
Jeremy Joslin967b5812016-06-02 07:58:14 -0700116 private BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
117 @Override
118 public void onReceive(Context context, Intent intent) {
119 final String action = intent.getAction();
120 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
121 if (DBG) Log.d(TAG, "Received " + action + " for userId " + userId);
122 if (userId == UserHandle.USER_NULL) return;
123
124 if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
125 onUserUnlocked(userId);
126 }
127 }
128 };
129
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -0700130 /**
131 * Clears scores when the active scorer package is no longer valid and
132 * manages the service connection.
133 */
134 private class NetworkScorerPackageMonitor extends PackageMonitor {
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800135 final String mPackageToWatch;
Jeff Davidson7842f642014-11-23 13:48:12 -0800136
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800137 private NetworkScorerPackageMonitor(String packageToWatch) {
138 mPackageToWatch = packageToWatch;
Jeff Davidson7842f642014-11-23 13:48:12 -0800139 }
140
141 @Override
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -0700142 public void onPackageAdded(String packageName, int uid) {
143 evaluateBinding(packageName, true /* forceUnbind */);
144 }
145
146 @Override
147 public void onPackageRemoved(String packageName, int uid) {
148 evaluateBinding(packageName, true /* forceUnbind */);
149 }
150
151 @Override
152 public void onPackageModified(String packageName) {
153 evaluateBinding(packageName, false /* forceUnbind */);
154 }
155
156 @Override
157 public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
158 if (doit) { // "doit" means the force stop happened instead of just being queried for.
159 for (String packageName : packages) {
160 evaluateBinding(packageName, true /* forceUnbind */);
161 }
162 }
163 return super.onHandleForceStop(intent, packages, uid, doit);
164 }
165
166 @Override
167 public void onPackageUpdateFinished(String packageName, int uid) {
168 evaluateBinding(packageName, true /* forceUnbind */);
169 }
170
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800171 private void evaluateBinding(String changedPackageName, boolean forceUnbind) {
172 if (!mPackageToWatch.equals(changedPackageName)) {
Jeremy Joslin86c2a5e2016-12-21 13:35:02 -0800173 // Early exit when we don't care about the package that has changed.
174 return;
175 }
176
177 if (DBG) {
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800178 Log.d(TAG, "Evaluating binding for: " + changedPackageName
Jeremy Joslin86c2a5e2016-12-21 13:35:02 -0800179 + ", forceUnbind=" + forceUnbind);
180 }
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800181
Jeremy Joslin86c2a5e2016-12-21 13:35:02 -0800182 final NetworkScorerAppData activeScorer = mNetworkScorerAppManager.getActiveScorer();
183 if (activeScorer == null) {
184 // Package change has invalidated a scorer, this will also unbind any service
185 // connection.
186 if (DBG) Log.d(TAG, "No active scorers available.");
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800187 refreshBinding();
188 } else { // The scoring service changed in some way.
Jeremy Joslin86c2a5e2016-12-21 13:35:02 -0800189 if (forceUnbind) {
190 unbindFromScoringServiceIfNeeded();
191 }
Jeremy Joslin86c2a5e2016-12-21 13:35:02 -0800192 if (DBG) {
Jeremy Joslin37e877b2017-02-02 11:06:14 -0800193 Log.d(TAG, "Binding to " + activeScorer.getRecommendationServiceComponent()
194 + " if needed.");
Jeremy Joslin86c2a5e2016-12-21 13:35:02 -0800195 }
196 bindToScoringServiceIfNeeded(activeScorer);
Jeff Davidson7842f642014-11-23 13:48:12 -0800197 }
198 }
199 }
200
Jeremy Joslinfa4f08e2016-12-06 07:42:38 -0800201 /**
Jeremy Joslincb594f32017-01-03 17:31:23 -0800202 * Dispatches observed content changes to a handler for further processing.
Jeremy Joslinfa4f08e2016-12-06 07:42:38 -0800203 */
Jeremy Joslincb594f32017-01-03 17:31:23 -0800204 @VisibleForTesting
205 public static class DispatchingContentObserver extends ContentObserver {
206 final private Map<Uri, Integer> mUriEventMap;
207 final private Context mContext;
208 final private Handler mHandler;
Jeremy Joslinfa4f08e2016-12-06 07:42:38 -0800209
Jeremy Joslincb594f32017-01-03 17:31:23 -0800210 public DispatchingContentObserver(Context context, Handler handler) {
211 super(handler);
212 mContext = context;
213 mHandler = handler;
214 mUriEventMap = new ArrayMap<>();
215 }
216
217 void observe(Uri uri, int what) {
218 mUriEventMap.put(uri, what);
219 final ContentResolver resolver = mContext.getContentResolver();
220 resolver.registerContentObserver(uri, false /*notifyForDescendants*/, this);
Jeremy Joslinfa4f08e2016-12-06 07:42:38 -0800221 }
222
223 @Override
224 public void onChange(boolean selfChange) {
225 onChange(selfChange, null);
226 }
227
228 @Override
229 public void onChange(boolean selfChange, Uri uri) {
230 if (DBG) Log.d(TAG, String.format("onChange(%s, %s)", selfChange, uri));
Jeremy Joslincb594f32017-01-03 17:31:23 -0800231 final Integer what = mUriEventMap.get(uri);
232 if (what != null) {
233 mHandler.obtainMessage(what).sendToTarget();
234 } else {
235 Log.w(TAG, "No matching event to send for URI = " + uri);
236 }
Jeremy Joslinfa4f08e2016-12-06 07:42:38 -0800237 }
238 }
239
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700240 public NetworkScoreService(Context context) {
Jeremy Joslince73c6f2016-12-29 14:49:38 -0800241 this(context, new NetworkScorerAppManager(context), Looper.myLooper());
Amin Shaikhaa09aa02016-11-21 17:27:53 -0800242 }
243
244 @VisibleForTesting
Jeremy Joslince73c6f2016-12-29 14:49:38 -0800245 NetworkScoreService(Context context, NetworkScorerAppManager networkScoreAppManager,
246 Looper looper) {
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700247 mContext = context;
Amin Shaikhaa09aa02016-11-21 17:27:53 -0800248 mNetworkScorerAppManager = networkScoreAppManager;
Amin Shaikh972e2362016-12-07 14:08:09 -0800249 mScoreCaches = new ArrayMap<>();
Jeremy Joslin967b5812016-06-02 07:58:14 -0700250 IntentFilter filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
251 // TODO: Need to update when we support per-user scorers. http://b/23422763
252 mContext.registerReceiverAsUser(
253 mUserIntentReceiver, UserHandle.SYSTEM, filter, null /* broadcastPermission*/,
254 null /* scheduler */);
Jeremy Joslin3452b692017-01-17 15:48:13 -0800255 mReqRecommendationCallerRef = new AtomicReference<>(
256 new RequestRecommendationCaller(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS));
Jeremy Joslince73c6f2016-12-29 14:49:38 -0800257 mRecommendationRequestTimeoutMs = TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS;
258 mHandler = new ServiceHandler(looper);
Jeremy Joslincb594f32017-01-03 17:31:23 -0800259 mContentObserver = new DispatchingContentObserver(context, mHandler);
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700260 }
261
262 /** Called when the system is ready to run third-party code but before it actually does so. */
263 void systemReady() {
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700264 if (DBG) Log.d(TAG, "systemReady");
Jeremy Joslincb594f32017-01-03 17:31:23 -0800265 registerRecommendationSettingsObserver();
Jeff Davidson7842f642014-11-23 13:48:12 -0800266 }
267
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700268 /** Called when the system is ready for us to start third-party code. */
269 void systemRunning() {
270 if (DBG) Log.d(TAG, "systemRunning");
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700271 }
272
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800273 @VisibleForTesting
274 void onUserUnlocked(int userId) {
275 if (DBG) Log.d(TAG, "onUserUnlocked(" + userId + ")");
276 refreshBinding();
277 }
278
279 private void refreshBinding() {
280 if (DBG) Log.d(TAG, "refreshBinding()");
Jeremy Joslin9925c6a2017-03-06 10:39:35 -0800281 // Make sure the scorer is up-to-date
282 mNetworkScorerAppManager.updateState();
Jeremy Joslinb0fe2172017-03-31 10:38:31 -0700283 mNetworkScorerAppManager.migrateNetworkScorerAppSettingIfNeeded();
Jeremy Joslin967b5812016-06-02 07:58:14 -0700284 registerPackageMonitorIfNeeded();
285 bindToScoringServiceIfNeeded();
286 }
287
Jeremy Joslincb594f32017-01-03 17:31:23 -0800288 private void registerRecommendationSettingsObserver() {
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800289 final Uri packageNameUri = Global.getUriFor(Global.NETWORK_RECOMMENDATIONS_PACKAGE);
290 mContentObserver.observe(packageNameUri,
291 ServiceHandler.MSG_RECOMMENDATIONS_PACKAGE_CHANGED);
Jeremy Joslincb594f32017-01-03 17:31:23 -0800292
293 final Uri timeoutUri = Global.getUriFor(Global.NETWORK_RECOMMENDATION_REQUEST_TIMEOUT_MS);
294 mContentObserver.observe(timeoutUri,
295 ServiceHandler.MSG_RECOMMENDATION_REQUEST_TIMEOUT_CHANGED);
Jeremy Joslin9925c6a2017-03-06 10:39:35 -0800296
297 final Uri settingUri = Global.getUriFor(Global.NETWORK_RECOMMENDATIONS_ENABLED);
298 mContentObserver.observe(settingUri,
299 ServiceHandler.MSG_RECOMMENDATION_ENABLED_SETTING_CHANGED);
Jeremy Joslinfa4f08e2016-12-06 07:42:38 -0800300 }
301
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800302 /**
303 * Ensures the package manager is registered to monitor the current active scorer.
304 * If a discrepancy is found any previous monitor will be cleaned up
305 * and a new monitor will be created.
306 *
307 * This method is idempotent.
308 */
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -0700309 private void registerPackageMonitorIfNeeded() {
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800310 if (DBG) Log.d(TAG, "registerPackageMonitorIfNeeded()");
311 final NetworkScorerAppData appData = mNetworkScorerAppManager.getActiveScorer();
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -0700312 synchronized (mPackageMonitorLock) {
313 // Unregister the current monitor if needed.
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800314 if (mPackageMonitor != null && (appData == null
315 || !appData.getRecommendationServicePackageName().equals(
316 mPackageMonitor.mPackageToWatch))) {
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -0700317 if (DBG) {
318 Log.d(TAG, "Unregistering package monitor for "
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800319 + mPackageMonitor.mPackageToWatch);
Jeff Davidson7842f642014-11-23 13:48:12 -0800320 }
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -0700321 mPackageMonitor.unregister();
322 mPackageMonitor = null;
Jeff Davidson7842f642014-11-23 13:48:12 -0800323 }
324
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800325 // Create and register the monitor if a scorer is active.
326 if (appData != null && mPackageMonitor == null) {
327 mPackageMonitor = new NetworkScorerPackageMonitor(
328 appData.getRecommendationServicePackageName());
Xiaohui Chene4de5a02015-09-22 15:33:31 -0700329 // TODO: Need to update when we support per-user scorers. http://b/23422763
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -0700330 mPackageMonitor.register(mContext, null /* thread */, UserHandle.SYSTEM,
331 false /* externalStorage */);
332 if (DBG) {
333 Log.d(TAG, "Registered package monitor for "
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800334 + mPackageMonitor.mPackageToWatch);
Jeff Davidson7842f642014-11-23 13:48:12 -0800335 }
336 }
337 }
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700338 }
339
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700340 private void bindToScoringServiceIfNeeded() {
341 if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded");
Amin Shaikhaa09aa02016-11-21 17:27:53 -0800342 NetworkScorerAppData scorerData = mNetworkScorerAppManager.getActiveScorer();
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700343 bindToScoringServiceIfNeeded(scorerData);
344 }
345
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800346 /**
347 * Ensures the service connection is bound to the current active scorer.
348 * If a discrepancy is found any previous connection will be cleaned up
349 * and a new connection will be created.
350 *
351 * This method is idempotent.
352 */
Jeremy Joslin37e877b2017-02-02 11:06:14 -0800353 private void bindToScoringServiceIfNeeded(NetworkScorerAppData appData) {
354 if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded(" + appData + ")");
355 if (appData != null) {
Jeremy Joslin145c3432016-12-09 13:11:51 -0800356 synchronized (mServiceConnectionLock) {
357 // If we're connected to a different component then drop it.
358 if (mServiceConnection != null
Jeremy Joslin37e877b2017-02-02 11:06:14 -0800359 && !mServiceConnection.mAppData.equals(appData)) {
Jeremy Joslin145c3432016-12-09 13:11:51 -0800360 unbindFromScoringServiceIfNeeded();
361 }
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700362
Jeremy Joslin145c3432016-12-09 13:11:51 -0800363 // If we're not connected at all then create a new connection.
364 if (mServiceConnection == null) {
Jeremy Joslin37e877b2017-02-02 11:06:14 -0800365 mServiceConnection = new ScoringServiceConnection(appData);
Jeremy Joslin145c3432016-12-09 13:11:51 -0800366 }
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700367
Jeremy Joslin145c3432016-12-09 13:11:51 -0800368 // Make sure the connection is connected (idempotent)
369 mServiceConnection.connect(mContext);
370 }
Jeremy Joslin967b5812016-06-02 07:58:14 -0700371 } else { // otherwise make sure it isn't bound.
372 unbindFromScoringServiceIfNeeded();
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700373 }
374 }
375
376 private void unbindFromScoringServiceIfNeeded() {
377 if (DBG) Log.d(TAG, "unbindFromScoringServiceIfNeeded");
Jeremy Joslin145c3432016-12-09 13:11:51 -0800378 synchronized (mServiceConnectionLock) {
379 if (mServiceConnection != null) {
380 mServiceConnection.disconnect(mContext);
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800381 if (DBG) Log.d(TAG, "Disconnected from: "
382 + mServiceConnection.mAppData.getRecommendationServiceComponent());
Jeremy Joslin145c3432016-12-09 13:11:51 -0800383 }
384 mServiceConnection = null;
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700385 }
Jeremy Joslinfa4f08e2016-12-06 07:42:38 -0800386 clearInternal();
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700387 }
388
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700389 @Override
390 public boolean updateScores(ScoredNetwork[] networks) {
Jeremy Joslin134c9d32017-01-09 16:22:20 -0800391 if (!isCallerActiveScorer(getCallingUid())) {
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700392 throw new SecurityException("Caller with UID " + getCallingUid() +
393 " is not the active scorer.");
394 }
395
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800396 final long token = Binder.clearCallingIdentity();
397 try {
398 // Separate networks by type.
399 Map<Integer, List<ScoredNetwork>> networksByType = new ArrayMap<>();
400 for (ScoredNetwork network : networks) {
401 List<ScoredNetwork> networkList = networksByType.get(network.networkKey.type);
402 if (networkList == null) {
403 networkList = new ArrayList<>();
404 networksByType.put(network.networkKey.type, networkList);
Amin Shaikh972e2362016-12-07 14:08:09 -0800405 }
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800406 networkList.add(network);
Amin Shaikh972e2362016-12-07 14:08:09 -0800407 }
408
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800409 // Pass the scores of each type down to the appropriate network scorer.
410 for (final Map.Entry<Integer, List<ScoredNetwork>> entry : networksByType.entrySet()) {
411 final RemoteCallbackList<INetworkScoreCache> callbackList;
412 final boolean isEmpty;
413 synchronized (mScoreCaches) {
414 callbackList = mScoreCaches.get(entry.getKey());
415 isEmpty = callbackList == null
416 || callbackList.getRegisteredCallbackCount() == 0;
417 }
Jeremy Joslinba242732017-01-24 17:16:42 -0800418
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800419 if (isEmpty) {
420 if (Log.isLoggable(TAG, Log.VERBOSE)) {
421 Log.v(TAG, "No scorer registered for type " + entry.getKey()
422 + ", discarding");
423 }
424 continue;
425 }
426
Jeremy Joslinba242732017-01-24 17:16:42 -0800427 final BiConsumer<INetworkScoreCache, Object> consumer =
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800428 FilteringCacheUpdatingConsumer.create(mContext, entry.getValue(),
Jeremy Joslinba242732017-01-24 17:16:42 -0800429 entry.getKey());
430 sendCacheUpdateCallback(consumer, Collections.singleton(callbackList));
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800431 }
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700432
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800433 return true;
434 } finally {
435 Binder.restoreCallingIdentity(token);
436 }
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700437 }
438
Jeremy Joslinba242732017-01-24 17:16:42 -0800439 /**
440 * A {@link BiConsumer} implementation that filters the given {@link ScoredNetwork}
441 * list (if needed) before invoking {@link INetworkScoreCache#updateScores(List)} on the
442 * accepted {@link INetworkScoreCache} implementation.
443 */
444 @VisibleForTesting
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800445 static class FilteringCacheUpdatingConsumer
Jeremy Joslinba242732017-01-24 17:16:42 -0800446 implements BiConsumer<INetworkScoreCache, Object> {
447 private final Context mContext;
448 private final List<ScoredNetwork> mScoredNetworkList;
449 private final int mNetworkType;
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800450 // TODO: 1/23/17 - Consider a Map if we implement more filters.
451 // These are created on-demand to defer the construction cost until
452 // an instance is actually needed.
453 private UnaryOperator<List<ScoredNetwork>> mCurrentNetworkFilter;
454 private UnaryOperator<List<ScoredNetwork>> mScanResultsFilter;
Jeremy Joslinba242732017-01-24 17:16:42 -0800455
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800456 static FilteringCacheUpdatingConsumer create(Context context,
Jeremy Joslinba242732017-01-24 17:16:42 -0800457 List<ScoredNetwork> scoredNetworkList, int networkType) {
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800458 return new FilteringCacheUpdatingConsumer(context, scoredNetworkList, networkType,
459 null, null);
Jeremy Joslinba242732017-01-24 17:16:42 -0800460 }
461
462 @VisibleForTesting
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800463 FilteringCacheUpdatingConsumer(Context context,
Jeremy Joslinba242732017-01-24 17:16:42 -0800464 List<ScoredNetwork> scoredNetworkList, int networkType,
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800465 UnaryOperator<List<ScoredNetwork>> currentNetworkFilter,
466 UnaryOperator<List<ScoredNetwork>> scanResultsFilter) {
Jeremy Joslinba242732017-01-24 17:16:42 -0800467 mContext = context;
468 mScoredNetworkList = scoredNetworkList;
469 mNetworkType = networkType;
470 mCurrentNetworkFilter = currentNetworkFilter;
471 mScanResultsFilter = scanResultsFilter;
472 }
473
474 @Override
475 public void accept(INetworkScoreCache networkScoreCache, Object cookie) {
476 int filterType = NetworkScoreManager.CACHE_FILTER_NONE;
477 if (cookie instanceof Integer) {
478 filterType = (Integer) cookie;
479 }
480
481 try {
482 final List<ScoredNetwork> filteredNetworkList =
483 filterScores(mScoredNetworkList, filterType);
484 if (!filteredNetworkList.isEmpty()) {
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800485 networkScoreCache.updateScores(filteredNetworkList);
Jeremy Joslinba242732017-01-24 17:16:42 -0800486 }
487 } catch (RemoteException e) {
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800488 if (VERBOSE) {
Jeremy Joslinba242732017-01-24 17:16:42 -0800489 Log.v(TAG, "Unable to update scores of type " + mNetworkType, e);
490 }
491 }
492 }
493
494 /**
495 * Applies the appropriate filter and returns the filtered results.
496 */
497 private List<ScoredNetwork> filterScores(List<ScoredNetwork> scoredNetworkList,
498 int filterType) {
499 switch (filterType) {
500 case NetworkScoreManager.CACHE_FILTER_NONE:
501 return scoredNetworkList;
502
503 case NetworkScoreManager.CACHE_FILTER_CURRENT_NETWORK:
504 if (mCurrentNetworkFilter == null) {
505 mCurrentNetworkFilter =
506 new CurrentNetworkScoreCacheFilter(new WifiInfoSupplier(mContext));
507 }
508 return mCurrentNetworkFilter.apply(scoredNetworkList);
509
510 case NetworkScoreManager.CACHE_FILTER_SCAN_RESULTS:
511 if (mScanResultsFilter == null) {
512 mScanResultsFilter = new ScanResultsScoreCacheFilter(
513 new ScanResultsSupplier(mContext));
514 }
515 return mScanResultsFilter.apply(scoredNetworkList);
516
517 default:
518 Log.w(TAG, "Unknown filter type: " + filterType);
519 return scoredNetworkList;
520 }
521 }
522 }
523
524 /**
525 * Helper class that improves the testability of the cache filter Functions.
526 */
527 private static class WifiInfoSupplier implements Supplier<WifiInfo> {
528 private final Context mContext;
529
530 WifiInfoSupplier(Context context) {
531 mContext = context;
532 }
533
534 @Override
535 public WifiInfo get() {
536 WifiManager wifiManager = mContext.getSystemService(WifiManager.class);
537 if (wifiManager != null) {
538 return wifiManager.getConnectionInfo();
539 }
540 Log.w(TAG, "WifiManager is null, failed to return the WifiInfo.");
541 return null;
542 }
543 }
544
545 /**
546 * Helper class that improves the testability of the cache filter Functions.
547 */
548 private static class ScanResultsSupplier implements Supplier<List<ScanResult>> {
549 private final Context mContext;
550
551 ScanResultsSupplier(Context context) {
552 mContext = context;
553 }
554
555 @Override
556 public List<ScanResult> get() {
557 WifiScanner wifiScanner = mContext.getSystemService(WifiScanner.class);
558 if (wifiScanner != null) {
559 return wifiScanner.getSingleScanResults();
560 }
561 Log.w(TAG, "WifiScanner is null, failed to return scan results.");
562 return Collections.emptyList();
563 }
564 }
565
566 /**
567 * Filters the given set of {@link ScoredNetwork}s and returns a new List containing only the
568 * {@link ScoredNetwork} associated with the current network. If no network is connected the
569 * returned list will be empty.
570 * <p>
571 * Note: this filter performs some internal caching for consistency and performance. The
572 * current network is determined at construction time and never changed. Also, the
573 * last filtered list is saved so if the same input is provided multiple times in a row
574 * the computation is only done once.
575 */
576 @VisibleForTesting
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800577 static class CurrentNetworkScoreCacheFilter implements UnaryOperator<List<ScoredNetwork>> {
Jeremy Joslinba242732017-01-24 17:16:42 -0800578 private final NetworkKey mCurrentNetwork;
Jeremy Joslinba242732017-01-24 17:16:42 -0800579
580 CurrentNetworkScoreCacheFilter(Supplier<WifiInfo> wifiInfoSupplier) {
581 mCurrentNetwork = NetworkKey.createFromWifiInfo(wifiInfoSupplier.get());
582 }
583
584 @Override
585 public List<ScoredNetwork> apply(List<ScoredNetwork> scoredNetworks) {
586 if (mCurrentNetwork == null || scoredNetworks.isEmpty()) {
587 return Collections.emptyList();
588 }
589
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800590 for (int i = 0; i < scoredNetworks.size(); i++) {
591 final ScoredNetwork scoredNetwork = scoredNetworks.get(i);
592 if (scoredNetwork.networkKey.equals(mCurrentNetwork)) {
593 return Collections.singletonList(scoredNetwork);
Jeremy Joslinba242732017-01-24 17:16:42 -0800594 }
595 }
596
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800597 return Collections.emptyList();
Jeremy Joslinba242732017-01-24 17:16:42 -0800598 }
599 }
600
601 /**
602 * Filters the given set of {@link ScoredNetwork}s and returns a new List containing only the
603 * {@link ScoredNetwork} associated with the current set of {@link ScanResult}s.
604 * If there are no {@link ScanResult}s the returned list will be empty.
605 * <p>
606 * Note: this filter performs some internal caching for consistency and performance. The
607 * current set of ScanResults is determined at construction time and never changed.
608 * Also, the last filtered list is saved so if the same input is provided multiple
609 * times in a row the computation is only done once.
610 */
611 @VisibleForTesting
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800612 static class ScanResultsScoreCacheFilter implements UnaryOperator<List<ScoredNetwork>> {
613 private final Set<NetworkKey> mScanResultKeys;
Jeremy Joslinba242732017-01-24 17:16:42 -0800614
615 ScanResultsScoreCacheFilter(Supplier<List<ScanResult>> resultsSupplier) {
Jeremy Joslinba242732017-01-24 17:16:42 -0800616 List<ScanResult> scanResults = resultsSupplier.get();
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800617 final int size = scanResults.size();
618 mScanResultKeys = new ArraySet<>(size);
619 for (int i = 0; i < size; i++) {
Jeremy Joslinba242732017-01-24 17:16:42 -0800620 ScanResult scanResult = scanResults.get(i);
Stephen Chenfde900d2017-02-14 16:40:21 -0800621 NetworkKey key = NetworkKey.createFromScanResult(scanResult);
622 if (key != null) {
623 mScanResultKeys.add(key);
624 }
Jeremy Joslinba242732017-01-24 17:16:42 -0800625 }
626 }
627
628 @Override
629 public List<ScoredNetwork> apply(List<ScoredNetwork> scoredNetworks) {
630 if (mScanResultKeys.isEmpty() || scoredNetworks.isEmpty()) {
631 return Collections.emptyList();
632 }
633
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800634 List<ScoredNetwork> filteredScores = new ArrayList<>();
635 for (int i = 0; i < scoredNetworks.size(); i++) {
636 final ScoredNetwork scoredNetwork = scoredNetworks.get(i);
637 if (mScanResultKeys.contains(scoredNetwork.networkKey)) {
638 filteredScores.add(scoredNetwork);
Jeremy Joslinba242732017-01-24 17:16:42 -0800639 }
Jeremy Joslinba242732017-01-24 17:16:42 -0800640 }
641
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800642 return filteredScores;
Jeremy Joslinba242732017-01-24 17:16:42 -0800643 }
644 }
645
Jeremy Joslin35f34ea2017-02-03 09:13:51 -0800646 private boolean callerCanRequestScores() {
Jeremy Joslin5519d7c2017-01-06 14:36:54 -0800647 // REQUEST_NETWORK_SCORES is a signature only permission.
648 return mContext.checkCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES) ==
649 PackageManager.PERMISSION_GRANTED;
650 }
651
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700652 @Override
653 public boolean clearScores() {
Jeremy Joslin5519d7c2017-01-06 14:36:54 -0800654 // Only the active scorer or the system should be allowed to flush all scores.
Jeremy Joslin35f34ea2017-02-03 09:13:51 -0800655 if (isCallerActiveScorer(getCallingUid()) || callerCanRequestScores()) {
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800656 final long token = Binder.clearCallingIdentity();
657 try {
658 clearInternal();
659 return true;
660 } finally {
661 Binder.restoreCallingIdentity(token);
662 }
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700663 } else {
664 throw new SecurityException(
665 "Caller is neither the active scorer nor the scorer manager.");
666 }
667 }
668
669 @Override
670 public boolean setActiveScorer(String packageName) {
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800671 // Only the system can set the active scorer
Jeremy Joslin3bddadd2017-03-21 16:16:46 -0700672 if (!isCallerSystemProcess(getCallingUid()) && !callerCanRequestScores()) {
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800673 throw new SecurityException(
674 "Caller is neither the system process nor a score requester.");
675 }
Jeremy Josline9052a32017-02-27 15:47:54 -0800676
677 return mNetworkScorerAppManager.setActiveScorer(packageName);
Jeff Davidson26fd1432014-07-29 09:39:52 -0700678 }
679
Jeremy Joslin134c9d32017-01-09 16:22:20 -0800680 /**
681 * Determine whether the application with the given UID is the enabled scorer.
682 *
683 * @param callingUid the UID to check
684 * @return true if the provided UID is the active scorer, false otherwise.
685 */
686 @Override
687 public boolean isCallerActiveScorer(int callingUid) {
688 synchronized (mServiceConnectionLock) {
Jeremy Joslin37e877b2017-02-02 11:06:14 -0800689 return mServiceConnection != null
690 && mServiceConnection.mAppData.packageUid == callingUid;
Jeremy Joslin134c9d32017-01-09 16:22:20 -0800691 }
692 }
693
Jeremy Joslina5172f62017-02-02 14:27:05 -0800694 private boolean isCallerSystemProcess(int callingUid) {
695 return callingUid == Process.SYSTEM_UID;
696 }
697
Jeremy Joslin6c1ca282017-01-10 13:08:32 -0800698 /**
699 * Obtain the package name of the current active network scorer.
700 *
701 * @return the full package name of the current active scorer, or null if there is no active
702 * scorer.
703 */
704 @Override
705 public String getActiveScorerPackage() {
706 synchronized (mServiceConnectionLock) {
707 if (mServiceConnection != null) {
Jeremy Joslin37e877b2017-02-02 11:06:14 -0800708 return mServiceConnection.getPackageName();
Jeremy Joslin6c1ca282017-01-10 13:08:32 -0800709 }
710 }
711 return null;
712 }
713
Jeremy Joslina5172f62017-02-02 14:27:05 -0800714 /**
715 * Returns metadata about the active scorer or <code>null</code> if there is no active scorer.
716 */
717 @Override
718 public NetworkScorerAppData getActiveScorer() {
719 // Only the system can access this data.
720 if (isCallerSystemProcess(getCallingUid()) || callerCanRequestScores()) {
721 synchronized (mServiceConnectionLock) {
722 if (mServiceConnection != null) {
723 return mServiceConnection.mAppData;
724 }
725 }
726 } else {
727 throw new SecurityException(
728 "Caller is neither the system process nor a score requester.");
729 }
730
731 return null;
732 }
733
Jeremy Joslinf95c8652017-02-09 15:32:04 -0800734 /**
735 * Returns the list of available scorer apps. The list will be empty if there are
736 * no valid scorers.
737 */
738 @Override
739 public List<NetworkScorerAppData> getAllValidScorers() {
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800740 // Only the system can access this data.
Jeremy Joslin3bddadd2017-03-21 16:16:46 -0700741 if (!isCallerSystemProcess(getCallingUid()) && !callerCanRequestScores()) {
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -0800742 throw new SecurityException(
743 "Caller is neither the system process nor a score requester.");
744 }
Jeremy Josline9052a32017-02-27 15:47:54 -0800745
746 return mNetworkScorerAppManager.getAllValidScorers();
Jeremy Joslinf95c8652017-02-09 15:32:04 -0800747 }
748
Jeff Davidson26fd1432014-07-29 09:39:52 -0700749 @Override
750 public void disableScoring() {
Jeremy Joslin5519d7c2017-01-06 14:36:54 -0800751 // Only the active scorer or the system should be allowed to disable scoring.
Jeremy Joslin3bddadd2017-03-21 16:16:46 -0700752 if (!isCallerActiveScorer(getCallingUid()) && !callerCanRequestScores()) {
Jeff Davidson26fd1432014-07-29 09:39:52 -0700753 throw new SecurityException(
754 "Caller is neither the active scorer nor the scorer manager.");
Jeff Davidsonb096bdc2014-07-01 12:29:11 -0700755 }
Jeremy Josline9052a32017-02-27 15:47:54 -0800756
757 // no-op for now but we could write to the setting if needed.
Jeff Davidson26fd1432014-07-29 09:39:52 -0700758 }
759
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700760 /** Clear scores. Callers are responsible for checking permissions as appropriate. */
761 private void clearInternal() {
Jeremy Joslinba242732017-01-24 17:16:42 -0800762 sendCacheUpdateCallback(new BiConsumer<INetworkScoreCache, Object>() {
Amin Shaikh972e2362016-12-07 14:08:09 -0800763 @Override
Jeremy Joslinba242732017-01-24 17:16:42 -0800764 public void accept(INetworkScoreCache networkScoreCache, Object cookie) {
Amin Shaikh972e2362016-12-07 14:08:09 -0800765 try {
766 networkScoreCache.clearScores();
767 } catch (RemoteException e) {
768 if (Log.isLoggable(TAG, Log.VERBOSE)) {
769 Log.v(TAG, "Unable to clear scores", e);
770 }
Jeff Davidson14f1ec02014-04-29 11:58:26 -0700771 }
772 }
Amin Shaikh972e2362016-12-07 14:08:09 -0800773 }, getScoreCacheLists());
Jeff Davidson14f1ec02014-04-29 11:58:26 -0700774 }
775
776 @Override
Jeremy Joslinc5ac5872016-11-30 15:05:40 -0800777 public void registerNetworkScoreCache(int networkType,
778 INetworkScoreCache scoreCache,
779 int filterType) {
Jeremy Joslin5519d7c2017-01-06 14:36:54 -0800780 mContext.enforceCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES, TAG);
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800781 final long token = Binder.clearCallingIdentity();
782 try {
783 synchronized (mScoreCaches) {
784 RemoteCallbackList<INetworkScoreCache> callbackList = mScoreCaches.get(networkType);
785 if (callbackList == null) {
786 callbackList = new RemoteCallbackList<>();
787 mScoreCaches.put(networkType, callbackList);
Amin Shaikh972e2362016-12-07 14:08:09 -0800788 }
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800789 if (!callbackList.register(scoreCache, filterType)) {
790 if (callbackList.getRegisteredCallbackCount() == 0) {
791 mScoreCaches.remove(networkType);
792 }
793 if (Log.isLoggable(TAG, Log.VERBOSE)) {
794 Log.v(TAG, "Unable to register NetworkScoreCache for type " + networkType);
795 }
Amin Shaikh972e2362016-12-07 14:08:09 -0800796 }
797 }
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800798 } finally {
799 Binder.restoreCallingIdentity(token);
Amin Shaikh972e2362016-12-07 14:08:09 -0800800 }
801 }
802
803 @Override
804 public void unregisterNetworkScoreCache(int networkType, INetworkScoreCache scoreCache) {
Jeremy Joslin5519d7c2017-01-06 14:36:54 -0800805 mContext.enforceCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES, TAG);
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800806 final long token = Binder.clearCallingIdentity();
807 try {
808 synchronized (mScoreCaches) {
809 RemoteCallbackList<INetworkScoreCache> callbackList = mScoreCaches.get(networkType);
810 if (callbackList == null || !callbackList.unregister(scoreCache)) {
811 if (Log.isLoggable(TAG, Log.VERBOSE)) {
812 Log.v(TAG, "Unable to unregister NetworkScoreCache for type "
813 + networkType);
814 }
815 } else if (callbackList.getRegisteredCallbackCount() == 0) {
816 mScoreCaches.remove(networkType);
Amin Shaikh972e2362016-12-07 14:08:09 -0800817 }
Amin Shaikh972e2362016-12-07 14:08:09 -0800818 }
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800819 } finally {
820 Binder.restoreCallingIdentity(token);
Jeff Davidson14f1ec02014-04-29 11:58:26 -0700821 }
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700822 }
823
824 @Override
Jeremy Joslind1daf6d2016-11-28 17:47:35 -0800825 public RecommendationResult requestRecommendation(RecommendationRequest request) {
Jeremy Joslin5519d7c2017-01-06 14:36:54 -0800826 mContext.enforceCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES, TAG);
Jeremy Joslin145c3432016-12-09 13:11:51 -0800827 throwIfCalledOnMainThread();
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800828 final long token = Binder.clearCallingIdentity();
829 try {
830 final INetworkRecommendationProvider provider = getRecommendationProvider();
831 if (provider != null) {
832 try {
Jeremy Joslin3452b692017-01-17 15:48:13 -0800833 final RequestRecommendationCaller caller = mReqRecommendationCallerRef.get();
834 return caller.getRecommendationResult(provider, request);
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800835 } catch (RemoteException | TimeoutException e) {
836 Log.w(TAG, "Failed to request a recommendation.", e);
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800837 // TODO: 12/15/16 - Keep track of failures.
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800838 }
Jeremy Joslin145c3432016-12-09 13:11:51 -0800839 }
Jeremy Joslin145c3432016-12-09 13:11:51 -0800840
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800841 if (DBG) {
842 Log.d(TAG, "Returning the default network recommendation.");
843 }
Jeremy Joslin145c3432016-12-09 13:11:51 -0800844
Jeremy Joslin26a45e52017-01-18 11:55:17 -0800845 if (request != null && request.getDefaultWifiConfig() != null) {
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800846 return RecommendationResult.createConnectRecommendation(
Jeremy Joslin26a45e52017-01-18 11:55:17 -0800847 request.getDefaultWifiConfig());
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800848 }
849 return RecommendationResult.createDoNotConnectRecommendation();
850 } finally {
851 Binder.restoreCallingIdentity(token);
Jeremy Joslind1daf6d2016-11-28 17:47:35 -0800852 }
Jeremy Joslind1daf6d2016-11-28 17:47:35 -0800853 }
854
Jeremy Joslin998d7ca2016-12-28 15:56:46 -0800855 /**
856 * Request a recommendation for the best network to connect to
857 * taking into account the inputs from the {@link RecommendationRequest}.
858 *
859 * @param request a {@link RecommendationRequest} instance containing the details of the request
860 * @param remoteCallback a {@link IRemoteCallback} instance to invoke when the recommendation
861 * is available.
862 * @throws SecurityException if the caller is not the system
863 */
864 @Override
865 public void requestRecommendationAsync(RecommendationRequest request,
866 RemoteCallback remoteCallback) {
Jeremy Joslin6397ab52017-01-18 15:12:01 -0800867 mContext.enforceCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES, TAG);
Jeremy Joslin998d7ca2016-12-28 15:56:46 -0800868
Jeremy Joslince73c6f2016-12-29 14:49:38 -0800869 final OneTimeCallback oneTimeCallback = new OneTimeCallback(remoteCallback);
870 final Pair<RecommendationRequest, OneTimeCallback> pair =
871 Pair.create(request, oneTimeCallback);
872 final Message timeoutMsg = mHandler.obtainMessage(
873 ServiceHandler.MSG_RECOMMENDATION_REQUEST_TIMEOUT, pair);
874 final INetworkRecommendationProvider provider = getRecommendationProvider();
875 final long token = Binder.clearCallingIdentity();
876 try {
877 if (provider != null) {
878 try {
879 mHandler.sendMessageDelayed(timeoutMsg, mRecommendationRequestTimeoutMs);
880 provider.requestRecommendation(request, new IRemoteCallback.Stub() {
881 @Override
882 public void sendResult(Bundle data) throws RemoteException {
883 // Remove the timeout message
884 mHandler.removeMessages(timeoutMsg.what, pair);
885 oneTimeCallback.sendResult(data);
886 }
887 }, 0 /*sequence*/);
888 return;
889 } catch (RemoteException e) {
890 Log.w(TAG, "Failed to request a recommendation.", e);
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800891 // TODO: 12/15/16 - Keep track of failures.
Jeremy Joslince73c6f2016-12-29 14:49:38 -0800892 // Remove the timeout message
893 mHandler.removeMessages(timeoutMsg.what, pair);
894 // Will fall through and send back the default recommendation.
895 }
896 }
897 } finally {
898 Binder.restoreCallingIdentity(token);
Jeremy Joslin998d7ca2016-12-28 15:56:46 -0800899 }
900
Jeremy Joslince73c6f2016-12-29 14:49:38 -0800901 // Else send back the default recommendation.
902 sendDefaultRecommendationResponse(request, oneTimeCallback);
Jeremy Joslin998d7ca2016-12-28 15:56:46 -0800903 }
904
Jeremy Joslind1daf6d2016-11-28 17:47:35 -0800905 @Override
Jeremy Joslinb2087a12016-12-13 16:11:51 -0800906 public boolean requestScores(NetworkKey[] networks) {
Jeremy Joslin5519d7c2017-01-06 14:36:54 -0800907 mContext.enforceCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES, TAG);
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800908 final long token = Binder.clearCallingIdentity();
909 try {
910 final INetworkRecommendationProvider provider = getRecommendationProvider();
911 if (provider != null) {
912 try {
913 provider.requestScores(networks);
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800914 // TODO: 12/15/16 - Consider pushing null scores into the cache to
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800915 // prevent repeated requests for the same scores.
916 return true;
917 } catch (RemoteException e) {
918 Log.w(TAG, "Failed to request scores.", e);
Jeremy Josline71fe2b2017-01-25 11:40:08 -0800919 // TODO: 12/15/16 - Keep track of failures.
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800920 }
Jeremy Joslin145c3432016-12-09 13:11:51 -0800921 }
Jeremy Joslin8f5521a2016-12-20 14:36:20 -0800922 return false;
923 } finally {
924 Binder.restoreCallingIdentity(token);
Jeremy Joslin145c3432016-12-09 13:11:51 -0800925 }
Jeremy Joslinb2087a12016-12-13 16:11:51 -0800926 }
927
928 @Override
Amin Shaikh972e2362016-12-07 14:08:09 -0800929 protected void dump(final FileDescriptor fd, final PrintWriter writer, final String[] args) {
Jeff Sharkeyfe9a53b2017-03-31 14:08:23 -0600930 if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return;
Jeremy Joslin534b6cf2017-01-25 18:20:21 -0800931 final long token = Binder.clearCallingIdentity();
932 try {
933 NetworkScorerAppData currentScorer = mNetworkScorerAppManager.getActiveScorer();
934 if (currentScorer == null) {
935 writer.println("Scoring is disabled.");
936 return;
937 }
Jeremy Joslin37e877b2017-02-02 11:06:14 -0800938 writer.println("Current scorer: " + currentScorer);
Jeremy Joslin4ad7ca02017-02-13 11:35:03 -0800939 writer.println("RecommendationRequestTimeoutMs: " + mRecommendationRequestTimeoutMs);
Jeff Davidson14f1ec02014-04-29 11:58:26 -0700940
Jeremy Joslin534b6cf2017-01-25 18:20:21 -0800941 sendCacheUpdateCallback(new BiConsumer<INetworkScoreCache, Object>() {
942 @Override
943 public void accept(INetworkScoreCache networkScoreCache, Object cookie) {
944 try {
945 TransferPipe.dumpAsync(networkScoreCache.asBinder(), fd, args);
946 } catch (IOException | RemoteException e) {
947 writer.println("Failed to dump score cache: " + e);
948 }
949 }
950 }, getScoreCacheLists());
951
952 synchronized (mServiceConnectionLock) {
953 if (mServiceConnection != null) {
954 mServiceConnection.dump(fd, writer, args);
955 } else {
956 writer.println("ScoringServiceConnection: null");
Amin Shaikh972e2362016-12-07 14:08:09 -0800957 }
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700958 }
Jeremy Joslin534b6cf2017-01-25 18:20:21 -0800959 writer.flush();
960 } finally {
961 Binder.restoreCallingIdentity(token);
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700962 }
Jeff Davidson6a4b2202014-04-16 17:29:40 -0700963 }
Jeff Davidson14f1ec02014-04-29 11:58:26 -0700964
965 /**
Amin Shaikh972e2362016-12-07 14:08:09 -0800966 * Returns a {@link Collection} of all {@link RemoteCallbackList}s that are currently active.
Jeff Davidson14f1ec02014-04-29 11:58:26 -0700967 *
968 * <p>May be used to perform an action on all score caches without potentially strange behavior
969 * if a new scorer is registered during that action's execution.
970 */
Amin Shaikh972e2362016-12-07 14:08:09 -0800971 private Collection<RemoteCallbackList<INetworkScoreCache>> getScoreCacheLists() {
Jeff Davidson14f1ec02014-04-29 11:58:26 -0700972 synchronized (mScoreCaches) {
Amin Shaikh972e2362016-12-07 14:08:09 -0800973 return new ArrayList<>(mScoreCaches.values());
974 }
975 }
976
Jeremy Joslinba242732017-01-24 17:16:42 -0800977 private void sendCacheUpdateCallback(BiConsumer<INetworkScoreCache, Object> consumer,
Amin Shaikh972e2362016-12-07 14:08:09 -0800978 Collection<RemoteCallbackList<INetworkScoreCache>> remoteCallbackLists) {
979 for (RemoteCallbackList<INetworkScoreCache> callbackList : remoteCallbackLists) {
980 synchronized (callbackList) { // Ensure only one active broadcast per RemoteCallbackList
981 final int count = callbackList.beginBroadcast();
982 try {
983 for (int i = 0; i < count; i++) {
Jeremy Joslinba242732017-01-24 17:16:42 -0800984 consumer.accept(callbackList.getBroadcastItem(i),
Jeremy Joslin7890e192017-02-06 11:14:34 -0800985 callbackList.getBroadcastCookie(i));
Amin Shaikh972e2362016-12-07 14:08:09 -0800986 }
987 } finally {
988 callbackList.finishBroadcast();
989 }
990 }
Jeff Davidson14f1ec02014-04-29 11:58:26 -0700991 }
992 }
Jeremy Joslindd251ef2016-03-14 11:17:41 -0700993
Jeremy Joslin145c3432016-12-09 13:11:51 -0800994 private void throwIfCalledOnMainThread() {
995 if (Thread.currentThread() == mContext.getMainLooper().getThread()) {
996 throw new RuntimeException("Cannot invoke on the main thread");
997 }
998 }
999
1000 @Nullable
1001 private INetworkRecommendationProvider getRecommendationProvider() {
1002 synchronized (mServiceConnectionLock) {
1003 if (mServiceConnection != null) {
1004 return mServiceConnection.getRecommendationProvider();
1005 }
1006 }
1007 return null;
1008 }
1009
Jeremy Joslince73c6f2016-12-29 14:49:38 -08001010 @VisibleForTesting
Jeremy Joslincb594f32017-01-03 17:31:23 -08001011 public void refreshRecommendationRequestTimeoutMs() {
1012 final ContentResolver cr = mContext.getContentResolver();
1013 long timeoutMs = Settings.Global.getLong(cr,
1014 Global.NETWORK_RECOMMENDATION_REQUEST_TIMEOUT_MS, -1L /*default*/);
1015 if (timeoutMs < 0) {
1016 timeoutMs = TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS;
1017 }
1018 if (DBG) Log.d(TAG, "Updating the recommendation request timeout to " + timeoutMs + " ms");
1019 mRecommendationRequestTimeoutMs = timeoutMs;
Jeremy Joslin3452b692017-01-17 15:48:13 -08001020 mReqRecommendationCallerRef.set(new RequestRecommendationCaller(timeoutMs));
Jeremy Joslince73c6f2016-12-29 14:49:38 -08001021 }
1022
Jeremy Joslindd251ef2016-03-14 11:17:41 -07001023 private static class ScoringServiceConnection implements ServiceConnection {
Jeremy Joslin37e877b2017-02-02 11:06:14 -08001024 private final NetworkScorerAppData mAppData;
Jeremy Joslin145c3432016-12-09 13:11:51 -08001025 private volatile boolean mBound = false;
1026 private volatile boolean mConnected = false;
1027 private volatile INetworkRecommendationProvider mRecommendationProvider;
Jeremy Joslindd251ef2016-03-14 11:17:41 -07001028
Jeremy Joslin37e877b2017-02-02 11:06:14 -08001029 ScoringServiceConnection(NetworkScorerAppData appData) {
1030 mAppData = appData;
Jeremy Joslindd251ef2016-03-14 11:17:41 -07001031 }
1032
1033 void connect(Context context) {
Jeremy Joslindd251ef2016-03-14 11:17:41 -07001034 if (!mBound) {
Joe LaPenna25e7ec22016-12-27 14:50:14 -08001035 Intent service = new Intent(NetworkScoreManager.ACTION_RECOMMEND_NETWORKS);
Jeremy Joslin37e877b2017-02-02 11:06:14 -08001036 service.setComponent(mAppData.getRecommendationServiceComponent());
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -07001037 mBound = context.bindServiceAsUser(service, this,
1038 Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
1039 UserHandle.SYSTEM);
1040 if (!mBound) {
1041 Log.w(TAG, "Bind call failed for " + service);
1042 } else {
1043 if (DBG) Log.d(TAG, "ScoringServiceConnection bound.");
1044 }
Jeremy Joslindd251ef2016-03-14 11:17:41 -07001045 }
1046 }
1047
1048 void disconnect(Context context) {
1049 try {
1050 if (mBound) {
1051 mBound = false;
1052 context.unbindService(this);
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -07001053 if (DBG) Log.d(TAG, "ScoringServiceConnection unbound.");
Jeremy Joslindd251ef2016-03-14 11:17:41 -07001054 }
1055 } catch (RuntimeException e) {
1056 Log.e(TAG, "Unbind failed.", e);
1057 }
Jeremy Joslin145c3432016-12-09 13:11:51 -08001058
1059 mRecommendationProvider = null;
1060 }
1061
1062 INetworkRecommendationProvider getRecommendationProvider() {
1063 return mRecommendationProvider;
Jeremy Joslindd251ef2016-03-14 11:17:41 -07001064 }
1065
Jeremy Joslin37e877b2017-02-02 11:06:14 -08001066 String getPackageName() {
1067 return mAppData.getRecommendationServiceComponent().getPackageName();
1068 }
1069
Jeremy Joslindd251ef2016-03-14 11:17:41 -07001070 @Override
1071 public void onServiceConnected(ComponentName name, IBinder service) {
1072 if (DBG) Log.d(TAG, "ScoringServiceConnection: " + name.flattenToString());
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -07001073 mConnected = true;
Jeremy Joslin145c3432016-12-09 13:11:51 -08001074 mRecommendationProvider = INetworkRecommendationProvider.Stub.asInterface(service);
Jeremy Joslindd251ef2016-03-14 11:17:41 -07001075 }
1076
1077 @Override
1078 public void onServiceDisconnected(ComponentName name) {
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -07001079 if (DBG) {
1080 Log.d(TAG, "ScoringServiceConnection, disconnected: " + name.flattenToString());
1081 }
1082 mConnected = false;
Jeremy Joslin145c3432016-12-09 13:11:51 -08001083 mRecommendationProvider = null;
Jeremy Joslindd251ef2016-03-14 11:17:41 -07001084 }
1085
1086 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
Jeremy Joslin37e877b2017-02-02 11:06:14 -08001087 writer.println("ScoringServiceConnection: "
1088 + mAppData.getRecommendationServiceComponent()
1089 + ", bound: " + mBound
Jeremy Joslin1ec8cd952016-05-26 15:28:48 -07001090 + ", connected: " + mConnected);
Jeremy Joslindd251ef2016-03-14 11:17:41 -07001091 }
1092 }
Jeremy Joslin145c3432016-12-09 13:11:51 -08001093
1094 /**
1095 * Executes the async requestRecommendation() call with a timeout.
1096 */
1097 private static final class RequestRecommendationCaller
1098 extends TimedRemoteCaller<RecommendationResult> {
1099 private final IRemoteCallback mCallback;
1100
1101 RequestRecommendationCaller(long callTimeoutMillis) {
1102 super(callTimeoutMillis);
1103 mCallback = new IRemoteCallback.Stub() {
1104 @Override
1105 public void sendResult(Bundle data) throws RemoteException {
1106 final RecommendationResult result =
1107 data.getParcelable(EXTRA_RECOMMENDATION_RESULT);
1108 final int sequence = data.getInt(EXTRA_SEQUENCE, -1);
Jeremy Joslin4ad7ca02017-02-13 11:35:03 -08001109 if (VERBOSE) Log.v(TAG, "callback received for sequence " + sequence);
Jeremy Joslin145c3432016-12-09 13:11:51 -08001110 onRemoteMethodResult(result, sequence);
1111 }
1112 };
1113 }
1114
1115 /**
1116 * Runs the requestRecommendation() call on the given {@link INetworkRecommendationProvider}
1117 * instance.
1118 *
1119 * @param target the {@link INetworkRecommendationProvider} to request a recommendation
1120 * from
1121 * @param request the {@link RecommendationRequest} from the calling client
1122 * @return a {@link RecommendationResult} from the provider
1123 * @throws RemoteException if the call failed
1124 * @throws TimeoutException if the call took longer than the set timeout
1125 */
1126 RecommendationResult getRecommendationResult(INetworkRecommendationProvider target,
1127 RecommendationRequest request) throws RemoteException, TimeoutException {
1128 final int sequence = onBeforeRemoteCall();
Jeremy Joslin4ad7ca02017-02-13 11:35:03 -08001129 if (VERBOSE) Log.v(TAG, "getRecommendationResult() seq=" + sequence);
Jeremy Joslin145c3432016-12-09 13:11:51 -08001130 target.requestRecommendation(request, mCallback, sequence);
1131 return getResultTimed(sequence);
1132 }
1133 }
Jeremy Joslince73c6f2016-12-29 14:49:38 -08001134
1135 /**
1136 * A wrapper around {@link RemoteCallback} that guarantees
1137 * {@link RemoteCallback#sendResult(Bundle)} will be invoked at most once.
1138 */
1139 @VisibleForTesting
1140 public static final class OneTimeCallback {
1141 private final RemoteCallback mRemoteCallback;
1142 private final AtomicBoolean mCallbackRun;
1143
1144 public OneTimeCallback(RemoteCallback remoteCallback) {
1145 mRemoteCallback = remoteCallback;
1146 mCallbackRun = new AtomicBoolean(false);
1147 }
1148
1149 public void sendResult(Bundle data) {
1150 if (mCallbackRun.compareAndSet(false, true)) {
1151 mRemoteCallback.sendResult(data);
1152 }
1153 }
1154 }
1155
1156 private static void sendDefaultRecommendationResponse(RecommendationRequest request,
1157 OneTimeCallback remoteCallback) {
1158 if (DBG) {
1159 Log.d(TAG, "Returning the default network recommendation.");
1160 }
1161
1162 final RecommendationResult result;
Jeremy Joslin26a45e52017-01-18 11:55:17 -08001163 if (request != null && request.getDefaultWifiConfig() != null) {
Jeremy Joslince73c6f2016-12-29 14:49:38 -08001164 result = RecommendationResult.createConnectRecommendation(
Jeremy Joslin26a45e52017-01-18 11:55:17 -08001165 request.getDefaultWifiConfig());
Jeremy Joslince73c6f2016-12-29 14:49:38 -08001166 } else {
1167 result = RecommendationResult.createDoNotConnectRecommendation();
1168 }
1169
1170 final Bundle data = new Bundle();
1171 data.putParcelable(EXTRA_RECOMMENDATION_RESULT, result);
1172 remoteCallback.sendResult(data);
1173 }
1174
1175 @VisibleForTesting
Jeremy Joslincb594f32017-01-03 17:31:23 -08001176 public final class ServiceHandler extends Handler {
Jeremy Joslince73c6f2016-12-29 14:49:38 -08001177 public static final int MSG_RECOMMENDATION_REQUEST_TIMEOUT = 1;
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -08001178 public static final int MSG_RECOMMENDATIONS_PACKAGE_CHANGED = 2;
Jeremy Joslincb594f32017-01-03 17:31:23 -08001179 public static final int MSG_RECOMMENDATION_REQUEST_TIMEOUT_CHANGED = 3;
Jeremy Joslin9925c6a2017-03-06 10:39:35 -08001180 public static final int MSG_RECOMMENDATION_ENABLED_SETTING_CHANGED = 4;
Jeremy Joslince73c6f2016-12-29 14:49:38 -08001181
1182 public ServiceHandler(Looper looper) {
1183 super(looper);
1184 }
1185
1186 @Override
1187 public void handleMessage(Message msg) {
1188 final int what = msg.what;
1189 switch (what) {
1190 case MSG_RECOMMENDATION_REQUEST_TIMEOUT:
1191 if (DBG) {
1192 Log.d(TAG, "Network recommendation request timed out.");
1193 }
1194 final Pair<RecommendationRequest, OneTimeCallback> pair =
1195 (Pair<RecommendationRequest, OneTimeCallback>) msg.obj;
1196 final RecommendationRequest request = pair.first;
1197 final OneTimeCallback remoteCallback = pair.second;
1198 sendDefaultRecommendationResponse(request, remoteCallback);
1199 break;
1200
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -08001201 case MSG_RECOMMENDATIONS_PACKAGE_CHANGED:
Jeremy Joslin9925c6a2017-03-06 10:39:35 -08001202 case MSG_RECOMMENDATION_ENABLED_SETTING_CHANGED:
Jeremy Joslinee3fb5c2017-02-13 13:44:11 -08001203 refreshBinding();
Jeremy Joslincb594f32017-01-03 17:31:23 -08001204 break;
1205
1206 case MSG_RECOMMENDATION_REQUEST_TIMEOUT_CHANGED:
1207 refreshRecommendationRequestTimeoutMs();
1208 break;
1209
Jeremy Joslince73c6f2016-12-29 14:49:38 -08001210 default:
1211 Log.w(TAG,"Unknown message: " + what);
1212 }
1213 }
1214 }
Jeff Davidson6a4b2202014-04-16 17:29:40 -07001215}