Merge "Configurable SoftAP: Add System API."
diff --git a/api/current.txt b/api/current.txt
index bdec1be..e31aed0 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -30037,7 +30037,7 @@
     method public void setTdlsEnabled(java.net.InetAddress, boolean);
     method public void setTdlsEnabledWithMacAddress(String, boolean);
     method @Deprecated public boolean setWifiEnabled(boolean);
-    method public void startLocalOnlyHotspot(android.net.wifi.WifiManager.LocalOnlyHotspotCallback, @Nullable android.os.Handler);
+    method @RequiresPermission(allOf={android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.ACCESS_FINE_LOCATION}) public void startLocalOnlyHotspot(android.net.wifi.WifiManager.LocalOnlyHotspotCallback, @Nullable android.os.Handler);
     method @Deprecated public boolean startScan();
     method @Deprecated public void startWps(android.net.wifi.WpsInfo, android.net.wifi.WifiManager.WpsCallback);
     method @Deprecated public int updateNetwork(android.net.wifi.WifiConfiguration);
diff --git a/api/system-current.txt b/api/system-current.txt
index 51ea369..a35148a 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -4734,6 +4734,24 @@
     field @Deprecated public byte id;
   }
 
+  public final class SoftApConfiguration implements android.os.Parcelable {
+    method public int describeContents();
+    method @Nullable public android.net.MacAddress getBssid();
+    method @Nullable public String getSsid();
+    method @Nullable public String getWpa2Passphrase();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.net.wifi.SoftApConfiguration> CREATOR;
+  }
+
+  public static final class SoftApConfiguration.Builder {
+    ctor public SoftApConfiguration.Builder();
+    ctor public SoftApConfiguration.Builder(@NonNull android.net.wifi.SoftApConfiguration);
+    method @NonNull public android.net.wifi.SoftApConfiguration build();
+    method @NonNull public android.net.wifi.SoftApConfiguration.Builder setBssid(@Nullable android.net.MacAddress);
+    method @NonNull public android.net.wifi.SoftApConfiguration.Builder setSsid(@Nullable String);
+    method @NonNull public android.net.wifi.SoftApConfiguration.Builder setWpa2Passphrase(@Nullable String);
+  }
+
   public final class WifiClient implements android.os.Parcelable {
     method public int describeContents();
     method @NonNull public android.net.MacAddress getMacAddress();
@@ -4790,6 +4808,7 @@
     method @RequiresPermission(android.Manifest.permission.CHANGE_WIFI_STATE) public boolean setWifiApConfiguration(android.net.wifi.WifiConfiguration);
     method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void startEasyConnectAsConfiguratorInitiator(@NonNull String, int, int, @NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.EasyConnectStatusCallback);
     method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void startEasyConnectAsEnrolleeInitiator(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.EasyConnectStatusCallback);
+    method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void startLocalOnlyHotspot(@NonNull android.net.wifi.SoftApConfiguration, @Nullable java.util.concurrent.Executor, @Nullable android.net.wifi.WifiManager.LocalOnlyHotspotCallback);
     method @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS) public boolean startScan(android.os.WorkSource);
     method @RequiresPermission(anyOf={"android.permission.NETWORK_STACK", android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public boolean startSoftAp(@Nullable android.net.wifi.WifiConfiguration);
     method @RequiresPermission(anyOf={"android.permission.NETWORK_SETTINGS", android.Manifest.permission.NETWORK_SETUP_WIZARD}) public void startSubscriptionProvisioning(@NonNull android.net.wifi.hotspot2.OsuProvider, @NonNull java.util.concurrent.Executor, @NonNull android.net.wifi.hotspot2.ProvisioningCallback);
diff --git a/api/system-lint-baseline.txt b/api/system-lint-baseline.txt
index 57a853a..a907fa6 100644
--- a/api/system-lint-baseline.txt
+++ b/api/system-lint-baseline.txt
@@ -257,6 +257,8 @@
     
 SamShouldBeLast: android.net.ConnectivityManager#createSocketKeepalive(android.net.Network, android.net.IpSecManager.UdpEncapsulationSocket, java.net.InetAddress, java.net.InetAddress, java.util.concurrent.Executor, android.net.SocketKeepalive.Callback):
     
+SamShouldBeLast: android.net.wifi.WifiManager#startLocalOnlyHotspot(android.net.wifi.SoftApConfiguration, java.util.concurrent.Executor, android.net.wifi.WifiManager.LocalOnlyHotspotCallback):
+    
 SamShouldBeLast: android.net.wifi.rtt.WifiRttManager#startRanging(android.net.wifi.rtt.RangingRequest, java.util.concurrent.Executor, android.net.wifi.rtt.RangingResultCallback):
     
 SamShouldBeLast: android.nfc.NfcAdapter#enableReaderMode(android.app.Activity, android.nfc.NfcAdapter.ReaderCallback, int, android.os.Bundle):
diff --git a/wifi/java/android/net/wifi/IWifiManager.aidl b/wifi/java/android/net/wifi/IWifiManager.aidl
index 19be132..023df70 100644
--- a/wifi/java/android/net/wifi/IWifiManager.aidl
+++ b/wifi/java/android/net/wifi/IWifiManager.aidl
@@ -34,6 +34,7 @@
 import android.net.wifi.ITxPacketCountListener;
 import android.net.wifi.IOnWifiUsabilityStatsListener;
 import android.net.wifi.ScanResult;
+import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.WifiActivityEnergyInfo;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiInfo;
@@ -142,7 +143,8 @@
 
     boolean stopSoftAp();
 
-    int startLocalOnlyHotspot(in ILocalOnlyHotspotCallback callback, String packageName);
+    int startLocalOnlyHotspot(in ILocalOnlyHotspotCallback callback, String packageName,
+                              in SoftApConfiguration customConfig);
 
     void stopLocalOnlyHotspot();
 
diff --git a/wifi/java/android/net/wifi/SoftApConfiguration.aidl b/wifi/java/android/net/wifi/SoftApConfiguration.aidl
new file mode 100644
index 0000000..1d06f45
--- /dev/null
+++ b/wifi/java/android/net/wifi/SoftApConfiguration.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 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 android.net.wifi;
+
+parcelable SoftApConfiguration;
\ No newline at end of file
diff --git a/wifi/java/android/net/wifi/SoftApConfiguration.java b/wifi/java/android/net/wifi/SoftApConfiguration.java
new file mode 100644
index 0000000..4cc8653
--- /dev/null
+++ b/wifi/java/android/net/wifi/SoftApConfiguration.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2019 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 android.net.wifi;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.net.MacAddress;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * WiFi configuration for a soft access point (a.k.a. Soft AP, SAP, Hotspot).
+ *
+ * This is input for the framework provided by a client app, i.e. it exposes knobs to instruct the
+ * framework how it should open a hotspot.  It is not meant to describe the network as it will be
+ * seen by clients; this role is currently served by {@link WifiConfiguration} (see
+ * {@link WifiManager.LocalOnlyHotspotReservation#getWifiConfiguration()}).
+ *
+ * System apps can use this to configure a local-only hotspot using
+ * {@link WifiManager#startLocalOnlyHotspot(SoftApConfiguration, Executor,
+ * WifiManager.LocalOnlyHotspotCallback)}.
+ *
+ * Instances of this class are immutable; use {@link SoftApConfiguration.Builder} and its methods to
+ * create a new instance.
+ *
+ * @hide
+ */
+@SystemApi
+public final class SoftApConfiguration implements Parcelable {
+    /**
+     * SSID for the AP, or null for a framework-determined SSID.
+     */
+    private final @Nullable String mSsid;
+    /**
+     * BSSID for the AP, or null to use a framework-determined BSSID.
+     */
+    private final @Nullable MacAddress mBssid;
+    /**
+     * Pre-shared key for WPA2-PSK encryption (non-null enables WPA2-PSK).
+     */
+    private final @Nullable String mWpa2Passphrase;
+
+    /** Private constructor for Builder and Parcelable implementation. */
+    private SoftApConfiguration(
+            @Nullable String ssid, @Nullable MacAddress bssid, String wpa2Passphrase) {
+        mSsid = ssid;
+        mBssid = bssid;
+        mWpa2Passphrase = wpa2Passphrase;
+    }
+
+    @Override
+    public boolean equals(Object otherObj) {
+        if (this == otherObj) {
+            return true;
+        }
+        if (!(otherObj instanceof SoftApConfiguration)) {
+            return false;
+        }
+        SoftApConfiguration other = (SoftApConfiguration) otherObj;
+        return Objects.equals(mSsid, other.mSsid)
+                && Objects.equals(mBssid, other.mBssid)
+                && Objects.equals(mWpa2Passphrase, other.mWpa2Passphrase);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mSsid, mBssid, mWpa2Passphrase);
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString(mSsid);
+        dest.writeParcelable(mBssid, flags);
+        dest.writeString(mWpa2Passphrase);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Creator<SoftApConfiguration> CREATOR = new Creator<SoftApConfiguration>() {
+        @Override
+        public SoftApConfiguration createFromParcel(Parcel in) {
+            return new SoftApConfiguration(
+                    in.readString(),
+                    in.readParcelable(MacAddress.class.getClassLoader()),
+                    in.readString());
+        }
+
+        @Override
+        public SoftApConfiguration[] newArray(int size) {
+            return new SoftApConfiguration[size];
+        }
+    };
+
+    @Nullable
+    public String getSsid() {
+        return mSsid;
+    }
+
+    @Nullable
+    public MacAddress getBssid() {
+        return mBssid;
+    }
+
+    @Nullable
+    public String getWpa2Passphrase() {
+        return mWpa2Passphrase;
+    }
+
+    /**
+     * Builds a {@link SoftApConfiguration}, which allows an app to configure various aspects of a
+     * Soft AP.
+     *
+     * All fields are optional. By default, SSID and BSSID are automatically chosen by the
+     * framework, and an open network is created.
+     */
+    public static final class Builder {
+        private String mSsid;
+        private MacAddress mBssid;
+        private String mWpa2Passphrase;
+
+        /**
+         * Constructs a Builder with default values (see {@link Builder}).
+         */
+        public Builder() {
+            mSsid = null;
+            mBssid = null;
+            mWpa2Passphrase = null;
+        }
+
+        /**
+         * Constructs a Builder initialized from an existing {@link SoftApConfiguration} instance.
+         */
+        public Builder(@NonNull SoftApConfiguration other) {
+            Objects.requireNonNull(other);
+
+            mSsid = other.mSsid;
+            mBssid = other.mBssid;
+            mWpa2Passphrase = other.mWpa2Passphrase;
+        }
+
+        /**
+         * Builds the {@link SoftApConfiguration}.
+         *
+         * @return A new {@link SoftApConfiguration}, as configured by previous method calls.
+         */
+        @NonNull
+        public SoftApConfiguration build() {
+            return new SoftApConfiguration(mSsid, mBssid, mWpa2Passphrase);
+        }
+
+        /**
+         * Specifies an SSID for the AP.
+         *
+         * @param ssid SSID of valid Unicode characters, or null to have the SSID automatically
+         *             chosen by the framework.
+         * @return Builder for chaining.
+         * @throws IllegalArgumentException when the SSID is empty or not valid Unicode.
+         */
+        @NonNull
+        public Builder setSsid(@Nullable String ssid) {
+            if (ssid != null) {
+                Preconditions.checkStringNotEmpty(ssid);
+                Preconditions.checkArgument(StandardCharsets.UTF_8.newEncoder().canEncode(ssid));
+            }
+            mSsid = ssid;
+            return this;
+        }
+
+        /**
+         * Specifies a BSSID for the AP.
+         *
+         * @param bssid BSSID, or null to have the BSSID chosen by the framework. The caller is
+         *              responsible for avoiding collisions.
+         * @return Builder for chaining.
+         * @throws IllegalArgumentException when the given BSSID is the all-zero or broadcast MAC
+         *                                  address.
+         */
+        @NonNull
+        public Builder setBssid(@Nullable MacAddress bssid) {
+            if (bssid != null) {
+                Preconditions.checkArgument(!bssid.equals(MacAddress.ALL_ZEROS_ADDRESS));
+                Preconditions.checkArgument(!bssid.equals(MacAddress.BROADCAST_ADDRESS));
+            }
+            mBssid = bssid;
+            return this;
+        }
+
+        /**
+         * Specifies that this AP should use WPA2-PSK with the given passphrase.  When set to null
+         * and no other encryption method is configured, an open network is created.
+         *
+         * @param passphrase The passphrase to use, or null to unset a previously-set WPA2-PSK
+         *                   configuration.
+         * @return Builder for chaining.
+         * @throws IllegalArgumentException when the passphrase is the empty string
+         */
+        @NonNull
+        public Builder setWpa2Passphrase(@Nullable String passphrase) {
+            if (passphrase != null) {
+                Preconditions.checkStringNotEmpty(passphrase);
+            }
+            mWpa2Passphrase = passphrase;
+            return this;
+        }
+    }
+}
diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java
index 8f71b0b..380ebf1 100644
--- a/wifi/java/android/net/wifi/WifiManager.java
+++ b/wifi/java/android/net/wifi/WifiManager.java
@@ -71,6 +71,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Executor;
 
@@ -2747,13 +2748,6 @@
         }
     }
 
-    private Executor executorForHandler(@Nullable Handler handler) {
-        if (handler == null) {
-            return mContext.getMainExecutor();
-        }
-        return new HandlerExecutor(handler);
-    }
-
     /**
      * Request a local only hotspot that an application can use to communicate between co-located
      * devices connected to the created WiFi hotspot.  The network created by this method will not
@@ -2809,9 +2803,59 @@
      * @param handler Handler to be used for callbacks.  If the caller passes a null Handler, the
      * main thread will be used.
      */
+    @RequiresPermission(allOf = {
+            android.Manifest.permission.CHANGE_WIFI_STATE,
+            android.Manifest.permission.ACCESS_FINE_LOCATION})
     public void startLocalOnlyHotspot(LocalOnlyHotspotCallback callback,
             @Nullable Handler handler) {
-        Executor executor = executorForHandler(handler);
+        Executor executor = handler == null ? null : new HandlerExecutor(handler);
+        startLocalOnlyHotspotInternal(null, executor, callback);
+    }
+
+    /**
+     * Starts a local-only hotspot with a specific configuration applied. See
+     * {@link #startLocalOnlyHotspot(LocalOnlyHotspotCallback, Handler)}.
+     *
+     * Applications need either {@link android.Manifest.permission#NETWORK_SETUP_WIZARD} or
+     * {@link android.Manifest.permission#NETWORK_SETTINGS} to call this method.
+     *
+     * Since custom configuration settings may be incompatible with each other, the hotspot started
+     * through this method cannot coexist with another hotspot created through
+     * startLocalOnlyHotspot. If this is attempted, the first hotspot request wins and others
+     * receive {@link LocalOnlyHotspotCallback#ERROR_GENERIC} through
+     * {@link LocalOnlyHotspotCallback#onFailed}.
+     *
+     * @param config Custom configuration for the hotspot. See {@link SoftApConfiguration}.
+     * @param executor Executor to run callback methods on, or null to use the main thread.
+     * @param callback Callback object for updates about hotspot status, or null for no updates.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD})
+    public void startLocalOnlyHotspot(@NonNull SoftApConfiguration config,
+            @Nullable Executor executor,
+            @Nullable LocalOnlyHotspotCallback callback) {
+        Objects.requireNonNull(config);
+        startLocalOnlyHotspotInternal(config, executor, callback);
+    }
+
+    /**
+     * Common implementation of both configurable and non-configurable LOHS.
+     *
+     * @param config App-specified configuration, or null. When present, additional privileges are
+     *               required, and the hotspot cannot be shared with other clients.
+     * @param executor Executor to run callback methods on, or null to use the main thread.
+     * @param callback Callback object for updates about hotspot status, or null for no updates.
+     */
+    private void startLocalOnlyHotspotInternal(
+            @Nullable SoftApConfiguration config,
+            @Nullable Executor executor,
+            @Nullable LocalOnlyHotspotCallback callback) {
+        if (executor == null) {
+            executor = mContext.getMainExecutor();
+        }
         synchronized (mLock) {
             LocalOnlyHotspotCallbackProxy proxy =
                     new LocalOnlyHotspotCallbackProxy(this, executor, callback);
@@ -2821,7 +2865,7 @@
                     throw new RemoteException("Wifi service is not running");
                 }
                 String packageName = mContext.getOpPackageName();
-                int returnCode = iWifiManager.startLocalOnlyHotspot(proxy, packageName);
+                int returnCode = iWifiManager.startLocalOnlyHotspot(proxy, packageName, config);
                 if (returnCode != LocalOnlyHotspotCallback.REQUEST_REGISTERED) {
                     // Send message to the proxy to make sure we call back on the correct thread
                     proxy.onHotspotFailed(returnCode);
@@ -2902,7 +2946,8 @@
      */
     public void watchLocalOnlyHotspot(LocalOnlyHotspotObserver observer,
             @Nullable Handler handler) {
-        Executor executor = executorForHandler(handler);
+        Executor executor = handler == null ? mContext.getMainExecutor()
+                : new HandlerExecutor(handler);
         synchronized (mLock) {
             mLOHSObserverProxy =
                     new LocalOnlyHotspotObserverProxy(this, executor, observer);
@@ -3484,10 +3529,12 @@
          *
          * @param manager WifiManager
          * @param executor Executor for delivering callbacks.
-         * @param callback LocalOnlyHotspotCallback to notify the calling application.
+         * @param callback LocalOnlyHotspotCallback to notify the calling application, or null.
          */
-        LocalOnlyHotspotCallbackProxy(WifiManager manager, Executor executor,
-                                      LocalOnlyHotspotCallback callback) {
+        LocalOnlyHotspotCallbackProxy(
+                @NonNull WifiManager manager,
+                @NonNull Executor executor,
+                @Nullable LocalOnlyHotspotCallback callback) {
             mWifiManager = new WeakReference<>(manager);
             mExecutor = executor;
             mCallback = callback;
@@ -3505,6 +3552,7 @@
             }
             final LocalOnlyHotspotReservation reservation =
                     manager.new LocalOnlyHotspotReservation(config);
+            if (mCallback == null) return;
             mExecutor.execute(() -> mCallback.onStarted(reservation));
         }
 
@@ -3514,6 +3562,7 @@
             if (manager == null) return;
 
             Log.w(TAG, "LocalOnlyHotspotCallbackProxy: hotspot stopped");
+            if (mCallback == null) return;
             mExecutor.execute(() -> mCallback.onStopped());
         }
 
@@ -3524,6 +3573,7 @@
 
             Log.w(TAG, "LocalOnlyHotspotCallbackProxy: failed to start.  reason: "
                     + reason);
+            if (mCallback == null) return;
             mExecutor.execute(() -> mCallback.onFailed(reason));
         }
     }
diff --git a/wifi/java/com/android/server/wifi/BaseWifiService.java b/wifi/java/com/android/server/wifi/BaseWifiService.java
index 5e6c107..4b7d205 100644
--- a/wifi/java/com/android/server/wifi/BaseWifiService.java
+++ b/wifi/java/com/android/server/wifi/BaseWifiService.java
@@ -32,6 +32,7 @@
 import android.net.wifi.ITxPacketCountListener;
 import android.net.wifi.IWifiManager;
 import android.net.wifi.ScanResult;
+import android.net.wifi.SoftApConfiguration;
 import android.net.wifi.WifiActivityEnergyInfo;
 import android.net.wifi.WifiConfiguration;
 import android.net.wifi.WifiInfo;
@@ -293,8 +294,15 @@
         throw new UnsupportedOperationException();
     }
 
-    @Override
+    /** @deprecated replaced by newer signature */
+    @Deprecated
     public int startLocalOnlyHotspot(ILocalOnlyHotspotCallback callback, String packageName) {
+        return startLocalOnlyHotspot(callback, packageName, null);
+    }
+
+    @Override
+    public int startLocalOnlyHotspot(ILocalOnlyHotspotCallback callback, String packageName,
+            SoftApConfiguration customConfig) {
         throw new UnsupportedOperationException();
     }
 
diff --git a/wifi/tests/src/android/net/wifi/SoftApConfigurationTest.java b/wifi/tests/src/android/net/wifi/SoftApConfigurationTest.java
new file mode 100644
index 0000000..949b479
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/SoftApConfigurationTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2019 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 android.net.wifi;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.MacAddress;
+import android.os.Parcel;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SmallTest
+public class SoftApConfigurationTest {
+    private SoftApConfiguration parcelUnparcel(SoftApConfiguration configIn) {
+        Parcel parcel = Parcel.obtain();
+        parcel.writeParcelable(configIn, 0);
+        parcel.setDataPosition(0);
+        SoftApConfiguration configOut =
+                parcel.readParcelable(SoftApConfiguration.class.getClassLoader());
+        parcel.recycle();
+        return configOut;
+    }
+
+    @Test
+    public void testBasicSettings() {
+        SoftApConfiguration original = new SoftApConfiguration.Builder()
+                .setSsid("ssid")
+                .setBssid(MacAddress.fromString("11:22:33:44:55:66"))
+                .build();
+        assertThat(original.getSsid()).isEqualTo("ssid");
+        assertThat(original.getBssid()).isEqualTo(MacAddress.fromString("11:22:33:44:55:66"));
+        assertThat(original.getWpa2Passphrase()).isNull();
+
+        SoftApConfiguration unparceled = parcelUnparcel(original);
+        assertThat(unparceled).isNotSameAs(original);
+        assertThat(unparceled).isEqualTo(original);
+        assertThat(unparceled.hashCode()).isEqualTo(original.hashCode());
+
+        SoftApConfiguration copy = new SoftApConfiguration.Builder(original).build();
+        assertThat(copy).isNotSameAs(original);
+        assertThat(copy).isEqualTo(original);
+        assertThat(copy.hashCode()).isEqualTo(original.hashCode());
+    }
+
+    @Test
+    public void testWpa2() {
+        SoftApConfiguration original = new SoftApConfiguration.Builder()
+                .setWpa2Passphrase("secretsecret")
+                .build();
+        assertThat(original.getWpa2Passphrase()).isEqualTo("secretsecret");
+
+        SoftApConfiguration unparceled = parcelUnparcel(original);
+        assertThat(unparceled).isNotSameAs(original);
+        assertThat(unparceled).isEqualTo(original);
+        assertThat(unparceled.hashCode()).isEqualTo(original.hashCode());
+
+        SoftApConfiguration copy = new SoftApConfiguration.Builder(original).build();
+        assertThat(copy).isNotSameAs(original);
+        assertThat(copy).isEqualTo(original);
+        assertThat(copy.hashCode()).isEqualTo(original.hashCode());
+    }
+}
diff --git a/wifi/tests/src/android/net/wifi/WifiManagerTest.java b/wifi/tests/src/android/net/wifi/WifiManagerTest.java
index 8d0579b..d2516a3 100644
--- a/wifi/tests/src/android/net/wifi/WifiManagerTest.java
+++ b/wifi/tests/src/android/net/wifi/WifiManagerTest.java
@@ -173,7 +173,7 @@
     public void testCreationAndCloseOfLocalOnlyHotspotReservation() throws Exception {
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         when(mWifiService.startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class),
-                anyString())).thenReturn(REQUEST_REGISTERED);
+                anyString(), eq(null))).thenReturn(REQUEST_REGISTERED);
         mWifiManager.startLocalOnlyHotspot(callback, mHandler);
 
         callback.onStarted(mWifiManager.new LocalOnlyHotspotReservation(mApConfig));
@@ -191,7 +191,7 @@
             throws Exception {
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         when(mWifiService.startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class),
-                anyString())).thenReturn(REQUEST_REGISTERED);
+                anyString(), eq(null))).thenReturn(REQUEST_REGISTERED);
         mWifiManager.startLocalOnlyHotspot(callback, mHandler);
 
         callback.onStarted(mWifiManager.new LocalOnlyHotspotReservation(mApConfig));
@@ -351,7 +351,7 @@
         mWifiManager.startLocalOnlyHotspot(callback, mHandler);
 
         verify(mWifiService)
-                .startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class), anyString());
+                .startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class), anyString(), eq(null));
     }
 
     /**
@@ -362,7 +362,7 @@
     public void testStartLocalOnlyHotspotThrowsSecurityException() throws Exception {
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         doThrow(new SecurityException()).when(mWifiService)
-                .startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class), anyString());
+                .startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class), anyString(), eq(null));
         mWifiManager.startLocalOnlyHotspot(callback, mHandler);
     }
 
@@ -374,7 +374,7 @@
     public void testStartLocalOnlyHotspotThrowsIllegalStateException() throws Exception {
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         doThrow(new IllegalStateException()).when(mWifiService)
-                .startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class), anyString());
+                .startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class), anyString(), eq(null));
         mWifiManager.startLocalOnlyHotspot(callback, mHandler);
     }
 
@@ -385,7 +385,7 @@
     public void testCorrectLooperIsUsedForHandler() throws Exception {
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         when(mWifiService.startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class),
-                anyString())).thenReturn(ERROR_INCOMPATIBLE_MODE);
+                anyString(), eq(null))).thenReturn(ERROR_INCOMPATIBLE_MODE);
         mWifiManager.startLocalOnlyHotspot(callback, mHandler);
         mLooper.dispatchAll();
         assertEquals(ERROR_INCOMPATIBLE_MODE, callback.mFailureReason);
@@ -404,7 +404,7 @@
         when(mContext.getMainExecutor()).thenReturn(altLooper.getNewExecutor());
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         when(mWifiService.startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class),
-                anyString())).thenReturn(ERROR_INCOMPATIBLE_MODE);
+                anyString(), eq(null))).thenReturn(ERROR_INCOMPATIBLE_MODE);
         mWifiManager.startLocalOnlyHotspot(callback, null);
         altLooper.dispatchAll();
         assertEquals(ERROR_INCOMPATIBLE_MODE, callback.mFailureReason);
@@ -423,7 +423,7 @@
         Handler callbackHandler = new Handler(callbackLooper.getLooper());
         ArgumentCaptor<ILocalOnlyHotspotCallback> internalCallback =
                 ArgumentCaptor.forClass(ILocalOnlyHotspotCallback.class);
-        when(mWifiService.startLocalOnlyHotspot(internalCallback.capture(), anyString()))
+        when(mWifiService.startLocalOnlyHotspot(internalCallback.capture(), anyString(), eq(null)))
                 .thenReturn(REQUEST_REGISTERED);
         mWifiManager.startLocalOnlyHotspot(callback, callbackHandler);
         callbackLooper.dispatchAll();
@@ -449,7 +449,7 @@
         Handler callbackHandler = new Handler(callbackLooper.getLooper());
         ArgumentCaptor<ILocalOnlyHotspotCallback> internalCallback =
                 ArgumentCaptor.forClass(ILocalOnlyHotspotCallback.class);
-        when(mWifiService.startLocalOnlyHotspot(internalCallback.capture(), anyString()))
+        when(mWifiService.startLocalOnlyHotspot(internalCallback.capture(), anyString(), eq(null)))
                 .thenReturn(REQUEST_REGISTERED);
         mWifiManager.startLocalOnlyHotspot(callback, callbackHandler);
         callbackLooper.dispatchAll();
@@ -474,7 +474,7 @@
         Handler callbackHandler = new Handler(callbackLooper.getLooper());
         ArgumentCaptor<ILocalOnlyHotspotCallback> internalCallback =
                 ArgumentCaptor.forClass(ILocalOnlyHotspotCallback.class);
-        when(mWifiService.startLocalOnlyHotspot(internalCallback.capture(), anyString()))
+        when(mWifiService.startLocalOnlyHotspot(internalCallback.capture(), anyString(), eq(null)))
                 .thenReturn(REQUEST_REGISTERED);
         mWifiManager.startLocalOnlyHotspot(callback, callbackHandler);
         callbackLooper.dispatchAll();
@@ -497,7 +497,7 @@
         Handler callbackHandler = new Handler(callbackLooper.getLooper());
         ArgumentCaptor<ILocalOnlyHotspotCallback> internalCallback =
                 ArgumentCaptor.forClass(ILocalOnlyHotspotCallback.class);
-        when(mWifiService.startLocalOnlyHotspot(internalCallback.capture(), anyString()))
+        when(mWifiService.startLocalOnlyHotspot(internalCallback.capture(), anyString(), eq(null)))
                 .thenReturn(REQUEST_REGISTERED);
         mWifiManager.startLocalOnlyHotspot(callback, callbackHandler);
         callbackLooper.dispatchAll();
@@ -517,7 +517,7 @@
     public void testLocalOnlyHotspotCallbackFullOnIncompatibleMode() throws Exception {
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         when(mWifiService.startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class),
-                anyString())).thenReturn(ERROR_INCOMPATIBLE_MODE);
+                anyString(), eq(null))).thenReturn(ERROR_INCOMPATIBLE_MODE);
         mWifiManager.startLocalOnlyHotspot(callback, mHandler);
         mLooper.dispatchAll();
         assertEquals(ERROR_INCOMPATIBLE_MODE, callback.mFailureReason);
@@ -533,7 +533,7 @@
     public void testLocalOnlyHotspotCallbackFullOnTetheringDisallowed() throws Exception {
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         when(mWifiService.startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class),
-                anyString())).thenReturn(ERROR_TETHERING_DISALLOWED);
+                anyString(), eq(null))).thenReturn(ERROR_TETHERING_DISALLOWED);
         mWifiManager.startLocalOnlyHotspot(callback, mHandler);
         mLooper.dispatchAll();
         assertEquals(ERROR_TETHERING_DISALLOWED, callback.mFailureReason);
@@ -550,7 +550,7 @@
     public void testLocalOnlyHotspotCallbackFullOnSecurityException() throws Exception {
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         doThrow(new SecurityException()).when(mWifiService)
-                .startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class), anyString());
+                .startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class), anyString(), eq(null));
         try {
             mWifiManager.startLocalOnlyHotspot(callback, mHandler);
         } catch (SecurityException e) {
@@ -571,7 +571,7 @@
     public void testLocalOnlyHotspotCallbackFullOnNoChannelError() throws Exception {
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         when(mWifiService.startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class),
-                anyString())).thenReturn(REQUEST_REGISTERED);
+                anyString(), eq(null))).thenReturn(REQUEST_REGISTERED);
         mWifiManager.startLocalOnlyHotspot(callback, mHandler);
         mLooper.dispatchAll();
         //assertEquals(ERROR_NO_CHANNEL, callback.mFailureReason);
@@ -587,7 +587,7 @@
     public void testCancelLocalOnlyHotspotRequestCallsStopOnWifiService() throws Exception {
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         when(mWifiService.startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class),
-                anyString())).thenReturn(REQUEST_REGISTERED);
+                anyString(), eq(null))).thenReturn(REQUEST_REGISTERED);
         mWifiManager.startLocalOnlyHotspot(callback, mHandler);
         mWifiManager.cancelLocalOnlyHotspotRequest();
         verify(mWifiService).stopLocalOnlyHotspot();
@@ -609,7 +609,7 @@
     public void testCallbackAfterLocalOnlyHotspotWasCancelled() throws Exception {
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         when(mWifiService.startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class),
-                anyString())).thenReturn(REQUEST_REGISTERED);
+                anyString(), eq(null))).thenReturn(REQUEST_REGISTERED);
         mWifiManager.startLocalOnlyHotspot(callback, mHandler);
         mWifiManager.cancelLocalOnlyHotspotRequest();
         verify(mWifiService).stopLocalOnlyHotspot();
@@ -628,7 +628,7 @@
     public void testCancelAfterLocalOnlyHotspotCallbackTriggered() throws Exception {
         TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
         when(mWifiService.startLocalOnlyHotspot(any(ILocalOnlyHotspotCallback.class),
-                anyString())).thenReturn(ERROR_INCOMPATIBLE_MODE);
+                anyString(), eq(null))).thenReturn(ERROR_INCOMPATIBLE_MODE);
         mWifiManager.startLocalOnlyHotspot(callback, mHandler);
         mLooper.dispatchAll();
         assertEquals(ERROR_INCOMPATIBLE_MODE, callback.mFailureReason);
@@ -639,6 +639,17 @@
         verify(mWifiService, never()).stopLocalOnlyHotspot();
     }
 
+    @Test
+    public void testStartLocalOnlyHotspotForwardsCustomConfig() throws Exception {
+        SoftApConfiguration customConfig = new SoftApConfiguration.Builder()
+                .setSsid("customSsid")
+                .build();
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        mWifiManager.startLocalOnlyHotspot(customConfig, mExecutor, callback);
+        verify(mWifiService).startLocalOnlyHotspot(
+                any(ILocalOnlyHotspotCallback.class), anyString(), eq(customConfig));
+    }
+
     /**
      * Verify the watchLocalOnlyHotspot call goes to WifiServiceImpl.
      */