diff --git a/core/java/android/net/INetworkScoreService.aidl b/core/java/android/net/INetworkScoreService.aidl
index 9573953e6e135..82432c758fa62 100644
--- a/core/java/android/net/INetworkScoreService.aidl
+++ b/core/java/android/net/INetworkScoreService.aidl
@@ -21,6 +21,7 @@ import android.net.NetworkKey;
import android.net.RecommendationRequest;
import android.net.RecommendationResult;
import android.net.ScoredNetwork;
+import android.os.RemoteCallback;
/**
* A service for updating network scores from a network scorer application.
@@ -117,4 +118,16 @@ interface INetworkScoreService
* scorer.
*/
String getActiveScorerPackage();
+
+ /**
+ * Request a recommendation for the best network to connect to
+ * taking into account the inputs from the {@link RecommendationRequest}.
+ *
+ * @param request a {@link RecommendationRequest} instance containing the details of the request
+ * @param remoteCallback a {@link RemoteCallback} instance to invoke when the recommendation
+ * is available.
+ * @throws SecurityException if the caller is not the system
+ */
+ oneway void requestRecommendationAsync(in RecommendationRequest request,
+ in RemoteCallback remoteCallback);
}
diff --git a/core/java/android/net/NetworkScoreManager.java b/core/java/android/net/NetworkScoreManager.java
index a6854dcb8eb3e..c57145f69d2f0 100644
--- a/core/java/android/net/NetworkScoreManager.java
+++ b/core/java/android/net/NetworkScoreManager.java
@@ -16,17 +16,24 @@
package android.net;
+import static android.net.NetworkRecommendationProvider.EXTRA_RECOMMENDATION_RESULT;
+
import android.Manifest;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SystemApi;
import android.content.Context;
import android.content.Intent;
import android.net.NetworkScorerAppManager.NetworkScorerAppData;
+import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.ServiceManager;
+import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -356,4 +363,53 @@ public class NetworkScoreManager {
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Request a recommendation for which network to connect to.
+ *
+ *
The callback will be run on the thread associated with provided {@link Handler}.
+ *
+ * @param request a {@link RecommendationRequest} instance containing additional
+ * request details
+ * @param callback a {@link RecommendationCallback} instance that will be invoked when
+ * the {@link RecommendationResult} is available
+ * @param handler a {@link Handler} instance representing the thread to run the callback on.
+ * @throws SecurityException
+ * @hide
+ */
+ public void requestRecommendation(
+ final @NonNull RecommendationRequest request,
+ final @NonNull RecommendationCallback callback,
+ final @NonNull Handler handler) {
+ Preconditions.checkNotNull(request, "RecommendationRequest cannot be null.");
+ Preconditions.checkNotNull(callback, "RecommendationCallback cannot be null.");
+ Preconditions.checkNotNull(handler, "Handler cannot be null.");
+
+ RemoteCallback remoteCallback = new RemoteCallback(new RemoteCallback.OnResultListener() {
+ @Override
+ public void onResult(Bundle data) {
+ RecommendationResult result = data.getParcelable(EXTRA_RECOMMENDATION_RESULT);
+ callback.onRecommendationAvailable(result);
+ }
+ }, handler);
+
+ try {
+ mService.requestRecommendationAsync(request, remoteCallback);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Callers of {@link #requestRecommendation(RecommendationRequest, RecommendationCallback, Handler)}
+ * must pass in an implementation of this class.
+ * @hide
+ */
+ public abstract static class RecommendationCallback {
+ /**
+ * Invoked when a {@link RecommendationResult} is available.
+ * @param result a {@link RecommendationResult} instance.
+ */
+ public abstract void onRecommendationAvailable(RecommendationResult result);
+ }
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 3ea5dcba815d2..35fa2108d8aae 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7672,6 +7672,16 @@ public final class Settings {
public static final String NETWORK_RECOMMENDATIONS_ENABLED =
"network_recommendations_enabled";
+ /**
+ * The number of milliseconds the {@link com.android.server.NetworkScoreService}
+ * will give a recommendation request to complete before returning a default response.
+ *
+ * Type: long
+ * @hide
+ */
+ public static final String NETWORK_RECOMMENDATION_REQUEST_TIMEOUT_MS =
+ "network_recommendation_request_timeout_ms";
+
/**
* Settings to allow BLE scans to be enabled even when Bluetooth is turned off for
* connectivity.
diff --git a/services/core/java/com/android/server/NetworkScoreService.java b/services/core/java/com/android/server/NetworkScoreService.java
index e23844c3e633b..bd9f6840abbd2 100644
--- a/services/core/java/com/android/server/NetworkScoreService.java
+++ b/services/core/java/com/android/server/NetworkScoreService.java
@@ -42,15 +42,22 @@ import android.net.RecommendationResult;
import android.net.ScoredNetwork;
import android.net.Uri;
import android.os.Binder;
+import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
import android.os.IRemoteCallback;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteCallback;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.provider.Settings;
import android.provider.Settings.Global;
import android.util.ArrayMap;
import android.util.Log;
+import android.util.Pair;
import android.util.TimedRemoteCaller;
import com.android.internal.annotations.GuardedBy;
@@ -67,6 +74,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
/**
@@ -75,7 +83,7 @@ import java.util.function.Consumer;
*/
public class NetworkScoreService extends INetworkScoreService.Stub {
private static final String TAG = "NetworkScoreService";
- private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final boolean DBG = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.DEBUG);
private final Context mContext;
private final NetworkScorerAppManager mNetworkScorerAppManager;
@@ -83,13 +91,16 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
@GuardedBy("mScoreCaches")
private final Map> mScoreCaches;
/** Lock used to update mPackageMonitor when scorer package changes occur. */
- private final Object mPackageMonitorLock = new Object[0];
- private final Object mServiceConnectionLock = new Object[0];
+ private final Object mPackageMonitorLock = new Object();
+ private final Object mServiceConnectionLock = new Object();
+ private final Handler mHandler;
+ private final DispatchingContentObserver mContentObserver;
@GuardedBy("mPackageMonitorLock")
private NetworkScorerPackageMonitor mPackageMonitor;
@GuardedBy("mServiceConnectionLock")
private ScoringServiceConnection mServiceConnection;
+ private volatile long mRecommendationRequestTimeoutMs;
private BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
@Override
@@ -185,12 +196,25 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
}
/**
- * Reevaluates the service binding when the Settings toggle is changed.
+ * Dispatches observed content changes to a handler for further processing.
*/
- private class SettingsObserver extends ContentObserver {
+ @VisibleForTesting
+ public static class DispatchingContentObserver extends ContentObserver {
+ final private Map mUriEventMap;
+ final private Context mContext;
+ final private Handler mHandler;
- public SettingsObserver() {
- super(null /*handler*/);
+ 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
@@ -201,16 +225,22 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
@Override
public void onChange(boolean selfChange, Uri uri) {
if (DBG) Log.d(TAG, String.format("onChange(%s, %s)", selfChange, uri));
- bindToScoringServiceIfNeeded();
+ 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));
+ this(context, new NetworkScorerAppManager(context), Looper.myLooper());
}
@VisibleForTesting
- NetworkScoreService(Context context, NetworkScorerAppManager networkScoreAppManager) {
+ NetworkScoreService(Context context, NetworkScorerAppManager networkScoreAppManager,
+ Looper looper) {
mContext = context;
mNetworkScorerAppManager = networkScoreAppManager;
mScoreCaches = new ArrayMap<>();
@@ -219,16 +249,19 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
mContext.registerReceiverAsUser(
mUserIntentReceiver, UserHandle.SYSTEM, filter, null /* broadcastPermission*/,
null /* scheduler */);
- // TODO(jjoslin): 12/15/16 - Make timeout configurable.
mRequestRecommendationCaller =
new RequestRecommendationCaller(TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS);
+ mRecommendationRequestTimeoutMs = TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS;
+ mHandler = new ServiceHandler(looper);
+ mContentObserver = new DispatchingContentObserver(context, mHandler);
}
/** 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");
registerPackageMonitorIfNeeded();
- registerRecommendationSettingObserverIfNeeded();
+ registerRecommendationSettingsObserver();
+ refreshRecommendationRequestTimeoutMs();
}
/** Called when the system is ready for us to start third-party code. */
@@ -242,14 +275,18 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
bindToScoringServiceIfNeeded();
}
- private void registerRecommendationSettingObserverIfNeeded() {
+ private void registerRecommendationSettingsObserver() {
final List providerPackages =
mNetworkScorerAppManager.getPotentialRecommendationProviderPackages();
if (!providerPackages.isEmpty()) {
- final ContentResolver resolver = mContext.getContentResolver();
- final Uri uri = Global.getUriFor(Global.NETWORK_RECOMMENDATIONS_ENABLED);
- resolver.registerContentObserver(uri, false, new SettingsObserver());
+ final Uri enabledUri = Global.getUriFor(Global.NETWORK_RECOMMENDATIONS_ENABLED);
+ mContentObserver.observe(enabledUri,
+ ServiceHandler.MSG_RECOMMENDATIONS_ENABLED_CHANGED);
}
+
+ final Uri timeoutUri = Global.getUriFor(Global.NETWORK_RECOMMENDATION_REQUEST_TIMEOUT_MS);
+ mContentObserver.observe(timeoutUri,
+ ServiceHandler.MSG_RECOMMENDATION_REQUEST_TIMEOUT_CHANGED);
}
private void registerPackageMonitorIfNeeded() {
@@ -553,6 +590,56 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
}
}
+ /**
+ * Request a recommendation for the best network to connect to
+ * taking into account the inputs from the {@link RecommendationRequest}.
+ *
+ * @param request a {@link RecommendationRequest} instance containing the details of the request
+ * @param remoteCallback a {@link IRemoteCallback} instance to invoke when the recommendation
+ * is available.
+ * @throws SecurityException if the caller is not the system
+ */
+ @Override
+ public void requestRecommendationAsync(RecommendationRequest request,
+ RemoteCallback remoteCallback) {
+ mContext.enforceCallingOrSelfPermission(permission.BROADCAST_NETWORK_PRIVILEGED, TAG);
+
+ final OneTimeCallback oneTimeCallback = new OneTimeCallback(remoteCallback);
+ final Pair pair =
+ Pair.create(request, oneTimeCallback);
+ final Message timeoutMsg = mHandler.obtainMessage(
+ ServiceHandler.MSG_RECOMMENDATION_REQUEST_TIMEOUT, pair);
+ final INetworkRecommendationProvider provider = getRecommendationProvider();
+ final long token = Binder.clearCallingIdentity();
+ try {
+ if (provider != null) {
+ try {
+ mHandler.sendMessageDelayed(timeoutMsg, mRecommendationRequestTimeoutMs);
+ provider.requestRecommendation(request, new IRemoteCallback.Stub() {
+ @Override
+ public void sendResult(Bundle data) throws RemoteException {
+ // Remove the timeout message
+ mHandler.removeMessages(timeoutMsg.what, pair);
+ oneTimeCallback.sendResult(data);
+ }
+ }, 0 /*sequence*/);
+ return;
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to request a recommendation.", e);
+ // TODO(jjoslin): 12/15/16 - Keep track of failures.
+ // Remove the timeout message
+ mHandler.removeMessages(timeoutMsg.what, pair);
+ // Will fall through and send back the default recommendation.
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+
+ // Else send back the default recommendation.
+ sendDefaultRecommendationResponse(request, oneTimeCallback);
+ }
+
@Override
public boolean requestScores(NetworkKey[] networks) {
mContext.enforceCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES, TAG);
@@ -651,6 +738,18 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
return null;
}
+ @VisibleForTesting
+ public void refreshRecommendationRequestTimeoutMs() {
+ final ContentResolver cr = mContext.getContentResolver();
+ long timeoutMs = Settings.Global.getLong(cr,
+ Global.NETWORK_RECOMMENDATION_REQUEST_TIMEOUT_MS, -1L /*default*/);
+ if (timeoutMs < 0) {
+ timeoutMs = TimedRemoteCaller.DEFAULT_CALL_TIMEOUT_MILLIS;
+ }
+ if (DBG) Log.d(TAG, "Updating the recommendation request timeout to " + timeoutMs + " ms");
+ mRecommendationRequestTimeoutMs = timeoutMs;
+ }
+
private static class ScoringServiceConnection implements ServiceConnection {
private final ComponentName mComponentName;
private final int mScoringAppUid;
@@ -756,4 +855,83 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
return getResultTimed(sequence);
}
}
+
+ /**
+ * A wrapper around {@link RemoteCallback} that guarantees
+ * {@link RemoteCallback#sendResult(Bundle)} will be invoked at most once.
+ */
+ @VisibleForTesting
+ public static final class OneTimeCallback {
+ private final RemoteCallback mRemoteCallback;
+ private final AtomicBoolean mCallbackRun;
+
+ public OneTimeCallback(RemoteCallback remoteCallback) {
+ mRemoteCallback = remoteCallback;
+ mCallbackRun = new AtomicBoolean(false);
+ }
+
+ public void sendResult(Bundle data) {
+ if (mCallbackRun.compareAndSet(false, true)) {
+ mRemoteCallback.sendResult(data);
+ }
+ }
+ }
+
+ private static void sendDefaultRecommendationResponse(RecommendationRequest request,
+ OneTimeCallback remoteCallback) {
+ if (DBG) {
+ Log.d(TAG, "Returning the default network recommendation.");
+ }
+
+ final RecommendationResult result;
+ if (request != null && request.getCurrentSelectedConfig() != null) {
+ result = RecommendationResult.createConnectRecommendation(
+ request.getCurrentSelectedConfig());
+ } else {
+ result = RecommendationResult.createDoNotConnectRecommendation();
+ }
+
+ final Bundle data = new Bundle();
+ data.putParcelable(EXTRA_RECOMMENDATION_RESULT, result);
+ remoteCallback.sendResult(data);
+ }
+
+ @VisibleForTesting
+ public final class ServiceHandler extends Handler {
+ public static final int MSG_RECOMMENDATION_REQUEST_TIMEOUT = 1;
+ public static final int MSG_RECOMMENDATIONS_ENABLED_CHANGED = 2;
+ public static final int MSG_RECOMMENDATION_REQUEST_TIMEOUT_CHANGED = 3;
+
+ public ServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final int what = msg.what;
+ switch (what) {
+ case MSG_RECOMMENDATION_REQUEST_TIMEOUT:
+ if (DBG) {
+ Log.d(TAG, "Network recommendation request timed out.");
+ }
+ final Pair pair =
+ (Pair) msg.obj;
+ final RecommendationRequest request = pair.first;
+ final OneTimeCallback remoteCallback = pair.second;
+ sendDefaultRecommendationResponse(request, remoteCallback);
+ break;
+
+ case MSG_RECOMMENDATIONS_ENABLED_CHANGED:
+ bindToScoringServiceIfNeeded();
+ break;
+
+ case MSG_RECOMMENDATION_REQUEST_TIMEOUT_CHANGED:
+ refreshRecommendationRequestTimeoutMs();
+ break;
+
+ default:
+ Log.w(TAG,"Unknown message: " + what);
+ }
+ }
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java b/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java
index 1189dae7b21ce..75d9c3911fca6 100644
--- a/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/NetworkScoreServiceTest.java
@@ -24,6 +24,7 @@ import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertSame;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
@@ -60,21 +61,28 @@ import android.net.NetworkScorerAppManager.NetworkScorerAppData;
import android.net.RecommendationRequest;
import android.net.RecommendationResult;
import android.net.ScoredNetwork;
+import android.net.Uri;
import android.net.WifiKey;
import android.net.wifi.WifiConfiguration;
import android.os.Binder;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.provider.Settings;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
import com.android.server.devicepolicy.MockUtils;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -89,6 +97,8 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
/**
* Tests for {@link NetworkScoreService}.
@@ -114,6 +124,9 @@ public class NetworkScoreServiceTest {
private ContentResolver mContentResolver;
private NetworkScoreService mNetworkScoreService;
private RecommendationRequest mRecommendationRequest;
+ private RemoteCallback mRemoteCallback;
+ private OnResultListener mOnResultListener;
+ private HandlerThread mHandlerThread;
@Before
public void setUp() throws Exception {
@@ -123,12 +136,25 @@ public class NetworkScoreServiceTest {
mContentResolver = InstrumentationRegistry.getContext().getContentResolver();
when(mContext.getContentResolver()).thenReturn(mContentResolver);
when(mContext.getResources()).thenReturn(mResources);
- mNetworkScoreService = new NetworkScoreService(mContext, mNetworkScorerAppManager);
+ mHandlerThread = new HandlerThread("NetworkScoreServiceTest");
+ mHandlerThread.start();
+ mNetworkScoreService = new NetworkScoreService(mContext, mNetworkScorerAppManager,
+ mHandlerThread.getLooper());
WifiConfiguration configuration = new WifiConfiguration();
configuration.SSID = "NetworkScoreServiceTest_SSID";
configuration.BSSID = "NetworkScoreServiceTest_BSSID";
mRecommendationRequest = new RecommendationRequest.Builder()
.setCurrentRecommendedWifiConfig(configuration).build();
+ mOnResultListener = new OnResultListener();
+ mRemoteCallback = new RemoteCallback(mOnResultListener);
+ Settings.Global.putLong(mContentResolver,
+ Settings.Global.NETWORK_RECOMMENDATION_REQUEST_TIMEOUT_MS, -1L);
+ mNetworkScoreService.refreshRecommendationRequestTimeoutMs();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mHandlerThread.quitSafely();
}
@Test
@@ -261,6 +287,116 @@ public class NetworkScoreServiceTest {
result.getWifiConfiguration().BSSID);
}
+ @Test
+ public void testRequestRecommendationAsync_noPermission() throws Exception {
+ doThrow(new SecurityException()).when(mContext)
+ .enforceCallingOrSelfPermission(eq(permission.BROADCAST_NETWORK_PRIVILEGED),
+ anyString());
+ try {
+ mNetworkScoreService.requestRecommendationAsync(mRecommendationRequest,
+ mRemoteCallback);
+ fail("BROADCAST_NETWORK_PRIVILEGED not enforced.");
+ } catch (SecurityException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testRequestRecommendationAsync_providerNotConnected() throws Exception {
+ mNetworkScoreService.requestRecommendationAsync(mRecommendationRequest,
+ mRemoteCallback);
+ boolean callbackRan = mOnResultListener.countDownLatch.await(3, TimeUnit.SECONDS);
+ assertTrue(callbackRan);
+ verifyZeroInteractions(mRecommendationProvider);
+ }
+
+ @Test
+ public void testRequestRecommendationAsync_requestTimesOut() throws Exception {
+ injectProvider();
+ Settings.Global.putLong(mContentResolver,
+ Settings.Global.NETWORK_RECOMMENDATION_REQUEST_TIMEOUT_MS, 1L);
+ mNetworkScoreService.refreshRecommendationRequestTimeoutMs();
+ mNetworkScoreService.requestRecommendationAsync(mRecommendationRequest,
+ mRemoteCallback);
+ boolean callbackRan = mOnResultListener.countDownLatch.await(3, TimeUnit.SECONDS);
+ assertTrue(callbackRan);
+ verify(mRecommendationProvider).requestRecommendation(eq(mRecommendationRequest),
+ isA(IRemoteCallback.Stub.class), anyInt());
+
+ assertTrue(mOnResultListener.receivedBundle.containsKey(EXTRA_RECOMMENDATION_RESULT));
+ RecommendationResult result =
+ mOnResultListener.receivedBundle.getParcelable(EXTRA_RECOMMENDATION_RESULT);
+ assertTrue(result.hasRecommendation());
+ assertEquals(mRecommendationRequest.getCurrentSelectedConfig().SSID,
+ result.getWifiConfiguration().SSID);
+ }
+
+ @Test
+ public void testRequestRecommendationAsync_requestSucceeds() throws Exception {
+ injectProvider();
+ final Bundle bundle = new Bundle();
+ doAnswer(invocation -> {
+ invocation.getArgumentAt(1, IRemoteCallback.class).sendResult(bundle);
+ return null;
+ }).when(mRecommendationProvider)
+ .requestRecommendation(eq(mRecommendationRequest), isA(IRemoteCallback.class),
+ anyInt());
+
+ mNetworkScoreService.requestRecommendationAsync(mRecommendationRequest,
+ mRemoteCallback);
+ boolean callbackRan = mOnResultListener.countDownLatch.await(3, TimeUnit.SECONDS);
+ assertTrue(callbackRan);
+ // If it's not the same instance then something else ran the callback.
+ assertSame(bundle, mOnResultListener.receivedBundle);
+ }
+
+ @Test
+ public void testRequestRecommendationAsync_requestThrowsRemoteException() throws Exception {
+ injectProvider();
+ doThrow(new RemoteException()).when(mRecommendationProvider)
+ .requestRecommendation(eq(mRecommendationRequest), isA(IRemoteCallback.class),
+ anyInt());
+
+ mNetworkScoreService.requestRecommendationAsync(mRecommendationRequest,
+ mRemoteCallback);
+ boolean callbackRan = mOnResultListener.countDownLatch.await(3, TimeUnit.SECONDS);
+ assertTrue(callbackRan);
+ }
+
+ @Test
+ public void dispatchingContentObserver_nullUri() throws Exception {
+ NetworkScoreService.DispatchingContentObserver observer =
+ new NetworkScoreService.DispatchingContentObserver(mContext, null /*handler*/);
+
+ observer.onChange(false, null);
+ // nothing to assert or verify but since we passed in a null handler we'd see a NPE
+ // if it were interacted with.
+ }
+
+ @Test
+ public void dispatchingContentObserver_dispatchUri() throws Exception {
+ final CountDownHandler handler = new CountDownHandler(mHandlerThread.getLooper());
+ NetworkScoreService.DispatchingContentObserver observer =
+ new NetworkScoreService.DispatchingContentObserver(mContext, handler);
+ Uri uri = Uri.parse("content://settings/global/network_score_service_test");
+ int expectedWhat = 24;
+ observer.observe(uri, expectedWhat);
+
+ observer.onChange(false, uri);
+ final boolean msgHandled = handler.latch.await(3, TimeUnit.SECONDS);
+ assertTrue(msgHandled);
+ assertEquals(expectedWhat, handler.receivedWhat);
+ }
+
+ @Test
+ public void oneTimeCallback_multipleCallbacks() throws Exception {
+ NetworkScoreService.OneTimeCallback callback =
+ new NetworkScoreService.OneTimeCallback(mRemoteCallback);
+ callback.sendResult(null);
+ callback.sendResult(null);
+ assertEquals(1, mOnResultListener.resultCount);
+ }
+
@Test
public void testUpdateScores_notActiveScorer() {
bindToScorer(false /*callerIsScorer*/);
@@ -515,4 +651,32 @@ public class NetworkScoreServiceTest {
isA(UserHandle.class))).thenReturn(true);
mNetworkScoreService.systemRunning();
}
+
+ private static class OnResultListener implements RemoteCallback.OnResultListener {
+ private final CountDownLatch countDownLatch = new CountDownLatch(1);
+ private int resultCount;
+ private Bundle receivedBundle;
+
+ @Override
+ public void onResult(Bundle result) {
+ countDownLatch.countDown();
+ resultCount++;
+ receivedBundle = result;
+ }
+ }
+
+ private static class CountDownHandler extends Handler {
+ CountDownLatch latch = new CountDownLatch(1);
+ int receivedWhat;
+
+ CountDownHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ latch.countDown();
+ receivedWhat = msg.what;
+ }
+ }
}