/*
|
* Copyright (C) 2014 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License
|
*/
|
|
package com.android.server;
|
|
import android.Manifest.permission;
|
import android.annotation.Nullable;
|
import android.content.BroadcastReceiver;
|
import android.content.ComponentName;
|
import android.content.ContentResolver;
|
import android.content.Context;
|
import android.content.Intent;
|
import android.content.IntentFilter;
|
import android.content.ServiceConnection;
|
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManagerInternal;
|
import android.database.ContentObserver;
|
import android.location.LocationManager;
|
import android.net.INetworkRecommendationProvider;
|
import android.net.INetworkScoreCache;
|
import android.net.INetworkScoreService;
|
import android.net.NetworkKey;
|
import android.net.NetworkScoreManager;
|
import android.net.NetworkScorerAppData;
|
import android.net.ScoredNetwork;
|
import android.net.Uri;
|
import android.net.wifi.ScanResult;
|
import android.net.wifi.WifiInfo;
|
import android.net.wifi.WifiManager;
|
import android.net.wifi.WifiScanner;
|
import android.os.Binder;
|
import android.os.Build;
|
import android.os.Handler;
|
import android.os.IBinder;
|
import android.os.Looper;
|
import android.os.Message;
|
import android.os.RemoteCallbackList;
|
import android.os.RemoteException;
|
import android.os.UserHandle;
|
import android.provider.Settings.Global;
|
import android.text.TextUtils;
|
import android.util.ArrayMap;
|
import android.util.ArraySet;
|
import android.util.IntArray;
|
import android.util.Log;
|
|
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.annotations.VisibleForTesting;
|
import com.android.internal.content.PackageMonitor;
|
import com.android.internal.os.TransferPipe;
|
import com.android.internal.telephony.SmsApplication;
|
import com.android.internal.util.DumpUtils;
|
|
import java.io.FileDescriptor;
|
import java.io.IOException;
|
import java.io.PrintWriter;
|
import java.util.ArrayList;
|
import java.util.Collection;
|
import java.util.Collections;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Set;
|
import java.util.function.BiConsumer;
|
import java.util.function.Function;
|
import java.util.function.Supplier;
|
import java.util.function.UnaryOperator;
|
|
/**
|
* Backing service for {@link android.net.NetworkScoreManager}.
|
* @hide
|
*/
|
public class NetworkScoreService extends INetworkScoreService.Stub {
|
private static final String TAG = "NetworkScoreService";
|
private static final boolean DBG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG);
|
private static final boolean VERBOSE = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.VERBOSE);
|
|
private final Context mContext;
|
private final NetworkScorerAppManager mNetworkScorerAppManager;
|
@GuardedBy("mScoreCaches")
|
private final Map<Integer, RemoteCallbackList<INetworkScoreCache>> mScoreCaches;
|
/** Lock used to update mPackageMonitor when scorer package changes occur. */
|
private final Object mPackageMonitorLock = new Object();
|
private final Object mServiceConnectionLock = new Object();
|
private final Handler mHandler;
|
private final DispatchingContentObserver mRecommendationSettingsObserver;
|
private final ContentObserver mUseOpenWifiPackageObserver;
|
private final Function<NetworkScorerAppData, ScoringServiceConnection> mServiceConnProducer;
|
|
@GuardedBy("mPackageMonitorLock")
|
private NetworkScorerPackageMonitor mPackageMonitor;
|
@GuardedBy("mServiceConnectionLock")
|
private ScoringServiceConnection mServiceConnection;
|
|
private BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
|
@Override
|
public void onReceive(Context context, Intent intent) {
|
final String action = intent.getAction();
|
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
|
if (DBG) Log.d(TAG, "Received " + action + " for userId " + userId);
|
if (userId == UserHandle.USER_NULL) return;
|
|
if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
|
onUserUnlocked(userId);
|
}
|
}
|
};
|
|
private BroadcastReceiver mLocationModeReceiver = new BroadcastReceiver() {
|
@Override
|
public void onReceive(Context context, Intent intent) {
|
final String action = intent.getAction();
|
if (LocationManager.MODE_CHANGED_ACTION.equals(action)) {
|
refreshBinding();
|
}
|
}
|
};
|
|
public static final class Lifecycle extends SystemService {
|
private final NetworkScoreService mService;
|
|
public Lifecycle(Context context) {
|
super(context);
|
mService = new NetworkScoreService(context);
|
}
|
|
@Override
|
public void onStart() {
|
Log.i(TAG, "Registering " + Context.NETWORK_SCORE_SERVICE);
|
publishBinderService(Context.NETWORK_SCORE_SERVICE, mService);
|
}
|
|
@Override
|
public void onBootPhase(int phase) {
|
if (phase == PHASE_SYSTEM_SERVICES_READY) {
|
mService.systemReady();
|
} else if (phase == PHASE_BOOT_COMPLETED) {
|
mService.systemRunning();
|
}
|
}
|
}
|
|
/**
|
* Clears scores when the active scorer package is no longer valid and
|
* manages the service connection.
|
*/
|
private class NetworkScorerPackageMonitor extends PackageMonitor {
|
final String mPackageToWatch;
|
|
private NetworkScorerPackageMonitor(String packageToWatch) {
|
mPackageToWatch = packageToWatch;
|
}
|
|
@Override
|
public void onPackageAdded(String packageName, int uid) {
|
evaluateBinding(packageName, true /* forceUnbind */);
|
}
|
|
@Override
|
public void onPackageRemoved(String packageName, int uid) {
|
evaluateBinding(packageName, true /* forceUnbind */);
|
}
|
|
@Override
|
public void onPackageModified(String packageName) {
|
evaluateBinding(packageName, false /* forceUnbind */);
|
}
|
|
@Override
|
public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
|
if (doit) { // "doit" means the force stop happened instead of just being queried for.
|
for (String packageName : packages) {
|
evaluateBinding(packageName, true /* forceUnbind */);
|
}
|
}
|
return super.onHandleForceStop(intent, packages, uid, doit);
|
}
|
|
@Override
|
public void onPackageUpdateFinished(String packageName, int uid) {
|
evaluateBinding(packageName, true /* forceUnbind */);
|
}
|
|
private void evaluateBinding(String changedPackageName, boolean forceUnbind) {
|
if (!mPackageToWatch.equals(changedPackageName)) {
|
// Early exit when we don't care about the package that has changed.
|
return;
|
}
|
|
if (DBG) {
|
Log.d(TAG, "Evaluating binding for: " + changedPackageName
|
+ ", forceUnbind=" + forceUnbind);
|
}
|
|
final NetworkScorerAppData activeScorer = mNetworkScorerAppManager.getActiveScorer();
|
if (activeScorer == null) {
|
// Package change has invalidated a scorer, this will also unbind any service
|
// connection.
|
if (DBG) Log.d(TAG, "No active scorers available.");
|
refreshBinding();
|
} else { // The scoring service changed in some way.
|
if (forceUnbind) {
|
unbindFromScoringServiceIfNeeded();
|
}
|
if (DBG) {
|
Log.d(TAG, "Binding to " + activeScorer.getRecommendationServiceComponent()
|
+ " if needed.");
|
}
|
bindToScoringServiceIfNeeded(activeScorer);
|
}
|
}
|
}
|
|
/**
|
* Dispatches observed content changes to a handler for further processing.
|
*/
|
@VisibleForTesting
|
public static class DispatchingContentObserver extends ContentObserver {
|
final private Map<Uri, Integer> mUriEventMap;
|
final private Context mContext;
|
final private Handler mHandler;
|
|
public DispatchingContentObserver(Context context, Handler handler) {
|
super(handler);
|
mContext = context;
|
mHandler = handler;
|
mUriEventMap = new ArrayMap<>();
|
}
|
|
void observe(Uri uri, int what) {
|
mUriEventMap.put(uri, what);
|
final ContentResolver resolver = mContext.getContentResolver();
|
resolver.registerContentObserver(uri, false /*notifyForDescendants*/, this);
|
}
|
|
@Override
|
public void onChange(boolean selfChange) {
|
onChange(selfChange, null);
|
}
|
|
@Override
|
public void onChange(boolean selfChange, Uri uri) {
|
if (DBG) Log.d(TAG, String.format("onChange(%s, %s)", selfChange, uri));
|
final Integer what = mUriEventMap.get(uri);
|
if (what != null) {
|
mHandler.obtainMessage(what).sendToTarget();
|
} else {
|
Log.w(TAG, "No matching event to send for URI = " + uri);
|
}
|
}
|
}
|
|
public NetworkScoreService(Context context) {
|
this(context, new NetworkScorerAppManager(context),
|
ScoringServiceConnection::new, Looper.myLooper());
|
}
|
|
@VisibleForTesting
|
NetworkScoreService(Context context, NetworkScorerAppManager networkScoreAppManager,
|
Function<NetworkScorerAppData, ScoringServiceConnection> serviceConnProducer,
|
Looper looper) {
|
mContext = context;
|
mNetworkScorerAppManager = networkScoreAppManager;
|
mScoreCaches = new ArrayMap<>();
|
IntentFilter filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED);
|
// TODO: Need to update when we support per-user scorers. http://b/23422763
|
mContext.registerReceiverAsUser(
|
mUserIntentReceiver, UserHandle.SYSTEM, filter, null /* broadcastPermission*/,
|
null /* scheduler */);
|
mHandler = new ServiceHandler(looper);
|
IntentFilter locationModeFilter = new IntentFilter(LocationManager.MODE_CHANGED_ACTION);
|
mContext.registerReceiverAsUser(
|
mLocationModeReceiver, UserHandle.SYSTEM, locationModeFilter,
|
null /* broadcastPermission*/, mHandler);
|
mRecommendationSettingsObserver = new DispatchingContentObserver(context, mHandler);
|
mServiceConnProducer = serviceConnProducer;
|
mUseOpenWifiPackageObserver = new ContentObserver(mHandler) {
|
@Override
|
public void onChange(boolean selfChange, Uri uri, int userId) {
|
Uri useOpenWifiPkgUri = Global.getUriFor(Global.USE_OPEN_WIFI_PACKAGE);
|
if (useOpenWifiPkgUri.equals(uri)) {
|
String useOpenWifiPackage = Global.getString(mContext.getContentResolver(),
|
Global.USE_OPEN_WIFI_PACKAGE);
|
if (!TextUtils.isEmpty(useOpenWifiPackage)) {
|
LocalServices.getService(PackageManagerInternal.class)
|
.grantDefaultPermissionsToDefaultUseOpenWifiApp(useOpenWifiPackage,
|
userId);
|
}
|
}
|
}
|
};
|
mContext.getContentResolver().registerContentObserver(
|
Global.getUriFor(Global.USE_OPEN_WIFI_PACKAGE),
|
false /*notifyForDescendants*/,
|
mUseOpenWifiPackageObserver);
|
// Set a callback for the package manager to query the use open wifi app.
|
LocalServices.getService(PackageManagerInternal.class).setUseOpenWifiAppPackagesProvider(
|
new PackageManagerInternal.PackagesProvider() {
|
@Override
|
public String[] getPackages(int userId) {
|
String useOpenWifiPackage = Global.getString(mContext.getContentResolver(),
|
Global.USE_OPEN_WIFI_PACKAGE);
|
if (!TextUtils.isEmpty(useOpenWifiPackage)) {
|
return new String[]{useOpenWifiPackage};
|
}
|
return null;
|
}
|
});
|
}
|
|
/** Called when the system is ready to run third-party code but before it actually does so. */
|
void systemReady() {
|
if (DBG) Log.d(TAG, "systemReady");
|
registerRecommendationSettingsObserver();
|
}
|
|
/** Called when the system is ready for us to start third-party code. */
|
void systemRunning() {
|
if (DBG) Log.d(TAG, "systemRunning");
|
}
|
|
@VisibleForTesting
|
void onUserUnlocked(int userId) {
|
if (DBG) Log.d(TAG, "onUserUnlocked(" + userId + ")");
|
refreshBinding();
|
}
|
|
private void refreshBinding() {
|
if (DBG) Log.d(TAG, "refreshBinding()");
|
// Make sure the scorer is up-to-date
|
mNetworkScorerAppManager.updateState();
|
mNetworkScorerAppManager.migrateNetworkScorerAppSettingIfNeeded();
|
registerPackageMonitorIfNeeded();
|
bindToScoringServiceIfNeeded();
|
}
|
|
private void registerRecommendationSettingsObserver() {
|
final Uri packageNameUri = Global.getUriFor(Global.NETWORK_RECOMMENDATIONS_PACKAGE);
|
mRecommendationSettingsObserver.observe(packageNameUri,
|
ServiceHandler.MSG_RECOMMENDATIONS_PACKAGE_CHANGED);
|
|
final Uri settingUri = Global.getUriFor(Global.NETWORK_RECOMMENDATIONS_ENABLED);
|
mRecommendationSettingsObserver.observe(settingUri,
|
ServiceHandler.MSG_RECOMMENDATION_ENABLED_SETTING_CHANGED);
|
}
|
|
/**
|
* Ensures the package manager is registered to monitor the current active scorer.
|
* If a discrepancy is found any previous monitor will be cleaned up
|
* and a new monitor will be created.
|
*
|
* This method is idempotent.
|
*/
|
private void registerPackageMonitorIfNeeded() {
|
if (DBG) Log.d(TAG, "registerPackageMonitorIfNeeded()");
|
final NetworkScorerAppData appData = mNetworkScorerAppManager.getActiveScorer();
|
synchronized (mPackageMonitorLock) {
|
// Unregister the current monitor if needed.
|
if (mPackageMonitor != null && (appData == null
|
|| !appData.getRecommendationServicePackageName().equals(
|
mPackageMonitor.mPackageToWatch))) {
|
if (DBG) {
|
Log.d(TAG, "Unregistering package monitor for "
|
+ mPackageMonitor.mPackageToWatch);
|
}
|
mPackageMonitor.unregister();
|
mPackageMonitor = null;
|
}
|
|
// Create and register the monitor if a scorer is active.
|
if (appData != null && mPackageMonitor == null) {
|
mPackageMonitor = new NetworkScorerPackageMonitor(
|
appData.getRecommendationServicePackageName());
|
// TODO: Need to update when we support per-user scorers. http://b/23422763
|
mPackageMonitor.register(mContext, null /* thread */, UserHandle.SYSTEM,
|
false /* externalStorage */);
|
if (DBG) {
|
Log.d(TAG, "Registered package monitor for "
|
+ mPackageMonitor.mPackageToWatch);
|
}
|
}
|
}
|
}
|
|
private void bindToScoringServiceIfNeeded() {
|
if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded");
|
NetworkScorerAppData scorerData = mNetworkScorerAppManager.getActiveScorer();
|
bindToScoringServiceIfNeeded(scorerData);
|
}
|
|
/**
|
* Ensures the service connection is bound to the current active scorer.
|
* If a discrepancy is found any previous connection will be cleaned up
|
* and a new connection will be created.
|
*
|
* This method is idempotent.
|
*/
|
private void bindToScoringServiceIfNeeded(NetworkScorerAppData appData) {
|
if (DBG) Log.d(TAG, "bindToScoringServiceIfNeeded(" + appData + ")");
|
if (appData != null) {
|
synchronized (mServiceConnectionLock) {
|
// If we're connected to a different component then drop it.
|
if (mServiceConnection != null
|
&& !mServiceConnection.getAppData().equals(appData)) {
|
unbindFromScoringServiceIfNeeded();
|
}
|
|
// If we're not connected at all then create a new connection.
|
if (mServiceConnection == null) {
|
mServiceConnection = mServiceConnProducer.apply(appData);
|
}
|
|
// Make sure the connection is connected (idempotent)
|
mServiceConnection.bind(mContext);
|
}
|
} else { // otherwise make sure it isn't bound.
|
unbindFromScoringServiceIfNeeded();
|
}
|
}
|
|
private void unbindFromScoringServiceIfNeeded() {
|
if (DBG) Log.d(TAG, "unbindFromScoringServiceIfNeeded");
|
synchronized (mServiceConnectionLock) {
|
if (mServiceConnection != null) {
|
mServiceConnection.unbind(mContext);
|
if (DBG) Log.d(TAG, "Disconnected from: "
|
+ mServiceConnection.getAppData().getRecommendationServiceComponent());
|
}
|
mServiceConnection = null;
|
}
|
clearInternal();
|
}
|
|
@Override
|
public boolean updateScores(ScoredNetwork[] networks) {
|
if (!isCallerActiveScorer(getCallingUid())) {
|
throw new SecurityException("Caller with UID " + getCallingUid() +
|
" is not the active scorer.");
|
}
|
|
final long token = Binder.clearCallingIdentity();
|
try {
|
// Separate networks by type.
|
Map<Integer, List<ScoredNetwork>> networksByType = new ArrayMap<>();
|
for (ScoredNetwork network : networks) {
|
List<ScoredNetwork> networkList = networksByType.get(network.networkKey.type);
|
if (networkList == null) {
|
networkList = new ArrayList<>();
|
networksByType.put(network.networkKey.type, networkList);
|
}
|
networkList.add(network);
|
}
|
|
// Pass the scores of each type down to the appropriate network scorer.
|
for (final Map.Entry<Integer, List<ScoredNetwork>> entry : networksByType.entrySet()) {
|
final RemoteCallbackList<INetworkScoreCache> callbackList;
|
final boolean isEmpty;
|
synchronized (mScoreCaches) {
|
callbackList = mScoreCaches.get(entry.getKey());
|
isEmpty = callbackList == null
|
|| callbackList.getRegisteredCallbackCount() == 0;
|
}
|
|
if (isEmpty) {
|
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
Log.v(TAG, "No scorer registered for type " + entry.getKey()
|
+ ", discarding");
|
}
|
continue;
|
}
|
|
final BiConsumer<INetworkScoreCache, Object> consumer =
|
FilteringCacheUpdatingConsumer.create(mContext, entry.getValue(),
|
entry.getKey());
|
sendCacheUpdateCallback(consumer, Collections.singleton(callbackList));
|
}
|
|
return true;
|
} finally {
|
Binder.restoreCallingIdentity(token);
|
}
|
}
|
|
/**
|
* A {@link BiConsumer} implementation that filters the given {@link ScoredNetwork}
|
* list (if needed) before invoking {@link INetworkScoreCache#updateScores(List)} on the
|
* accepted {@link INetworkScoreCache} implementation.
|
*/
|
@VisibleForTesting
|
static class FilteringCacheUpdatingConsumer
|
implements BiConsumer<INetworkScoreCache, Object> {
|
private final Context mContext;
|
private final List<ScoredNetwork> mScoredNetworkList;
|
private final int mNetworkType;
|
// TODO: 1/23/17 - Consider a Map if we implement more filters.
|
// These are created on-demand to defer the construction cost until
|
// an instance is actually needed.
|
private UnaryOperator<List<ScoredNetwork>> mCurrentNetworkFilter;
|
private UnaryOperator<List<ScoredNetwork>> mScanResultsFilter;
|
|
static FilteringCacheUpdatingConsumer create(Context context,
|
List<ScoredNetwork> scoredNetworkList, int networkType) {
|
return new FilteringCacheUpdatingConsumer(context, scoredNetworkList, networkType,
|
null, null);
|
}
|
|
@VisibleForTesting
|
FilteringCacheUpdatingConsumer(Context context,
|
List<ScoredNetwork> scoredNetworkList, int networkType,
|
UnaryOperator<List<ScoredNetwork>> currentNetworkFilter,
|
UnaryOperator<List<ScoredNetwork>> scanResultsFilter) {
|
mContext = context;
|
mScoredNetworkList = scoredNetworkList;
|
mNetworkType = networkType;
|
mCurrentNetworkFilter = currentNetworkFilter;
|
mScanResultsFilter = scanResultsFilter;
|
}
|
|
@Override
|
public void accept(INetworkScoreCache networkScoreCache, Object cookie) {
|
int filterType = NetworkScoreManager.CACHE_FILTER_NONE;
|
if (cookie instanceof Integer) {
|
filterType = (Integer) cookie;
|
}
|
|
try {
|
final List<ScoredNetwork> filteredNetworkList =
|
filterScores(mScoredNetworkList, filterType);
|
if (!filteredNetworkList.isEmpty()) {
|
networkScoreCache.updateScores(filteredNetworkList);
|
}
|
} catch (RemoteException e) {
|
if (VERBOSE) {
|
Log.v(TAG, "Unable to update scores of type " + mNetworkType, e);
|
}
|
}
|
}
|
|
/**
|
* Applies the appropriate filter and returns the filtered results.
|
*/
|
private List<ScoredNetwork> filterScores(List<ScoredNetwork> scoredNetworkList,
|
int filterType) {
|
switch (filterType) {
|
case NetworkScoreManager.CACHE_FILTER_NONE:
|
return scoredNetworkList;
|
|
case NetworkScoreManager.CACHE_FILTER_CURRENT_NETWORK:
|
if (mCurrentNetworkFilter == null) {
|
mCurrentNetworkFilter =
|
new CurrentNetworkScoreCacheFilter(new WifiInfoSupplier(mContext));
|
}
|
return mCurrentNetworkFilter.apply(scoredNetworkList);
|
|
case NetworkScoreManager.CACHE_FILTER_SCAN_RESULTS:
|
if (mScanResultsFilter == null) {
|
mScanResultsFilter = new ScanResultsScoreCacheFilter(
|
new ScanResultsSupplier(mContext));
|
}
|
return mScanResultsFilter.apply(scoredNetworkList);
|
|
default:
|
Log.w(TAG, "Unknown filter type: " + filterType);
|
return scoredNetworkList;
|
}
|
}
|
}
|
|
/**
|
* Helper class that improves the testability of the cache filter Functions.
|
*/
|
private static class WifiInfoSupplier implements Supplier<WifiInfo> {
|
private final Context mContext;
|
|
WifiInfoSupplier(Context context) {
|
mContext = context;
|
}
|
|
@Override
|
public WifiInfo get() {
|
WifiManager wifiManager = mContext.getSystemService(WifiManager.class);
|
if (wifiManager != null) {
|
return wifiManager.getConnectionInfo();
|
}
|
Log.w(TAG, "WifiManager is null, failed to return the WifiInfo.");
|
return null;
|
}
|
}
|
|
/**
|
* Helper class that improves the testability of the cache filter Functions.
|
*/
|
private static class ScanResultsSupplier implements Supplier<List<ScanResult>> {
|
private final Context mContext;
|
|
ScanResultsSupplier(Context context) {
|
mContext = context;
|
}
|
|
@Override
|
public List<ScanResult> get() {
|
WifiScanner wifiScanner = mContext.getSystemService(WifiScanner.class);
|
if (wifiScanner != null) {
|
return wifiScanner.getSingleScanResults();
|
}
|
Log.w(TAG, "WifiScanner is null, failed to return scan results.");
|
return Collections.emptyList();
|
}
|
}
|
|
/**
|
* Filters the given set of {@link ScoredNetwork}s and returns a new List containing only the
|
* {@link ScoredNetwork} associated with the current network. If no network is connected the
|
* returned list will be empty.
|
* <p>
|
* Note: this filter performs some internal caching for consistency and performance. The
|
* current network is determined at construction time and never changed. Also, the
|
* last filtered list is saved so if the same input is provided multiple times in a row
|
* the computation is only done once.
|
*/
|
@VisibleForTesting
|
static class CurrentNetworkScoreCacheFilter implements UnaryOperator<List<ScoredNetwork>> {
|
private final NetworkKey mCurrentNetwork;
|
|
CurrentNetworkScoreCacheFilter(Supplier<WifiInfo> wifiInfoSupplier) {
|
mCurrentNetwork = NetworkKey.createFromWifiInfo(wifiInfoSupplier.get());
|
}
|
|
@Override
|
public List<ScoredNetwork> apply(List<ScoredNetwork> scoredNetworks) {
|
if (mCurrentNetwork == null || scoredNetworks.isEmpty()) {
|
return Collections.emptyList();
|
}
|
|
for (int i = 0; i < scoredNetworks.size(); i++) {
|
final ScoredNetwork scoredNetwork = scoredNetworks.get(i);
|
if (scoredNetwork.networkKey.equals(mCurrentNetwork)) {
|
return Collections.singletonList(scoredNetwork);
|
}
|
}
|
|
return Collections.emptyList();
|
}
|
}
|
|
/**
|
* Filters the given set of {@link ScoredNetwork}s and returns a new List containing only the
|
* {@link ScoredNetwork} associated with the current set of {@link ScanResult}s.
|
* If there are no {@link ScanResult}s the returned list will be empty.
|
* <p>
|
* Note: this filter performs some internal caching for consistency and performance. The
|
* current set of ScanResults is determined at construction time and never changed.
|
* Also, the last filtered list is saved so if the same input is provided multiple
|
* times in a row the computation is only done once.
|
*/
|
@VisibleForTesting
|
static class ScanResultsScoreCacheFilter implements UnaryOperator<List<ScoredNetwork>> {
|
private final Set<NetworkKey> mScanResultKeys;
|
|
ScanResultsScoreCacheFilter(Supplier<List<ScanResult>> resultsSupplier) {
|
List<ScanResult> scanResults = resultsSupplier.get();
|
final int size = scanResults.size();
|
mScanResultKeys = new ArraySet<>(size);
|
for (int i = 0; i < size; i++) {
|
ScanResult scanResult = scanResults.get(i);
|
NetworkKey key = NetworkKey.createFromScanResult(scanResult);
|
if (key != null) {
|
mScanResultKeys.add(key);
|
}
|
}
|
}
|
|
@Override
|
public List<ScoredNetwork> apply(List<ScoredNetwork> scoredNetworks) {
|
if (mScanResultKeys.isEmpty() || scoredNetworks.isEmpty()) {
|
return Collections.emptyList();
|
}
|
|
List<ScoredNetwork> filteredScores = new ArrayList<>();
|
for (int i = 0; i < scoredNetworks.size(); i++) {
|
final ScoredNetwork scoredNetwork = scoredNetworks.get(i);
|
if (mScanResultKeys.contains(scoredNetwork.networkKey)) {
|
filteredScores.add(scoredNetwork);
|
}
|
}
|
|
return filteredScores;
|
}
|
}
|
|
@Override
|
public boolean clearScores() {
|
// Only the active scorer or the system should be allowed to flush all scores.
|
enforceSystemOrIsActiveScorer(getCallingUid());
|
final long token = Binder.clearCallingIdentity();
|
try {
|
clearInternal();
|
return true;
|
} finally {
|
Binder.restoreCallingIdentity(token);
|
}
|
}
|
|
@Override
|
public boolean setActiveScorer(String packageName) {
|
enforceSystemOrHasScoreNetworks();
|
return mNetworkScorerAppManager.setActiveScorer(packageName);
|
}
|
|
/**
|
* Determine whether the application with the given UID is the enabled scorer.
|
*
|
* @param callingUid the UID to check
|
* @return true if the provided UID is the active scorer, false otherwise.
|
*/
|
@Override
|
public boolean isCallerActiveScorer(int callingUid) {
|
synchronized (mServiceConnectionLock) {
|
return mServiceConnection != null
|
&& mServiceConnection.getAppData().packageUid == callingUid;
|
}
|
}
|
|
private void enforceSystemOnly() throws SecurityException {
|
// REQUEST_NETWORK_SCORES is a signature only permission.
|
mContext.enforceCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES,
|
"Caller must be granted REQUEST_NETWORK_SCORES.");
|
}
|
|
private void enforceSystemOrHasScoreNetworks() throws SecurityException {
|
if (mContext.checkCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES)
|
!= PackageManager.PERMISSION_GRANTED
|
&& mContext.checkCallingOrSelfPermission(permission.SCORE_NETWORKS)
|
!= PackageManager.PERMISSION_GRANTED) {
|
throw new SecurityException(
|
"Caller is neither the system process or a network scorer.");
|
}
|
}
|
|
private void enforceSystemOrIsActiveScorer(int callingUid) throws SecurityException {
|
if (mContext.checkCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES)
|
!= PackageManager.PERMISSION_GRANTED
|
&& !isCallerActiveScorer(callingUid)) {
|
throw new SecurityException(
|
"Caller is neither the system process or the active network scorer.");
|
}
|
}
|
|
/**
|
* Obtain the package name of the current active network scorer.
|
*
|
* @return the full package name of the current active scorer, or null if there is no active
|
* scorer.
|
*/
|
@Override
|
public String getActiveScorerPackage() {
|
enforceSystemOrHasScoreNetworks();
|
synchronized (mServiceConnectionLock) {
|
if (mServiceConnection != null) {
|
return mServiceConnection.getPackageName();
|
}
|
}
|
return null;
|
}
|
|
/**
|
* Returns metadata about the active scorer or <code>null</code> if there is no active scorer.
|
*/
|
@Override
|
public NetworkScorerAppData getActiveScorer() {
|
// Only the system can access this data.
|
enforceSystemOnly();
|
synchronized (mServiceConnectionLock) {
|
if (mServiceConnection != null) {
|
return mServiceConnection.getAppData();
|
}
|
}
|
|
return null;
|
}
|
|
/**
|
* Returns the list of available scorer apps. The list will be empty if there are
|
* no valid scorers.
|
*/
|
@Override
|
public List<NetworkScorerAppData> getAllValidScorers() {
|
// Only the system can access this data.
|
enforceSystemOnly();
|
return mNetworkScorerAppManager.getAllValidScorers();
|
}
|
|
@Override
|
public void disableScoring() {
|
// Only the active scorer or the system should be allowed to disable scoring.
|
enforceSystemOrIsActiveScorer(getCallingUid());
|
// no-op for now but we could write to the setting if needed.
|
}
|
|
/** Clear scores. Callers are responsible for checking permissions as appropriate. */
|
private void clearInternal() {
|
sendCacheUpdateCallback(new BiConsumer<INetworkScoreCache, Object>() {
|
@Override
|
public void accept(INetworkScoreCache networkScoreCache, Object cookie) {
|
try {
|
networkScoreCache.clearScores();
|
} catch (RemoteException e) {
|
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
Log.v(TAG, "Unable to clear scores", e);
|
}
|
}
|
}
|
}, getScoreCacheLists());
|
}
|
|
@Override
|
public void registerNetworkScoreCache(int networkType,
|
INetworkScoreCache scoreCache,
|
int filterType) {
|
enforceSystemOnly();
|
final long token = Binder.clearCallingIdentity();
|
try {
|
synchronized (mScoreCaches) {
|
RemoteCallbackList<INetworkScoreCache> callbackList = mScoreCaches.get(networkType);
|
if (callbackList == null) {
|
callbackList = new RemoteCallbackList<>();
|
mScoreCaches.put(networkType, callbackList);
|
}
|
if (!callbackList.register(scoreCache, filterType)) {
|
if (callbackList.getRegisteredCallbackCount() == 0) {
|
mScoreCaches.remove(networkType);
|
}
|
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
Log.v(TAG, "Unable to register NetworkScoreCache for type " + networkType);
|
}
|
}
|
}
|
} finally {
|
Binder.restoreCallingIdentity(token);
|
}
|
}
|
|
@Override
|
public void unregisterNetworkScoreCache(int networkType, INetworkScoreCache scoreCache) {
|
enforceSystemOnly();
|
final long token = Binder.clearCallingIdentity();
|
try {
|
synchronized (mScoreCaches) {
|
RemoteCallbackList<INetworkScoreCache> callbackList = mScoreCaches.get(networkType);
|
if (callbackList == null || !callbackList.unregister(scoreCache)) {
|
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
Log.v(TAG, "Unable to unregister NetworkScoreCache for type "
|
+ networkType);
|
}
|
} else if (callbackList.getRegisteredCallbackCount() == 0) {
|
mScoreCaches.remove(networkType);
|
}
|
}
|
} finally {
|
Binder.restoreCallingIdentity(token);
|
}
|
}
|
|
@Override
|
public boolean requestScores(NetworkKey[] networks) {
|
enforceSystemOnly();
|
final long token = Binder.clearCallingIdentity();
|
try {
|
final INetworkRecommendationProvider provider = getRecommendationProvider();
|
if (provider != null) {
|
try {
|
provider.requestScores(networks);
|
// TODO: 12/15/16 - Consider pushing null scores into the cache to
|
// prevent repeated requests for the same scores.
|
return true;
|
} catch (RemoteException e) {
|
Log.w(TAG, "Failed to request scores.", e);
|
// TODO: 12/15/16 - Keep track of failures.
|
}
|
}
|
return false;
|
} finally {
|
Binder.restoreCallingIdentity(token);
|
}
|
}
|
|
@Override
|
protected void dump(final FileDescriptor fd, final PrintWriter writer, final String[] args) {
|
if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return;
|
final long token = Binder.clearCallingIdentity();
|
try {
|
NetworkScorerAppData currentScorer = mNetworkScorerAppManager.getActiveScorer();
|
if (currentScorer == null) {
|
writer.println("Scoring is disabled.");
|
return;
|
}
|
writer.println("Current scorer: " + currentScorer);
|
|
sendCacheUpdateCallback(new BiConsumer<INetworkScoreCache, Object>() {
|
@Override
|
public void accept(INetworkScoreCache networkScoreCache, Object cookie) {
|
try {
|
TransferPipe.dumpAsync(networkScoreCache.asBinder(), fd, args);
|
} catch (IOException | RemoteException e) {
|
writer.println("Failed to dump score cache: " + e);
|
}
|
}
|
}, getScoreCacheLists());
|
|
synchronized (mServiceConnectionLock) {
|
if (mServiceConnection != null) {
|
mServiceConnection.dump(fd, writer, args);
|
} else {
|
writer.println("ScoringServiceConnection: null");
|
}
|
}
|
writer.flush();
|
} finally {
|
Binder.restoreCallingIdentity(token);
|
}
|
}
|
|
/**
|
* Returns a {@link Collection} of all {@link RemoteCallbackList}s that are currently active.
|
*
|
* <p>May be used to perform an action on all score caches without potentially strange behavior
|
* if a new scorer is registered during that action's execution.
|
*/
|
private Collection<RemoteCallbackList<INetworkScoreCache>> getScoreCacheLists() {
|
synchronized (mScoreCaches) {
|
return new ArrayList<>(mScoreCaches.values());
|
}
|
}
|
|
private void sendCacheUpdateCallback(BiConsumer<INetworkScoreCache, Object> consumer,
|
Collection<RemoteCallbackList<INetworkScoreCache>> remoteCallbackLists) {
|
for (RemoteCallbackList<INetworkScoreCache> callbackList : remoteCallbackLists) {
|
synchronized (callbackList) { // Ensure only one active broadcast per RemoteCallbackList
|
final int count = callbackList.beginBroadcast();
|
try {
|
for (int i = 0; i < count; i++) {
|
consumer.accept(callbackList.getBroadcastItem(i),
|
callbackList.getBroadcastCookie(i));
|
}
|
} finally {
|
callbackList.finishBroadcast();
|
}
|
}
|
}
|
}
|
|
@Nullable
|
private INetworkRecommendationProvider getRecommendationProvider() {
|
synchronized (mServiceConnectionLock) {
|
if (mServiceConnection != null) {
|
return mServiceConnection.getRecommendationProvider();
|
}
|
}
|
return null;
|
}
|
|
// The class and methods need to be public for Mockito to work.
|
@VisibleForTesting
|
public static class ScoringServiceConnection implements ServiceConnection {
|
private final NetworkScorerAppData mAppData;
|
private volatile boolean mBound = false;
|
private volatile boolean mConnected = false;
|
private volatile INetworkRecommendationProvider mRecommendationProvider;
|
|
ScoringServiceConnection(NetworkScorerAppData appData) {
|
mAppData = appData;
|
}
|
|
@VisibleForTesting
|
public void bind(Context context) {
|
if (!mBound) {
|
Intent service = new Intent(NetworkScoreManager.ACTION_RECOMMEND_NETWORKS);
|
service.setComponent(mAppData.getRecommendationServiceComponent());
|
mBound = context.bindServiceAsUser(service, this,
|
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
|
UserHandle.SYSTEM);
|
if (!mBound) {
|
Log.w(TAG, "Bind call failed for " + service);
|
context.unbindService(this);
|
} else {
|
if (DBG) Log.d(TAG, "ScoringServiceConnection bound.");
|
}
|
}
|
}
|
|
@VisibleForTesting
|
public void unbind(Context context) {
|
try {
|
if (mBound) {
|
mBound = false;
|
context.unbindService(this);
|
if (DBG) Log.d(TAG, "ScoringServiceConnection unbound.");
|
}
|
} catch (RuntimeException e) {
|
Log.e(TAG, "Unbind failed.", e);
|
}
|
|
mConnected = false;
|
mRecommendationProvider = null;
|
}
|
|
@VisibleForTesting
|
public NetworkScorerAppData getAppData() {
|
return mAppData;
|
}
|
|
@VisibleForTesting
|
public INetworkRecommendationProvider getRecommendationProvider() {
|
return mRecommendationProvider;
|
}
|
|
@VisibleForTesting
|
public String getPackageName() {
|
return mAppData.getRecommendationServiceComponent().getPackageName();
|
}
|
|
@VisibleForTesting
|
public boolean isAlive() {
|
return mBound && mConnected;
|
}
|
|
@Override
|
public void onServiceConnected(ComponentName name, IBinder service) {
|
if (DBG) Log.d(TAG, "ScoringServiceConnection: " + name.flattenToString());
|
mConnected = true;
|
mRecommendationProvider = INetworkRecommendationProvider.Stub.asInterface(service);
|
}
|
|
@Override
|
public void onServiceDisconnected(ComponentName name) {
|
if (DBG) {
|
Log.d(TAG, "ScoringServiceConnection, disconnected: " + name.flattenToString());
|
}
|
mConnected = false;
|
mRecommendationProvider = null;
|
}
|
|
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
|
writer.println("ScoringServiceConnection: "
|
+ mAppData.getRecommendationServiceComponent()
|
+ ", bound: " + mBound
|
+ ", connected: " + mConnected);
|
}
|
}
|
|
@VisibleForTesting
|
public final class ServiceHandler extends Handler {
|
public static final int MSG_RECOMMENDATIONS_PACKAGE_CHANGED = 1;
|
public static final int MSG_RECOMMENDATION_ENABLED_SETTING_CHANGED = 2;
|
|
public ServiceHandler(Looper looper) {
|
super(looper);
|
}
|
|
@Override
|
public void handleMessage(Message msg) {
|
final int what = msg.what;
|
switch (what) {
|
case MSG_RECOMMENDATIONS_PACKAGE_CHANGED:
|
case MSG_RECOMMENDATION_ENABLED_SETTING_CHANGED:
|
refreshBinding();
|
break;
|
|
default:
|
Log.w(TAG,"Unknown message: " + what);
|
}
|
}
|
}
|
}
|