[Passpoint] Request Venue URL ANQP following a connection

When connection event is received, send a Venue URL ANQP request
if the connection is Passpoint. Update the internal APIs to allow
updating an existing ANQP entry because the Venue URL is done
separately from the other ANQP requests, and done only after
the connection.

Bug: 162783305
Test: Manual - verify receipt of Venue URL
Test: atest ANQPDataTest ANQPRequestManagerTest AnqpCacheTest PasspointManagerTest
Change-Id: I75737a05f934b7be196885c62aec8f5f1ecf8c8b
diff --git a/service/java/com/android/server/wifi/ClientModeImpl.java b/service/java/com/android/server/wifi/ClientModeImpl.java
index 031fc06..1c0ce8b 100644
--- a/service/java/com/android/server/wifi/ClientModeImpl.java
+++ b/service/java/com/android/server/wifi/ClientModeImpl.java
@@ -3436,8 +3436,9 @@
                     break;
                 }
                 case WifiMonitor.ANQP_DONE_EVENT: {
-                    // TODO(zqiu): remove this when switch over to wificond for ANQP requests.
                     mPasspointManager.notifyANQPDone((AnqpEvent) message.obj);
+                    //TODO(haishalom@): If this was a Venue URL response, need to invoke the
+                    //networking API to display a notification
                     break;
                 }
                 case CMD_STOP_IP_PACKET_OFFLOAD: {
@@ -3449,13 +3450,10 @@
                     break;
                 }
                 case WifiMonitor.RX_HS20_ANQP_ICON_EVENT: {
-                    // TODO(zqiu): remove this when switch over to wificond for icon requests.
                     mPasspointManager.notifyIconDone((IconEvent) message.obj);
                     break;
                 }
                 case WifiMonitor.HS20_REMEDIATION_EVENT: {
-                    // TODO(zqiu): remove this when switch over to wificond for WNM frames
-                    // monitoring.
                     mPasspointManager.receivedWnmFrame((WnmData) message.obj);
                     break;
                 }
@@ -3963,54 +3961,74 @@
                     // work, so disconnect the network and let network selector reselect a new
                     // network.
                     WifiConfiguration config = getConnectedWifiConfigurationInternal();
-                    if (config != null) {
-                        mWifiInfo.setBSSID(mLastBssid);
-                        mWifiInfo.setNetworkId(mLastNetworkId);
-                        mWifiInfo.setMacAddress(mWifiNative.getMacAddress(mInterfaceName));
-
-                        ScanDetailCache scanDetailCache =
-                                mWifiConfigManager.getScanDetailCacheForNetwork(config.networkId);
-                        if (scanDetailCache != null && mLastBssid != null) {
-                            ScanResult scanResult = scanDetailCache.getScanResult(mLastBssid);
-                            if (scanResult != null) {
-                                mWifiInfo.setFrequency(scanResult.frequency);
-                            }
-                        }
-
-                        // We need to get the updated pseudonym from supplicant for EAP-SIM/AKA/AKA'
-                        if (config.enterpriseConfig != null
-                                && config.enterpriseConfig.isAuthenticationSimBased()) {
-                            mLastSubId = mWifiCarrierInfoManager.getBestMatchSubscriptionId(config);
-                            mLastSimBasedConnectionCarrierName =
-                                    mWifiCarrierInfoManager.getCarrierNameforSubId(mLastSubId);
-                            String anonymousIdentity =
-                                    mWifiNative.getEapAnonymousIdentity(mInterfaceName);
-                            if (!TextUtils.isEmpty(anonymousIdentity)
-                                    && !WifiCarrierInfoManager
-                                    .isAnonymousAtRealmIdentity(anonymousIdentity)) {
-                                String decoratedPseudonym = mWifiCarrierInfoManager
-                                        .decoratePseudonymWith3GppRealm(config,
-                                                anonymousIdentity);
-                                if (decoratedPseudonym != null) {
-                                    anonymousIdentity = decoratedPseudonym;
-                                }
-                                if (mVerboseLoggingEnabled) {
-                                    log("EAP Pseudonym: " + anonymousIdentity);
-                                }
-                                // Save the pseudonym only if it is a real one
-                                config.enterpriseConfig.setAnonymousIdentity(anonymousIdentity);
-                            } else {
-                                // Clear any stored pseudonyms
-                                config.enterpriseConfig.setAnonymousIdentity(null);
-                            }
-                            mWifiConfigManager.addOrUpdateNetwork(config, Process.WIFI_UID);
-                        }
-                        transitionTo(mL3ProvisioningState);
-                    } else {
+                    if (config == null) {
                         logw("Connected to unknown networkId " + mLastNetworkId
                                 + ", disconnecting...");
                         sendMessage(CMD_DISCONNECT);
+                        break;
                     }
+                    mWifiInfo.setBSSID(mLastBssid);
+                    mWifiInfo.setNetworkId(mLastNetworkId);
+                    mWifiInfo.setMacAddress(mWifiNative.getMacAddress(mInterfaceName));
+
+                    ScanDetailCache scanDetailCache =
+                            mWifiConfigManager.getScanDetailCacheForNetwork(config.networkId);
+                    ScanResult scanResult = null;
+                    if (scanDetailCache != null && mLastBssid != null) {
+                        scanResult = scanDetailCache.getScanResult(mLastBssid);
+                        if (scanResult != null) {
+                            mWifiInfo.setFrequency(scanResult.frequency);
+                        }
+                    }
+
+                    // We need to get the updated pseudonym from supplicant for EAP-SIM/AKA/AKA'
+                    if (config.enterpriseConfig != null
+                            && config.enterpriseConfig.isAuthenticationSimBased()) {
+                        mLastSubId = mWifiCarrierInfoManager.getBestMatchSubscriptionId(config);
+                        mLastSimBasedConnectionCarrierName =
+                                mWifiCarrierInfoManager.getCarrierNameforSubId(mLastSubId);
+                        String anonymousIdentity =
+                                mWifiNative.getEapAnonymousIdentity(mInterfaceName);
+                        if (!TextUtils.isEmpty(anonymousIdentity)
+                                && !WifiCarrierInfoManager
+                                .isAnonymousAtRealmIdentity(anonymousIdentity)) {
+                            String decoratedPseudonym = mWifiCarrierInfoManager
+                                    .decoratePseudonymWith3GppRealm(config,
+                                            anonymousIdentity);
+                            if (decoratedPseudonym != null) {
+                                anonymousIdentity = decoratedPseudonym;
+                            }
+                            if (mVerboseLoggingEnabled) {
+                                log("EAP Pseudonym: " + anonymousIdentity);
+                            }
+                            // Save the pseudonym only if it is a real one
+                            config.enterpriseConfig.setAnonymousIdentity(anonymousIdentity);
+                        } else {
+                            // Clear any stored pseudonyms
+                            config.enterpriseConfig.setAnonymousIdentity(null);
+                        }
+                        mWifiConfigManager.addOrUpdateNetwork(config, Process.WIFI_UID);
+                    }
+                    // When connecting to Passpoint, ask for the Venue URL
+                    if (config.isPasspoint()) {
+                        if (scanResult == null && mLastBssid != null) {
+                            // The cached scan result of connected network would be null at the
+                            // first connection, try to check full scan result list again to look up
+                            // matched scan result associated to the current SSID and BSSID.
+                            List<ScanResult> scanResults = mScanRequestProxy.getScanResults();
+                            for (ScanResult result : scanResults) {
+                                if (result.SSID.equals(WifiInfo.removeDoubleQuotes(config.SSID))
+                                        && result.BSSID.equals(mLastBssid)) {
+                                    scanResult = result;
+                                    break;
+                                }
+                            }
+                        }
+                        if (scanResult != null) {
+                            mPasspointManager.requestVenueUrlAnqpElement(scanResult);
+                        }
+                    }
+                    transitionTo(mL3ProvisioningState);
                     break;
                 }
                 case WifiMonitor.NETWORK_DISCONNECTION_EVENT: {
diff --git a/service/java/com/android/server/wifi/hotspot2/ANQPData.java b/service/java/com/android/server/wifi/hotspot2/ANQPData.java
index 26286a8..f39f2cf 100644
--- a/service/java/com/android/server/wifi/hotspot2/ANQPData.java
+++ b/service/java/com/android/server/wifi/hotspot2/ANQPData.java
@@ -37,7 +37,7 @@
 
     private final Clock mClock;
     private final Map<Constants.ANQPElementType, ANQPElement> mANQPElements;
-    private final long mExpiryTime;
+    private long mExpiryTime;
 
     public ANQPData(Clock clock, Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
         mClock = clock;
@@ -49,6 +49,16 @@
     }
 
     /**
+     * Update an entry with post association ANQP elelemtns
+     *
+     * @param anqpElements ANQP elements to add
+     */
+    public void update(Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
+        mANQPElements.putAll(anqpElements);
+        mExpiryTime = mClock.getElapsedSinceBootMillis() + DATA_LIFETIME_MILLISECONDS;
+    }
+
+    /**
      * Return the ANQP elements.
      *
      * @return Map of ANQP elements
diff --git a/service/java/com/android/server/wifi/hotspot2/ANQPRequestManager.java b/service/java/com/android/server/wifi/hotspot2/ANQPRequestManager.java
index 2634fa8..071cbd4 100644
--- a/service/java/com/android/server/wifi/hotspot2/ANQPRequestManager.java
+++ b/service/java/com/android/server/wifi/hotspot2/ANQPRequestManager.java
@@ -138,6 +138,22 @@
     }
 
     /**
+     * Request Venue URL ANQP-element from the specified AP post connection.
+     *
+     * @param bssid The BSSID of the AP
+     * @param anqpNetworkKey The unique network key associated with this request
+     * @return true if a request was sent successfully
+     */
+    public boolean requestVenueUrlAnqpElement(long bssid, ANQPNetworkKey anqpNetworkKey) {
+        if (!mPasspointHandler.requestVenueUrlAnqp(bssid)) {
+            return false;
+        }
+
+        mPendingQueries.put(bssid, anqpNetworkKey);
+        return true;
+    }
+
+    /**
      * Notification of the completion of an ANQP request.
      *
      * @param bssid The BSSID of the AP
diff --git a/service/java/com/android/server/wifi/hotspot2/AnqpCache.java b/service/java/com/android/server/wifi/hotspot2/AnqpCache.java
index 9a3a1fb..b8a5b53 100644
--- a/service/java/com/android/server/wifi/hotspot2/AnqpCache.java
+++ b/service/java/com/android/server/wifi/hotspot2/AnqpCache.java
@@ -59,6 +59,24 @@
     }
 
     /**
+     * Add or update additional ANQP elements to an ANQP entry associated with a given key. This
+     * method is useful for Venue URL or any other ANQP element which is queried after a connection
+     * is made.
+     *
+     * @param key The key that's associated with the entry
+     * @param anqpElements The additional ANQP elements from the AP, post connection
+     */
+    public void addOrUpdateEntry(ANQPNetworkKey key,
+            Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
+        if (!mANQPCache.containsKey(key)) {
+            // Create a new entry
+            addEntry(key, anqpElements);
+            return;
+        }
+        mANQPCache.get(key).update(anqpElements);
+    }
+
+    /**
      * Get the ANQP data associated with the given AP.
      *
      * @param key The key that's associated with the entry
diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointManager.java b/service/java/com/android/server/wifi/hotspot2/PasspointManager.java
index b12509e..0de03ee 100644
--- a/service/java/com/android/server/wifi/hotspot2/PasspointManager.java
+++ b/service/java/com/android/server/wifi/hotspot2/PasspointManager.java
@@ -143,7 +143,11 @@
                 Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
             if (mVerboseLoggingEnabled) {
                 Log.d(TAG, "ANQP response received from BSSID "
-                        + Utils.macToString(bssid));
+                        + Utils.macToString(bssid) + " - List of ANQP elements:");
+                int i = 0;
+                for (Constants.ANQPElementType type : anqpElements.keySet()) {
+                    Log.d(TAG, "#" + i++ + ": " + type);
+                }
             }
             // Notify request manager for the completion of a request.
             ANQPNetworkKey anqpKey =
@@ -155,7 +159,7 @@
             }
 
             // Add new entry to the cache.
-            mAnqpCache.addEntry(anqpKey, anqpElements);
+            mAnqpCache.addOrUpdateEntry(anqpKey, anqpElements);
         }
 
         @Override
@@ -1316,4 +1320,27 @@
         params.setRevocationEnabled(false);
         validator.validate(path, params);
     }
+
+    /**
+     * Request the Venue URL ANQP-element from the AP post connection
+     *
+     * @param scanResult Scan result associated to the requested AP
+     */
+    public void requestVenueUrlAnqpElement(@NonNull ScanResult scanResult) {
+        long bssid;
+        try {
+            bssid = Utils.parseMac(scanResult.BSSID);
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Invalid BSSID provided in the scan result: " + scanResult.BSSID);
+            return;
+        }
+        InformationElementUtil.Vsa vsa = InformationElementUtil.getHS2VendorSpecificIE(
+                scanResult.informationElements);
+        ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(scanResult.SSID, bssid, scanResult.hessid,
+                vsa.anqpDomainID);
+        // TODO(haishalom@): Should we limit to R3 only? vsa.hsRelease > NetworkDetail.HSRelease.R2
+        // I am seeing R2's that respond to Venue URL request, so may keep it this way.
+        // APs that do not support this ANQP request simply ignore it.
+        mAnqpRequestManager.requestVenueUrlAnqpElement(bssid, anqpKey);
+    }
 }
diff --git a/service/tests/wifitests/src/com/android/server/wifi/ClientModeImplTest.java b/service/tests/wifitests/src/com/android/server/wifi/ClientModeImplTest.java
index 6dcd862..f4521fa 100644
--- a/service/tests/wifitests/src/com/android/server/wifi/ClientModeImplTest.java
+++ b/service/tests/wifitests/src/com/android/server/wifi/ClientModeImplTest.java
@@ -5249,5 +5249,47 @@
         verify(mLinkProbeCallback, never()).onFailure(anyInt());
         verify(mLinkProbeCallback, never()).onAck(anyInt());
     }
-}
 
+    private void setupPasspointConnection() throws Exception {
+        mConnectedNetwork = spy(WifiConfigurationTestUtil.createPasspointNetwork());
+        mConnectedNetwork.carrierId = CARRIER_ID_1;
+        doReturn(DATA_SUBID).when(mWifiCarrierInfoManager)
+                .getBestMatchSubscriptionId(any(WifiConfiguration.class));
+        when(mDataTelephonyManager.getSimOperator()).thenReturn("123456");
+        when(mDataTelephonyManager.getSimState()).thenReturn(TelephonyManager.SIM_STATE_READY);
+        mConnectedNetwork.enterpriseConfig.setAnonymousIdentity("");
+
+        triggerConnect();
+
+        when(mWifiConfigManager.getScanDetailCacheForNetwork(FRAMEWORK_NETWORK_ID))
+                .thenReturn(mScanDetailCache);
+        when(mScanDetailCache.getScanDetail(sBSSID)).thenReturn(
+                getGoogleGuestScanDetail(TEST_RSSI, sBSSID, sFreq));
+        when(mScanDetailCache.getScanResult(sBSSID)).thenReturn(
+                getGoogleGuestScanDetail(TEST_RSSI, sBSSID, sFreq).getScanResult());
+
+        mCmi.sendMessage(WifiMonitor.NETWORK_CONNECTION_EVENT, 0, 0, sBSSID);
+        mLooper.dispatchAll();
+        assertEquals("L3ProvisioningState", getCurrentState().getName());
+    }
+
+    /**
+     * When connecting to a Passpoint network, verify that the Venue URL ANQP request is sent.
+     */
+    @Test
+    public void testVenueUrlRequestForPasspointNetworks() throws Exception {
+        setupPasspointConnection();
+        verify(mPasspointManager).requestVenueUrlAnqpElement(any(ScanResult.class));
+        assertEquals("L3ProvisioningState", getCurrentState().getName());
+    }
+
+    /**
+     * Verify that the Venue URL ANQP request is not sent for non-Passpoint EAP networks
+     */
+    @Test
+    public void testVenueUrlNotRequestedForNonPasspointNetworks() throws Exception {
+        setupEapSimConnection();
+        verify(mPasspointManager, never()).requestVenueUrlAnqpElement(any(ScanResult.class));
+        assertEquals("L3ProvisioningState", getCurrentState().getName());
+    }
+}
diff --git a/service/tests/wifitests/src/com/android/server/wifi/hotspot2/ANQPDataTest.java b/service/tests/wifitests/src/com/android/server/wifi/hotspot2/ANQPDataTest.java
index a467f82..572ea38 100644
--- a/service/tests/wifitests/src/com/android/server/wifi/hotspot2/ANQPDataTest.java
+++ b/service/tests/wifitests/src/com/android/server/wifi/hotspot2/ANQPDataTest.java
@@ -17,6 +17,7 @@
 package com.android.server.wifi.hotspot2;
 
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.when;
 import static org.mockito.MockitoAnnotations.initMocks;
@@ -27,11 +28,19 @@
 import com.android.server.wifi.WifiBaseTest;
 import com.android.server.wifi.hotspot2.anqp.ANQPElement;
 import com.android.server.wifi.hotspot2.anqp.Constants;
+import com.android.server.wifi.hotspot2.anqp.I18Name;
+import com.android.server.wifi.hotspot2.anqp.VenueNameElement;
+import com.android.server.wifi.hotspot2.anqp.VenueUrlElement;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
 
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 
 /**
@@ -43,6 +52,11 @@
 @SmallTest
 public class ANQPDataTest extends WifiBaseTest {
     @Mock Clock mClock;
+    private static final String TEST_LANGUAGE = "en";
+    private static final Locale TEST_LOCALE = Locale.forLanguageTag(TEST_LANGUAGE);
+    private static final String TEST_VENUE_NAME1 = "Venue1";
+    private static final String TEST_VENUE_NAME2 = "Venue2";
+    private static final String TEST_VENUE_URL1 = "https://www.google.com/";
 
     /**
      * Sets up test.
@@ -77,4 +91,54 @@
         assertFalse(data.expired(ANQPData.DATA_LIFETIME_MILLISECONDS - 1));
         assertTrue(data.expired(ANQPData.DATA_LIFETIME_MILLISECONDS));
     }
+
+    private URL createUrlFromString(String stringUrl) {
+        URL url;
+        try {
+            url = new URL(stringUrl);
+        } catch (java.net.MalformedURLException e) {
+            return null;
+        }
+        return url;
+    }
+
+    /**
+     * Verify creation of ANQPData with data elements and then update the entry.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void createWithElementsAndUpdate() throws Exception {
+        Map<Constants.ANQPElementType, ANQPElement> anqpList1 = new HashMap<>();
+        List<I18Name> nameList = new ArrayList<>();
+        nameList.add(new I18Name(TEST_LANGUAGE, TEST_LOCALE, TEST_VENUE_NAME1));
+        VenueNameElement venueNameElement = new VenueNameElement(nameList);
+
+        // Add one ANQP element
+        anqpList1.put(Constants.ANQPElementType.ANQPVenueName, venueNameElement);
+        ANQPData data = new ANQPData(mClock, anqpList1);
+        assertNotNull(data);
+        assertFalse(data.getElements().isEmpty());
+        assertTrue(data.getElements().get(Constants.ANQPElementType.ANQPVenueName)
+                .equals(venueNameElement));
+
+        // Add another ANQP element to the same entry
+        Map<Constants.ANQPElementType, ANQPElement> anqpList2 = new HashMap<>();
+        Map<Integer, URL> urlList = new HashMap<>();
+        urlList.put(Integer.valueOf(1), createUrlFromString(TEST_VENUE_URL1));
+        VenueUrlElement venueUrlElement = new VenueUrlElement(urlList);
+        anqpList2.put(Constants.ANQPElementType.ANQPVenueUrl, venueUrlElement);
+
+        // Update the name
+        nameList = new ArrayList<>();
+        nameList.add(new I18Name(TEST_LANGUAGE, TEST_LOCALE, TEST_VENUE_NAME2));
+        venueNameElement = new VenueNameElement(nameList);
+        anqpList2.put(Constants.ANQPElementType.ANQPVenueName, venueNameElement);
+
+        data.update(anqpList2);
+        assertTrue(data.getElements().get(Constants.ANQPElementType.ANQPVenueName)
+                .equals(venueNameElement));
+        assertTrue(data.getElements().get(Constants.ANQPElementType.ANQPVenueUrl)
+                .equals(venueUrlElement));
+    }
 }
diff --git a/service/tests/wifitests/src/com/android/server/wifi/hotspot2/ANQPRequestManagerTest.java b/service/tests/wifitests/src/com/android/server/wifi/hotspot2/ANQPRequestManagerTest.java
index 2f84c0d..e13920d 100644
--- a/service/tests/wifitests/src/com/android/server/wifi/hotspot2/ANQPRequestManagerTest.java
+++ b/service/tests/wifitests/src/com/android/server/wifi/hotspot2/ANQPRequestManagerTest.java
@@ -361,4 +361,15 @@
         assertTrue(mManager.requestANQPElements(TEST_BSSID, TEST_ANQP_KEY, true,
                 NetworkDetail.HSRelease.R3));
     }
+
+    /**
+     * Verify that the Venue URL ANQP element is being requested when called.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void requestVenueUrlAnqpElement() throws Exception {
+        when(mHandler.requestVenueUrlAnqp(TEST_BSSID)).thenReturn(true);
+        assertTrue(mManager.requestVenueUrlAnqpElement(TEST_BSSID, TEST_ANQP_KEY));
+    }
 }
diff --git a/service/tests/wifitests/src/com/android/server/wifi/hotspot2/AnqpCacheTest.java b/service/tests/wifitests/src/com/android/server/wifi/hotspot2/AnqpCacheTest.java
index c3c96cd..c36ef31 100644
--- a/service/tests/wifitests/src/com/android/server/wifi/hotspot2/AnqpCacheTest.java
+++ b/service/tests/wifitests/src/com/android/server/wifi/hotspot2/AnqpCacheTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wifi.hotspot2;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -26,13 +27,23 @@
 
 import com.android.server.wifi.Clock;
 import com.android.server.wifi.WifiBaseTest;
-import com.android.server.wifi.hotspot2.ANQPData;
-import com.android.server.wifi.hotspot2.AnqpCache;
+import com.android.server.wifi.hotspot2.anqp.ANQPElement;
+import com.android.server.wifi.hotspot2.anqp.Constants;
+import com.android.server.wifi.hotspot2.anqp.I18Name;
+import com.android.server.wifi.hotspot2.anqp.VenueNameElement;
+import com.android.server.wifi.hotspot2.anqp.VenueUrlElement;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
 
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
 /**
  * Unit tests for {@link com.android.server.wifi.hotspot2.AnqpCache}.
  *
@@ -42,6 +53,13 @@
 @SmallTest
 public class AnqpCacheTest extends WifiBaseTest {
     private static final ANQPNetworkKey ENTRY_KEY = new ANQPNetworkKey("test", 0L, 0L, 1);
+    private static final String TEST_LANGUAGE = "en";
+    private static final Locale TEST_LOCALE = Locale.forLanguageTag(TEST_LANGUAGE);
+    private static final String TEST_VENUE_NAME1 = "Venue1";
+    private static final String TEST_VENUE_NAME2 = "Venue2";
+    private static final String TEST_VENUE_URL1 = "https://www.google.com/";
+    private static final String TEST_VENUE_URL2 = "https://www.android.com/";
+    private static final String TEST_VENUE_URL3 = "https://support.google.com/";
 
     @Mock Clock mClock;
     AnqpCache mCache;
@@ -112,4 +130,52 @@
         mCache.flush();
         assertNull(mCache.getEntry(ENTRY_KEY));
     }
+
+    private URL createUrlFromString(String stringUrl) {
+        URL url;
+        try {
+            url = new URL(stringUrl);
+        } catch (java.net.MalformedURLException e) {
+            return null;
+        }
+        return url;
+    }
+
+    /**
+     * Verify expectation for addOrUpdateEntry and getEntry.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void addThenUpdateAndGetEntry() throws Exception {
+        Map<Constants.ANQPElementType, ANQPElement> anqpList1 = new HashMap<>();
+        List<I18Name> nameList = new ArrayList<>();
+        nameList.add(new I18Name(TEST_LANGUAGE, TEST_LOCALE, TEST_VENUE_NAME1));
+        nameList.add(new I18Name(TEST_LANGUAGE, TEST_LOCALE, TEST_VENUE_NAME2));
+        VenueNameElement venueNameElement = new VenueNameElement(nameList);
+
+        // Add one ANQP element
+        anqpList1.put(Constants.ANQPElementType.ANQPVenueName, venueNameElement);
+        mCache.addOrUpdateEntry(ENTRY_KEY, anqpList1);
+        ANQPData data = mCache.getEntry(ENTRY_KEY);
+        assertNotNull(data);
+        assertFalse(data.getElements().isEmpty());
+        assertTrue(data.getElements().get(Constants.ANQPElementType.ANQPVenueName)
+                .equals(venueNameElement));
+
+        // Add another ANQP element to the same entry
+        Map<Constants.ANQPElementType, ANQPElement> anqpList2 = new HashMap<>();
+        Map<Integer, URL> urlList = new HashMap<>();
+        urlList.put(Integer.valueOf(1), createUrlFromString(TEST_VENUE_URL1));
+        urlList.put(Integer.valueOf(2), createUrlFromString(TEST_VENUE_URL2));
+        urlList.put(Integer.valueOf(4), createUrlFromString(TEST_VENUE_URL3));
+        VenueUrlElement venueUrlElement = new VenueUrlElement(urlList);
+        anqpList2.put(Constants.ANQPElementType.ANQPVenueUrl, venueUrlElement);
+        mCache.addOrUpdateEntry(ENTRY_KEY, anqpList2);
+        data = mCache.getEntry(ENTRY_KEY);
+        assertTrue(data.getElements().get(Constants.ANQPElementType.ANQPVenueName)
+                .equals(venueNameElement));
+        assertTrue(data.getElements().get(Constants.ANQPElementType.ANQPVenueUrl)
+                .equals(venueUrlElement));
+    }
 }
diff --git a/service/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java b/service/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java
index ea4cdb9..67ac9d8 100644
--- a/service/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java
+++ b/service/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointManagerTest.java
@@ -441,7 +441,7 @@
 
         when(mAnqpRequestManager.onRequestCompleted(TEST_BSSID, true)).thenReturn(TEST_ANQP_KEY);
         mCallbacks.onANQPResponse(TEST_BSSID, anqpElementMap);
-        verify(mAnqpCache).addEntry(TEST_ANQP_KEY, anqpElementMap);
+        verify(mAnqpCache).addOrUpdateEntry(TEST_ANQP_KEY, anqpElementMap);
         verify(mContext, never()).sendBroadcastAsUser(any(Intent.class), any(UserHandle.class),
                 any(String.class));
     }
@@ -460,7 +460,7 @@
 
         when(mAnqpRequestManager.onRequestCompleted(TEST_BSSID, true)).thenReturn(null);
         mCallbacks.onANQPResponse(TEST_BSSID, anqpElementMap);
-        verify(mAnqpCache, never()).addEntry(any(ANQPNetworkKey.class), anyMap());
+        verify(mAnqpCache, never()).addOrUpdateEntry(any(ANQPNetworkKey.class), anyMap());
     }
 
     /**
@@ -472,7 +472,7 @@
     public void anqpResponseFailure() throws Exception {
         when(mAnqpRequestManager.onRequestCompleted(TEST_BSSID, false)).thenReturn(TEST_ANQP_KEY);
         mCallbacks.onANQPResponse(TEST_BSSID, null);
-        verify(mAnqpCache, never()).addEntry(any(ANQPNetworkKey.class), anyMap());
+        verify(mAnqpCache, never()).addOrUpdateEntry(any(ANQPNetworkKey.class), anyMap());
 
     }
 
@@ -2572,4 +2572,28 @@
         verify(mWifiMetrics).incrementNumPasspointProviderInstallation();
         verify(mWifiMetrics).incrementNumPasspointProviderInstallSuccess();
     }
+
+    /**
+     * Verify that venue URL ANQP request is sent correctly.
+     *
+     * @throws Exception
+     */
+    @Test
+    public void verifyRequestVenueUrlAnqpElement() throws Exception {
+        // static mocking
+        MockitoSession session =
+                com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession().mockStatic(
+                        InformationElementUtil.class).startMocking();
+        try {
+            ScanResult scanResult = createTestScanResult();
+            InformationElementUtil.Vsa vsa = new InformationElementUtil.Vsa();
+            vsa.anqpDomainID = scanResult.anqpDomainId;
+            when(InformationElementUtil.getHS2VendorSpecificIE(isNull())).thenReturn(vsa);
+            long bssid = Utils.parseMac(scanResult.BSSID);
+            mManager.requestVenueUrlAnqpElement(scanResult);
+            verify(mAnqpRequestManager).requestVenueUrlAnqpElement(eq(bssid), any());
+        } finally {
+            session.finishMocking();
+        }
+    }
 }