/*
 * Copyright (C) 2010 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 com.android.server.wifi;

import static android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA2_PSK;
import static android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION;
import static android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA3_SAE;
import static android.net.wifi.SoftApConfiguration.SECURITY_TYPE_WPA3_SAE_TRANSITION;

import static com.android.server.wifi.WifiSettingsConfigStore.WIFI_STATIC_CHIP_INFO;

import android.annotation.NonNull;
import android.app.compat.CompatChanges;
import android.content.Context;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.MacAddress;
import android.net.wifi.SoftApCapability;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.SoftApConfiguration.BandType;
import android.net.wifi.WifiSsid;
import android.os.Handler;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseIntArray;

import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.MacAddressUtils;
import com.android.server.wifi.util.ApConfigUtil;
import com.android.wifi.resources.R;

import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Random;

import javax.annotation.Nullable;

/**
 * Provides API for reading/writing soft access point configuration.
 */
public class WifiApConfigStore {

    // Intent when user has interacted with the softap settings change notification
    public static final String ACTION_HOTSPOT_CONFIG_USER_TAPPED_CONTENT =
            "com.android.server.wifi.WifiApConfigStoreUtil.HOTSPOT_CONFIG_USER_TAPPED_CONTENT";

    private static final String TAG = "WifiApConfigStore";

    private static final int RAND_SSID_INT_MIN = 1000;
    private static final int RAND_SSID_INT_MAX = 9999;

    @VisibleForTesting
    static final int SAE_ASCII_MIN_LEN = 1;
    @VisibleForTesting
    static final int PSK_ASCII_MIN_LEN = 8;
    @VisibleForTesting
    static final int PSK_SAE_ASCII_MAX_LEN = 63;

    // Should only be accessed via synchronized methods.
    private SoftApConfiguration mPersistentWifiApConfig = null;
    private String mLastConfiguredPassphrase = null;

    private final Context mContext;
    private final Handler mHandler;
    private final WifiMetrics mWifiMetrics;
    private final BackupManagerProxy mBackupManagerProxy;
    private final MacAddressUtil mMacAddressUtil;
    private final WifiConfigManager mWifiConfigManager;
    private final ActiveModeWarden mActiveModeWarden;
    private final WifiNative mWifiNative;
    private final HalDeviceManager mHalDeviceManager;
    private final WifiSettingsConfigStore mWifiSettingsConfigStore;
    private boolean mHasNewDataToSerialize = false;
    private boolean mForceApChannel = false;
    private int mForcedApBand;
    private int mForcedApChannel;
    private int mForcedApMaximumChannelBandWidth;
    private final boolean mIsAutoAppendLowerBandEnabled;

    /**
     * Module to interact with the wifi config store.
     */
    private class SoftApStoreDataSource implements SoftApStoreData.DataSource {

        public SoftApConfiguration toSerialize() {
            mHasNewDataToSerialize = false;
            return mPersistentWifiApConfig;
        }

        public void fromDeserialized(SoftApConfiguration config) {
            if (config.getPersistentRandomizedMacAddress() == null) {
                config = updatePersistentRandomizedMacAddress(config);
            }
            mPersistentWifiApConfig = new SoftApConfiguration.Builder(config).build();
            if (!TextUtils.isEmpty(mPersistentWifiApConfig.getPassphrase())) {
                mLastConfiguredPassphrase = mPersistentWifiApConfig.getPassphrase();
            }
        }

        public void reset() {
            mPersistentWifiApConfig = null;
        }

        public boolean hasNewDataToSerialize() {
            return mHasNewDataToSerialize;
        }
    }

    WifiApConfigStore(Context context,
            WifiInjector wifiInjector,
            Handler handler,
            BackupManagerProxy backupManagerProxy,
            WifiConfigStore wifiConfigStore,
            WifiConfigManager wifiConfigManager,
            ActiveModeWarden activeModeWarden,
            WifiMetrics wifiMetrics) {
        mContext = context;
        mHandler = handler;
        mBackupManagerProxy = backupManagerProxy;
        mWifiConfigManager = wifiConfigManager;
        mActiveModeWarden = activeModeWarden;
        mWifiMetrics = wifiMetrics;
        mWifiNative = wifiInjector.getWifiNative();
        // Register store data listener
        wifiConfigStore.registerStoreData(
                wifiInjector.makeSoftApStoreData(new SoftApStoreDataSource()));

        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_HOTSPOT_CONFIG_USER_TAPPED_CONTENT);
        mMacAddressUtil = wifiInjector.getMacAddressUtil();
        mIsAutoAppendLowerBandEnabled = mContext.getResources().getBoolean(
                R.bool.config_wifiSoftapAutoAppendLowerBandsToBandConfigurationEnabled);
        mHalDeviceManager = wifiInjector.getHalDeviceManager();
        mWifiSettingsConfigStore = wifiInjector.getSettingsConfigStore();
        mWifiSettingsConfigStore.registerChangeListener(WIFI_STATIC_CHIP_INFO,
                (key, value) -> {
                    if (mPersistentWifiApConfig != null
                            && mHalDeviceManager.isConcurrencyComboLoadedFromDriver()) {
                        Log.i(TAG, "Chip capability is updated, check config");
                        SoftApConfiguration.Builder configBuilder =
                                new SoftApConfiguration.Builder(mPersistentWifiApConfig);
                        if (SdkLevel.isAtLeastS()
                                && mPersistentWifiApConfig.getBands().length > 1) {
                            // Current band setting is dual band, check if device supports it.
                            if (!ApConfigUtil.isBridgedModeSupported(mContext, mWifiNative)) {
                                Log.i(TAG, "Chip doesn't support bridgedAp, reset to default band");
                                configBuilder.setBand(generateDefaultBand(mContext));
                                persistConfigAndTriggerBackupManagerProxy(configBuilder.build());
                            }
                        }
                    }
                }, mHandler);
    }

    /**
     * Return the current soft access point configuration.
     */
    public synchronized SoftApConfiguration getApConfiguration() {
        if (mPersistentWifiApConfig == null) {
            /* Use default configuration. */
            Log.d(TAG, "Fallback to use default AP configuration");
            persistConfigAndTriggerBackupManagerProxy(
                    updatePersistentRandomizedMacAddress(getDefaultApConfiguration()));
        }
        SoftApConfiguration sanitizedPersistentconfig =
                sanitizePersistentApConfig(mPersistentWifiApConfig);
        if (!Objects.equals(mPersistentWifiApConfig, sanitizedPersistentconfig)) {
            Log.d(TAG, "persisted config was converted, need to resave it");
            persistConfigAndTriggerBackupManagerProxy(sanitizedPersistentconfig);
        }

        if (mForceApChannel) {
            Log.d(TAG, "getApConfiguration: Band force to "
                    + mForcedApBand
                    + ", and channel force to "
                    + mForcedApChannel
                    + ", and maximum channel width limited to "
                    + mForcedApMaximumChannelBandWidth);
            if (SdkLevel.isAtLeastT()) {
                return mForcedApChannel == 0
                        ? new SoftApConfiguration.Builder(mPersistentWifiApConfig)
                                .setBand(mForcedApBand)
                                .setMaxChannelBandwidth(mForcedApMaximumChannelBandWidth)
                                .build()
                        : new SoftApConfiguration.Builder(mPersistentWifiApConfig)
                                .setChannel(mForcedApChannel, mForcedApBand)
                                .setMaxChannelBandwidth(mForcedApMaximumChannelBandWidth)
                                .build();
            } else {
                return mForcedApChannel == 0
                        ? new SoftApConfiguration.Builder(mPersistentWifiApConfig)
                                .setBand(mForcedApBand)
                                .build()
                        : new SoftApConfiguration.Builder(mPersistentWifiApConfig)
                                .setChannel(mForcedApChannel, mForcedApBand)
                                .build();
            }
        }
        return mPersistentWifiApConfig;
    }

    /**
     * Update the current soft access point configuration.
     * Restore to default AP configuration if null is provided.
     * This can be invoked under context of binder threads (WifiManager.setWifiApConfiguration)
     * and the main Wifi thread (CMD_START_AP).
     */
    public synchronized void setApConfiguration(SoftApConfiguration config) {
        SoftApConfiguration newConfig = config == null ? getDefaultApConfiguration()
                : new SoftApConfiguration.Builder(sanitizePersistentApConfig(config))
                        .setUserConfiguration(true).build();
        persistConfigAndTriggerBackupManagerProxy(
                updatePersistentRandomizedMacAddress(newConfig));
    }

    /**
     * Returns SoftApConfiguration in which some parameters might be upgrade to supported default
     * configuration.
     */
    public synchronized SoftApConfiguration upgradeSoftApConfiguration(
            @NonNull SoftApConfiguration config) {
        SoftApConfiguration.Builder configBuilder = new SoftApConfiguration.Builder(config);
        if (SdkLevel.isAtLeastS() && ApConfigUtil.isBridgedModeSupported(mContext, mWifiNative)
                && config.getBands().length == 1 && mContext.getResources().getBoolean(
                        R.bool.config_wifiSoftapAutoUpgradeToBridgedConfigWhenSupported)) {
            int[] dual_bands = new int[] {
                    SoftApConfiguration.BAND_2GHZ,
                    SoftApConfiguration.BAND_2GHZ | SoftApConfiguration.BAND_5GHZ};
            if (SdkLevel.isAtLeastS()) {
                configBuilder.setBands(dual_bands);
            }
            Log.i(TAG, "Device support bridged AP, upgrade band setting to bridged configuration");
        }
        return configBuilder.build();
    }

    /**
     * Returns SoftApConfiguration in which some parameters might be reset to supported default
     * config since it depends on UI or HW.
     *
     * MaxNumberOfClients and isClientControlByUserEnabled will need HAL support client force
     * disconnect, and Band setting (5g/6g) need HW support.
     *
     * HiddenSsid, Channel, ShutdownTimeoutMillis and AutoShutdownEnabled are features
     * which need UI(Setting) support.
     *
     * SAE/SAE-Transition need hardware support, reset to secured WPA2 security type when device
     * doesn't support it.
     *
     * Check band(s) setting to make sure all of the band(s) are supported.
     * - If previous bands configuration is bridged mode. Reset to 2.4G when device doesn't support
     *   it.
     */
    public synchronized SoftApConfiguration resetToDefaultForUnsupportedConfig(
            @NonNull SoftApConfiguration config) {
        SoftApConfiguration.Builder configBuilder = new SoftApConfiguration.Builder(config);
        if ((!ApConfigUtil.isClientForceDisconnectSupported(mContext)
                || mContext.getResources().getBoolean(
                R.bool.config_wifiSoftapResetUserControlConfig))
                && (config.isClientControlByUserEnabled()
                || config.getBlockedClientList().size() != 0)) {
            configBuilder.setClientControlByUserEnabled(false);
            configBuilder.setBlockedClientList(new ArrayList<>());
            Log.i(TAG, "Reset ClientControlByUser to false due to device doesn't support");
        }

        if ((!ApConfigUtil.isClientForceDisconnectSupported(mContext)
                || mContext.getResources().getBoolean(
                R.bool.config_wifiSoftapResetMaxClientSettingConfig))
                && config.getMaxNumberOfClients() != 0) {
            configBuilder.setMaxNumberOfClients(0);
            Log.i(TAG, "Reset MaxNumberOfClients to 0 due to device doesn't support");
        }

        if (!ApConfigUtil.isWpa3SaeSupported(mContext) && (config.getSecurityType()
                == SECURITY_TYPE_WPA3_SAE
                || config.getSecurityType()
                == SECURITY_TYPE_WPA3_SAE_TRANSITION)) {
            try {
                configBuilder.setPassphrase(generatePassword(),
                        SECURITY_TYPE_WPA2_PSK);
            } catch (IllegalArgumentException e) {
                Log.wtf(TAG, "Generated password was invalid: " + e);
            }
            Log.i(TAG, "Device doesn't support WPA3-SAE, reset config to WPA2");
        }

        if (mContext.getResources().getBoolean(R.bool.config_wifiSoftapResetChannelConfig)
                && config.getChannel() != 0) {
            // The device might not support customize channel or forced channel might not
            // work in some countries. Need to reset it.
            configBuilder.setBand(ApConfigUtil.append24GToBandIf24GSupported(
                    config.getBand(), mContext));
            Log.i(TAG, "Reset SAP channel configuration");
        }

        if (SdkLevel.isAtLeastS() && config.getBands().length > 1) {
            if (!ApConfigUtil.isBridgedModeSupported(mContext, mWifiNative)
                    || !isBandsSupported(config.getBands(), mContext)) {
                int newSingleApBand = 0;
                for (int targetBand : config.getBands()) {
                    int availableBand = ApConfigUtil.removeUnsupportedBands(
                            mContext, targetBand);
                    newSingleApBand |= availableBand;
                }
                newSingleApBand = ApConfigUtil.append24GToBandIf24GSupported(
                        newSingleApBand, mContext);
                configBuilder.setBand(newSingleApBand);
                Log.i(TAG, "An unsupported band setting for the bridged mode, force to "
                        + newSingleApBand);
            }
        } else {
            // Single band case, check and remove unsupported band.
            int newBand = ApConfigUtil.removeUnsupportedBands(mContext, config.getBand());
            if (newBand != config.getBand()) {
                newBand = ApConfigUtil.append24GToBandIf24GSupported(newBand, mContext);
                Log.i(TAG, "Reset band from " + config.getBand() + " to "
                        + newBand);
                configBuilder.setBand(newBand);
            }
        }

        if (mContext.getResources().getBoolean(R.bool.config_wifiSoftapResetHiddenConfig)
                && config.isHiddenSsid()) {
            configBuilder.setHiddenSsid(false);
            Log.i(TAG, "Reset SAP Hidden Network configuration");
        }

        if (mContext.getResources().getBoolean(
                R.bool.config_wifiSoftapResetAutoShutdownTimerConfig)
                && config.getShutdownTimeoutMillis() > 0) {
            if (CompatChanges.isChangeEnabled(
                    SoftApConfiguration.REMOVE_ZERO_FOR_TIMEOUT_SETTING)) {
                configBuilder.setShutdownTimeoutMillis(SoftApConfiguration.DEFAULT_TIMEOUT);
            } else {
                configBuilder.setShutdownTimeoutMillis(0);
            }
            Log.i(TAG, "Reset SAP auto shutdown configuration");
        }

        if (!ApConfigUtil.isApMacRandomizationSupported(mContext)) {
            if (SdkLevel.isAtLeastS()) {
                configBuilder.setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE);
                Log.i(TAG, "Force set SAP MAC randomization to NONE when not supported");
            }
        }

        mWifiMetrics.noteSoftApConfigReset(config, configBuilder.build());
        return configBuilder.build();
    }

    private SoftApConfiguration sanitizePersistentApConfig(SoftApConfiguration config) {
        SoftApConfiguration.Builder convertedConfigBuilder =
                new SoftApConfiguration.Builder(config);
        int[] bands = config.getBands();
        SparseIntArray newChannels = new SparseIntArray();
        // The bands length should always 1 in R. Adding SdkLevel.isAtLeastS for lint check only.
        for (int i = 0; i < bands.length; i++) {
            int channel = SdkLevel.isAtLeastS()
                    ? config.getChannels().valueAt(i) : config.getChannel();
            int newBand = bands[i];
            if (channel == 0 && mIsAutoAppendLowerBandEnabled
                    && ApConfigUtil.isBandSupported(newBand, mContext)) {
                // some countries are unable to support 5GHz only operation, always allow for 2GHz
                // when config doesn't force channel
                if ((newBand & SoftApConfiguration.BAND_2GHZ) == 0) {
                    newBand = ApConfigUtil.append24GToBandIf24GSupported(newBand, mContext);
                }
                // If the 6G configuration doesn't includes 5G band (2.4G have appended because
                // countries reason), it will cause that driver can't switch channel from 6G to
                // 5G/2.4G when coexistence happened (For instance: wifi connected to 2.4G or 5G
                // channel). Always append 5G into band configuration when configured band includes
                // 6G.
                if ((newBand & SoftApConfiguration.BAND_6GHZ) != 0
                        && (newBand & SoftApConfiguration.BAND_5GHZ) == 0) {
                    newBand = ApConfigUtil.append5GToBandIf5GSupported(newBand, mContext);
                }
            }
            newChannels.put(newBand, channel);
        }
        if (SdkLevel.isAtLeastS()) {
            convertedConfigBuilder.setChannels(newChannels);
        } else if (bands.length > 0 && newChannels.valueAt(0) == 0) {
            convertedConfigBuilder.setBand(newChannels.keyAt(0));
        }
        return convertedConfigBuilder.build();
    }

    private synchronized void persistConfigAndTriggerBackupManagerProxy(
            SoftApConfiguration config) {
        mPersistentWifiApConfig = config;
        if (!TextUtils.isEmpty(config.getPassphrase())) {
            mLastConfiguredPassphrase = config.getPassphrase();
        }
        mHasNewDataToSerialize = true;
        mHandler.post(() -> mWifiConfigManager.saveToStore());
        mBackupManagerProxy.notifyDataChanged();
    }

    /**
     * Generate a default WPA3 SAE transition (if supported) or WPA2 based
     * configuration with a random password.
     * We are changing the Wifi Ap configuration storage from secure settings to a
     * flat file accessible only by the system. A WPA2 based default configuration
     * will keep the device secure after the update.
     */
    private SoftApConfiguration getDefaultApConfiguration() {
        SoftApConfiguration.Builder configBuilder = new SoftApConfiguration.Builder();
        configBuilder.setBand(generateDefaultBand(mContext));
        configBuilder.setSsid(mContext.getResources().getString(
                R.string.wifi_tether_configure_ssid_default) + "_" + getRandomIntForDefaultSsid());
        try {
            if (ApConfigUtil.isWpa3SaeSupported(mContext)) {
                configBuilder.setPassphrase(generatePassword(),
                        SECURITY_TYPE_WPA3_SAE_TRANSITION);
            } else {
                configBuilder.setPassphrase(generatePassword(),
                        SECURITY_TYPE_WPA2_PSK);
            }
        } catch (IllegalArgumentException e) {
            Log.wtf(TAG, "Generated password was invalid: " + e);
        }

        // It is new overlay configuration, it should always false in R. Add SdkLevel.isAtLeastS for
        // lint check
        if (SdkLevel.isAtLeastS()) {
            boolean isBridgedModeSupported = mHalDeviceManager.isConcurrencyComboLoadedFromDriver()
                    ? ApConfigUtil.isBridgedModeSupported(mContext, mWifiNative)
                            : ApConfigUtil.isBridgedModeSupportedInConfig(mContext);
            if (isBridgedModeSupported) {
                int[] dual_bands = new int[] {
                        SoftApConfiguration.BAND_2GHZ,
                        SoftApConfiguration.BAND_2GHZ | SoftApConfiguration.BAND_5GHZ};
                configBuilder.setBands(dual_bands);
            }
        }

        // Update default MAC randomization setting to NONE when feature doesn't support it.
        if (!ApConfigUtil.isApMacRandomizationSupported(mContext)) {
            if (SdkLevel.isAtLeastS()) {
                configBuilder.setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE);
            }
        }

        configBuilder.setUserConfiguration(false);
        return configBuilder.build();
    }

    private static int getRandomIntForDefaultSsid() {
        Random random = new Random();
        return random.nextInt((RAND_SSID_INT_MAX - RAND_SSID_INT_MIN) + 1) + RAND_SSID_INT_MIN;
    }

    private static String generateLohsSsid(Context context) {
        return context.getResources().getString(
                R.string.wifi_localhotspot_configure_ssid_default) + "_"
                + getRandomIntForDefaultSsid();
    }

    private static boolean hasAutomotiveFeature(Context context) {
        return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
    }

    /**
     * Generate a temporary WPA2 based configuration for use by the local only hotspot.
     * This config is not persisted and will not be stored by the WifiApConfigStore.
     */
    public SoftApConfiguration generateLocalOnlyHotspotConfig(@NonNull Context context,
            @Nullable SoftApConfiguration customConfig, @NonNull SoftApCapability capability) {
        SoftApConfiguration.Builder configBuilder;
        if (customConfig != null) {
            configBuilder = new SoftApConfiguration.Builder(customConfig);
            // Make sure that we use available band on old build.
            if (!SdkLevel.isAtLeastT()
                    && !isBandsSupported(customConfig.getBands(), context)) {
                configBuilder.setBand(generateDefaultBand(context));
            }
        } else {
            configBuilder = new SoftApConfiguration.Builder();
            // Make sure the default band configuration is supported.
            configBuilder.setBand(generateDefaultBand(context));
            // Default to disable the auto shutdown
            configBuilder.setAutoShutdownEnabled(false);
            try {
                if (ApConfigUtil.isWpa3SaeSupported(context)) {
                    configBuilder.setPassphrase(generatePassword(),
                            SECURITY_TYPE_WPA3_SAE_TRANSITION);
                } else {
                    configBuilder.setPassphrase(generatePassword(),
                            SECURITY_TYPE_WPA2_PSK);
                }
            } catch (IllegalArgumentException e) {
                Log.wtf(TAG, "Generated password was invalid: " + e);
            }
            synchronized (this) {
                // Update default MAC randomization setting to NONE when feature doesn't support
                // it, or it was disabled in tethered mode.
                if (!ApConfigUtil.isApMacRandomizationSupported(context)
                        || (mPersistentWifiApConfig != null
                        && mPersistentWifiApConfig.getMacRandomizationSettingInternal()
                        == SoftApConfiguration.RANDOMIZATION_NONE)) {
                    if (SdkLevel.isAtLeastS()) {
                        configBuilder.setMacRandomizationSetting(
                                SoftApConfiguration.RANDOMIZATION_NONE);
                    }
                }
            }
        }

        // Automotive mode can force the LOHS to specific bands
        if (hasAutomotiveFeature(context)) {
            int desiredBand = SoftApConfiguration.BAND_2GHZ;
            if (context.getResources().getBoolean(R.bool.config_wifiLocalOnlyHotspot6ghz)
                    && ApConfigUtil.isBandSupported(SoftApConfiguration.BAND_6GHZ, mContext)) {
                desiredBand |= SoftApConfiguration.BAND_6GHZ;
            }
            if (context.getResources().getBoolean(R.bool.config_wifi_local_only_hotspot_5ghz)
                    && ApConfigUtil.isBandSupported(SoftApConfiguration.BAND_5GHZ, mContext)) {
                desiredBand |= SoftApConfiguration.BAND_5GHZ;
            }
            configBuilder.setBand(desiredBand);
        }
        if (customConfig == null || customConfig.getSsid() == null) {
            configBuilder.setSsid(generateLohsSsid(context));
        }

        return updatePersistentRandomizedMacAddress(configBuilder.build());
    }

    /**
     * @return a copy of the given SoftApConfig with the BSSID randomized, unless a custom BSSID is
     * already set.
     */
    SoftApConfiguration randomizeBssidIfUnset(Context context, SoftApConfiguration config) {
        SoftApConfiguration.Builder configBuilder = new SoftApConfiguration.Builder(config);
        if (config.getBssid() == null && ApConfigUtil.isApMacRandomizationSupported(mContext)
                && config.getMacRandomizationSettingInternal()
                    != SoftApConfiguration.RANDOMIZATION_NONE) {
            MacAddress macAddress = null;
            if (config.getMacRandomizationSettingInternal()
                    == SoftApConfiguration.RANDOMIZATION_PERSISTENT) {
                macAddress = config.getPersistentRandomizedMacAddress();
                if (macAddress == null) {
                    WifiSsid ssid = config.getWifiSsid();
                    macAddress = mMacAddressUtil.calculatePersistentMacForSap(
                            ssid != null ? ssid.toString() : null, Process.WIFI_UID);
                    if (macAddress == null) {
                        Log.e(TAG, "Failed to calculate MAC from SSID. "
                                + "Generating new random MAC instead.");
                    }
                }
            }
            if (macAddress == null) {
                macAddress = MacAddressUtils.createRandomUnicastAddress();
            }
            configBuilder.setBssid(macAddress);
            if (macAddress != null && SdkLevel.isAtLeastS()) {
                configBuilder.setMacRandomizationSetting(SoftApConfiguration.RANDOMIZATION_NONE);
            }
        }
        return configBuilder.build();
    }

    /**
     * Verify provided preSharedKey in ap config for WPA2_PSK/WPA3_SAE (Transition) network
     * meets requirements.
     */
    @SuppressWarnings("ReturnValueIgnored")
    private static boolean validateApConfigAsciiPreSharedKey(
            @SoftApConfiguration.SecurityType int securityType, String preSharedKey) {
        final int sharedKeyLen = preSharedKey.length();
        final int keyMinLen = securityType == SECURITY_TYPE_WPA3_SAE
                ? SAE_ASCII_MIN_LEN : PSK_ASCII_MIN_LEN;
        if (sharedKeyLen < keyMinLen || sharedKeyLen > PSK_SAE_ASCII_MAX_LEN) {
            Log.d(TAG, "softap network password string size must be at least " + keyMinLen
                    + " and no more than " + PSK_SAE_ASCII_MAX_LEN + " when type is "
                    + securityType);
            return false;
        }

        try {
            preSharedKey.getBytes(StandardCharsets.UTF_8);
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "softap network password verification failed: malformed string");
            return false;
        }
        return true;
    }

    /**
     * Validate a SoftApConfiguration is properly configured for use by SoftApManager.
     *
     * This method checks for consistency between security settings (if it requires a password, was
     * one provided?).
     *
     * @param apConfig {@link SoftApConfiguration} to use for softap mode
     * @param isPrivileged indicate the caller can pass some fields check or not
     * @param wifiNative to use native API to get iface combinations.
     * @return boolean true if the provided config meets the minimum set of details, false
     * otherwise.
     */
    static boolean validateApWifiConfiguration(@NonNull SoftApConfiguration apConfig,
            boolean isPrivileged, Context context, WifiNative wifiNative) {
        // first check the SSID
        WifiSsid ssid = apConfig.getWifiSsid();
        if (ssid == null || ssid.getBytes().length == 0) {
            Log.d(TAG, "SSID for softap configuration cannot be null or 0 length.");
            return false;
        }

        // BSSID can be set if caller own permission:android.Manifest.permission.NETWORK_SETTINGS.
        if (apConfig.getBssid() != null && !isPrivileged) {
            Log.e(TAG, "Config BSSID needs NETWORK_SETTINGS permission");
            return false;
        }

        String preSharedKey = apConfig.getPassphrase();
        boolean hasPreSharedKey = !TextUtils.isEmpty(preSharedKey);
        int authType;

        try {
            authType = apConfig.getSecurityType();
        } catch (IllegalStateException e) {
            Log.d(TAG, "Unable to get AuthType for softap config: " + e.getMessage());
            return false;
        }

        if (ApConfigUtil.isNonPasswordAP(authType)) {
            // open networks should not have a password
            if (hasPreSharedKey) {
                Log.d(TAG, "open softap network should not have a password");
                return false;
            }
        } else if (authType == SECURITY_TYPE_WPA2_PSK
                || authType == SECURITY_TYPE_WPA3_SAE_TRANSITION
                || authType == SECURITY_TYPE_WPA3_SAE) {
            // this is a config that should have a password - check that first
            if (!hasPreSharedKey) {
                Log.d(TAG, "softap network password must be set");
                return false;
            }

            if (context.getResources().getBoolean(
                    R.bool.config_wifiSoftapPassphraseAsciiEncodableCheck)) {
                final CharsetEncoder asciiEncoder = StandardCharsets.US_ASCII.newEncoder();
                if (!asciiEncoder.canEncode(preSharedKey)) {
                    Log.d(TAG, "passphrase not ASCII encodable");
                    return false;
                }
                if (!validateApConfigAsciiPreSharedKey(authType, preSharedKey)) {
                    // failed preSharedKey checks for WPA2 and WPA3 SAE (Transition) mode.
                    return false;
                }
            }
        } else {
            // this is not a supported security type
            Log.d(TAG, "softap configs must either be open or WPA2 PSK networks");
            return false;
        }

        if (!isBandsSupported(apConfig.getBands(), context)) {
            return false;
        }

        if (ApConfigUtil.isSecurityTypeRestrictedFor6gBand(authType)) {
            for (int band : apConfig.getBands()) {
                // Only return failure if requested band is limited to 6GHz only
                if (band == SoftApConfiguration.BAND_6GHZ
                        && !ApConfigUtil.canHALConvertRestrictedSecurityTypeFor6GHz(
                                context.getResources(), authType)) {
                    Log.d(TAG, "security type: " +  authType
                            + " is not allowed for softap in 6GHz band");
                    return false;
                }
            }
        }

        if (SdkLevel.isAtLeastT()
                && authType == SECURITY_TYPE_WPA3_OWE_TRANSITION) {
            if (!ApConfigUtil.isBridgedModeSupported(context, wifiNative)) {
                Log.d(TAG, "softap owe transition needs bridge mode support");
                return false;
            } else if (apConfig.getBands().length > 1) {
                Log.d(TAG, "softap owe transition must use single band");
                return false;
            }
        }

        return true;
    }

    private static String generatePassword() {
        // Characters that will be used for password generation. Some characters commonly known to
        // be confusing like 0 and O excluded from this list.
        final String allowed = "23456789abcdefghijkmnpqrstuvwxyz";
        final int passLength = 15;

        StringBuilder sb = new StringBuilder(passLength);
        SecureRandom random = new SecureRandom();
        for (int i = 0; i < passLength; i++) {
            sb.append(allowed.charAt(random.nextInt(allowed.length())));
        }
        return sb.toString();
    }

    /**
     * Generate default band base on supported band configuration.
     *
     * @param context The caller context used to get value from resource file.
     * @return A band which will be used for a default band in default configuration.
     */
    public static @BandType int generateDefaultBand(Context context) {
        for (int band : SoftApConfiguration.BAND_TYPES) {
            if (ApConfigUtil.isBandSupported(band, context)) {
                return band;
            }
        }
        Log.e(TAG, "Invalid overlay configuration! No any band supported on SoftAp");
        return SoftApConfiguration.BAND_2GHZ;
    }

    private static boolean isBandsSupported(@NonNull int[] apBands, Context context) {
        for (int band : apBands) {
            if (!ApConfigUtil.isBandSupported(band, context)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Enable force-soft-AP-channel mode which takes effect when soft AP starts next time
     *
     * @param forcedApBand The forced band.
     * @param forcedApChannel The forced IEEE channel number or 0 when forced AP band only.
     * @param forcedApMaximumChannelBandWidth The forced maximum channel bandwidth.
     */
    public synchronized void enableForceSoftApBandOrChannel(
            @BandType int forcedApBand, int forcedApChannel, int forcedApMaximumChannelBandWidth) {
        mForceApChannel = true;
        mForcedApChannel = forcedApChannel;
        mForcedApBand = forcedApBand;
        mForcedApMaximumChannelBandWidth = forcedApMaximumChannelBandWidth;
    }

    /**
     * Disable force-soft-AP-channel mode which take effect when soft AP starts next time
     */
    public synchronized void disableForceSoftApBandOrChannel() {
        mForceApChannel = false;
    }

    private SoftApConfiguration updatePersistentRandomizedMacAddress(SoftApConfiguration config) {
        // Update randomized MacAddress
        WifiSsid ssid = config.getWifiSsid();
        MacAddress randomizedMacAddress = mMacAddressUtil.calculatePersistentMacForSap(
                ssid != null ? ssid.toString() : null, Process.WIFI_UID);
        if (randomizedMacAddress != null) {
            return new SoftApConfiguration.Builder(config)
                    .setRandomizedMacAddress(randomizedMacAddress).build();
        }

        if (config.getPersistentRandomizedMacAddress() != null) {
            return config;
        }

        randomizedMacAddress = MacAddressUtils.createRandomUnicastAddress();
        return new SoftApConfiguration.Builder(config)
                .setRandomizedMacAddress(randomizedMacAddress).build();
    }

    /**
     * Returns the last configured Wi-Fi tethered AP passphrase.
     */
    public synchronized String getLastConfiguredTetheredApPassphraseSinceBoot() {
        return mLastConfiguredPassphrase;
    }
}
