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();
+        }
+    }
+}