Make the onRequestRecommendation() method async.
Converted the NetworkRecommendationProvider.onRequestRecommendation()
method into an async call to give implementors more flexibility.
Added unit tests for NetworkRecommendationProvider.
Test: Added NetworkRecommendationProviderTest.
BUG: 32909424
Change-Id: Iebe72f260133e9ad1946b0b75e2f69635e154ef3
diff --git a/api/system-current.txt b/api/system-current.txt
index 1d14343..c220ba2 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -25989,11 +25989,15 @@
public abstract class NetworkRecommendationProvider {
ctor public NetworkRecommendationProvider(android.os.Handler);
method public final android.os.IBinder getBinder();
- method public abstract android.net.RecommendationResult onRequestRecommendation(android.net.RecommendationRequest);
+ method public abstract void onRequestRecommendation(android.net.RecommendationRequest, android.net.NetworkRecommendationProvider.ResultCallback);
field public static final java.lang.String EXTRA_RECOMMENDATION_RESULT = "android.net.extra.RECOMMENDATION_RESULT";
field public static final java.lang.String EXTRA_SEQUENCE = "android.net.extra.SEQUENCE";
}
+ public static final class NetworkRecommendationProvider.ResultCallback {
+ method public void onResult(android.net.RecommendationResult);
+ }
+
public class NetworkRequest implements android.os.Parcelable {
method public int describeContents();
method public void writeToParcel(android.os.Parcel, int);
diff --git a/core/java/android/net/NetworkRecommendationProvider.java b/core/java/android/net/NetworkRecommendationProvider.java
index cd2ede8..fc3213f 100644
--- a/core/java/android/net/NetworkRecommendationProvider.java
+++ b/core/java/android/net/NetworkRecommendationProvider.java
@@ -10,6 +10,11 @@
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
/**
* The base class for implementing a network recommendation provider.
* @hide
@@ -42,11 +47,12 @@
*
* @param request a {@link RecommendationRequest} instance containing additional
* request details
- * @return a {@link RecommendationResult} instance containing the recommended
- * network to connect to
+ * @param callback a {@link ResultCallback} instance. When a {@link RecommendationResult} is
+ * available it must be passed into
+ * {@link ResultCallback#onResult(RecommendationResult)}.
*/
- public abstract RecommendationResult onRequestRecommendation(RecommendationRequest request);
-
+ public abstract void onRequestRecommendation(RecommendationRequest request,
+ ResultCallback callback);
/**
* Services that can handle {@link NetworkScoreManager#ACTION_RECOMMEND_NETWORKS} should
@@ -56,6 +62,60 @@
return mService;
}
+ /**
+ * A callback implementing applications should invoke when a {@link RecommendationResult}
+ * is available.
+ */
+ public static final class ResultCallback {
+ private final IRemoteCallback mCallback;
+ private final int mSequence;
+ private final AtomicBoolean mCallbackRun;
+
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public ResultCallback(IRemoteCallback callback, int sequence) {
+ mCallback = callback;
+ mSequence = sequence;
+ mCallbackRun = new AtomicBoolean(false);
+ }
+
+ /**
+ * Run the callback with the available {@link RecommendationResult}.
+ * @param result a {@link RecommendationResult} instance.
+ */
+ public void onResult(RecommendationResult result) {
+ if (!mCallbackRun.compareAndSet(false, true)) {
+ throw new IllegalStateException("The callback cannot be run more than once.");
+ }
+ final Bundle data = new Bundle();
+ data.putInt(EXTRA_SEQUENCE, mSequence);
+ data.putParcelable(EXTRA_RECOMMENDATION_RESULT, result);
+ try {
+ mCallback.sendResult(data);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Callback failed for seq: " + mSequence, e);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ResultCallback that = (ResultCallback) o;
+
+ return mSequence == that.mSequence
+ && Objects.equals(mCallback, that.mCallback);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mCallback, mSequence);
+ }
+ }
+
private final class ServiceHandler extends Handler {
static final int MSG_GET_RECOMMENDATION = 1;
@@ -72,16 +132,8 @@
final int seq = msg.arg1;
final RecommendationRequest request =
msg.getData().getParcelable(EXTRA_RECOMMENDATION_REQUEST);
- final RecommendationResult result = onRequestRecommendation(request);
- final Bundle data = new Bundle();
- data.putInt(EXTRA_SEQUENCE, seq);
- data.putParcelable(EXTRA_RECOMMENDATION_RESULT, result);
- try {
- callback.sendResult(data);
- } catch (RemoteException e) {
- Log.w(TAG, "Callback failed for seq: " + seq, e);
- }
-
+ final ResultCallback resultCallback = new ResultCallback(callback, seq);
+ onRequestRecommendation(request, resultCallback);
break;
default:
diff --git a/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java b/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java
new file mode 100644
index 0000000..5ac8f56
--- /dev/null
+++ b/core/tests/coretests/src/android/net/NetworkRecommendationProviderTest.java
@@ -0,0 +1,119 @@
+package android.net;
+
+import static android.net.NetworkRecommendationProvider.EXTRA_RECOMMENDATION_RESULT;
+import static android.net.NetworkRecommendationProvider.EXTRA_SEQUENCE;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IRemoteCallback;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Unit test for the {@link NetworkRecommendationProvider}.
+ */
+public class NetworkRecommendationProviderTest extends InstrumentationTestCase {
+ @Mock private IRemoteCallback mMockRemoteCallback;
+ private NetworkRecProvider mRecProvider;
+ private Handler mHandler;
+ private INetworkRecommendationProvider mStub;
+ private CountDownLatch mCountDownLatch;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ // Configuration needed to make mockito/dexcache work.
+ final Context context = getInstrumentation().getTargetContext();
+ System.setProperty("dexmaker.dexcache",
+ context.getCacheDir().getPath());
+ ClassLoader newClassLoader = getInstrumentation().getClass().getClassLoader();
+ Thread.currentThread().setContextClassLoader(newClassLoader);
+
+ MockitoAnnotations.initMocks(this);
+
+ HandlerThread thread = new HandlerThread("NetworkRecommendationProviderTest");
+ thread.start();
+ mCountDownLatch = new CountDownLatch(1);
+ mHandler = new Handler(thread.getLooper());
+ mRecProvider = new NetworkRecProvider(mHandler, mCountDownLatch);
+ mStub = INetworkRecommendationProvider.Stub.asInterface(mRecProvider.getBinder());
+ }
+
+ @MediumTest
+ public void testRequestReceived() throws Exception {
+ final RecommendationRequest request = new RecommendationRequest.Builder().build();
+ final int sequence = 100;
+ mStub.requestRecommendation(request, mMockRemoteCallback, sequence);
+
+ // wait for onRequestRecommendation() to be called in our impl below.
+ mCountDownLatch.await(200, TimeUnit.MILLISECONDS);
+ NetworkRecommendationProvider.ResultCallback expectedResultCallback =
+ new NetworkRecommendationProvider.ResultCallback(mMockRemoteCallback, sequence);
+ assertEquals(request, mRecProvider.mCapturedRequest);
+ assertEquals(expectedResultCallback, mRecProvider.mCapturedCallback);
+ }
+
+ @SmallTest
+ public void testResultCallbackOnResult() throws Exception {
+ final int sequence = 100;
+ final NetworkRecommendationProvider.ResultCallback callback =
+ new NetworkRecommendationProvider.ResultCallback(mMockRemoteCallback, sequence);
+
+ final RecommendationResult result = new RecommendationResult(null);
+ callback.onResult(result);
+
+ final ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class);
+ Mockito.verify(mMockRemoteCallback).sendResult(bundleCaptor.capture());
+ Bundle capturedBundle = bundleCaptor.getValue();
+ assertEquals(sequence, capturedBundle.getInt(EXTRA_SEQUENCE));
+ assertSame(result, capturedBundle.getParcelable(EXTRA_RECOMMENDATION_RESULT));
+ }
+
+ @SmallTest
+ public void testResultCallbackOnResult_runTwice_throwsException() throws Exception {
+ final int sequence = 100;
+ final NetworkRecommendationProvider.ResultCallback callback =
+ new NetworkRecommendationProvider.ResultCallback(mMockRemoteCallback, sequence);
+
+ final RecommendationResult result = new RecommendationResult(null);
+ callback.onResult(result);
+
+ try {
+ callback.onResult(result);
+ fail("Callback ran more than once.");
+ } catch (IllegalStateException e) {
+ // expected
+ }
+ }
+
+ private static class NetworkRecProvider extends NetworkRecommendationProvider {
+ private final CountDownLatch mCountDownLatch;
+ RecommendationRequest mCapturedRequest;
+ ResultCallback mCapturedCallback;
+
+ NetworkRecProvider(Handler handler, CountDownLatch countDownLatch) {
+ super(handler);
+ mCountDownLatch = countDownLatch;
+ }
+
+ @Override
+ public void onRequestRecommendation(RecommendationRequest request,
+ ResultCallback callback) {
+ mCapturedRequest = request;
+ mCapturedCallback = callback;
+ mCountDownLatch.countDown();
+ }
+ }
+}