content: /*
   * 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 android.annotation.NonNull;
  import android.app.Notification;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.net.wifi.WifiConfiguration;
  import android.net.wifi.WifiConfiguration.KeyMgmt;
  import android.os.Environment;
  import android.os.Handler;
  import android.os.Looper;
  import android.text.TextUtils;
  import android.util.Log;
  
  import com.android.internal.R;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
  import com.android.internal.notification.SystemNotificationChannels;
  
  import java.io.BufferedInputStream;
  import java.io.BufferedOutputStream;
  import java.io.DataInputStream;
  import java.io.DataOutputStream;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.Random;
  import java.util.UUID;
  
  /**
   * 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 String DEFAULT_AP_CONFIG_FILE =
              Environment.getDataDirectory() + "/misc/wifi/softap.conf";
  
      private static final int AP_CONFIG_FILE_VERSION = 3;
  
      private static final int RAND_SSID_INT_MIN = 1000;
      private static final int RAND_SSID_INT_MAX = 9999;
  
      @VisibleForTesting
      static final int SSID_MIN_LEN = 1;
      @VisibleForTesting
      static final int SSID_MAX_LEN = 32;
      @VisibleForTesting
      static final int PSK_MIN_LEN = 8;
      @VisibleForTesting
      static final int PSK_MAX_LEN = 63;
  
      @VisibleForTesting
      static final int AP_CHANNEL_DEFAULT = 0;
  
      private WifiConfiguration mWifiApConfig = null;
  
      private ArrayList<Integer> mAllowed2GChannel = null;
  
      private final Context mContext;
      private final Handler mHandler;
      private final String mApConfigFile;
      private final BackupManagerProxy mBackupManagerProxy;
      private final FrameworkFacade mFrameworkFacade;
      private boolean mRequiresApBandConversion = false;
  
      WifiApConfigStore(Context context, Looper looper,
              BackupManagerProxy backupManagerProxy, FrameworkFacade frameworkFacade) {
          this(context, looper, backupManagerProxy, frameworkFacade, DEFAULT_AP_CONFIG_FILE);
      }
  
      WifiApConfigStore(Context context,
                        Looper looper,
                        BackupManagerProxy backupManagerProxy,
                        FrameworkFacade frameworkFacade,
                        String apConfigFile) {
          mContext = context;
          mHandler = new Handler(looper);
          mBackupManagerProxy = backupManagerProxy;
          mFrameworkFacade = frameworkFacade;
          mApConfigFile = apConfigFile;
  
          String ap2GChannelListStr = mContext.getResources().getString(
                  R.string.config_wifi_framework_sap_2G_channel_list);
          Log.d(TAG, "2G band allowed channels are:" + ap2GChannelListStr);
  
          if (ap2GChannelListStr != null) {
              mAllowed2GChannel = new ArrayList<Integer>();
              String channelList[] = ap2GChannelListStr.split(",");
              for (String tmp : channelList) {
                  mAllowed2GChannel.add(Integer.parseInt(tmp));
              }
          }
  
          mRequiresApBandConversion = mContext.getResources().getBoolean(
                  R.bool.config_wifi_convert_apband_5ghz_to_any);
  
          /* Load AP configuration from persistent storage. */
          mWifiApConfig = loadApConfiguration(mApConfigFile);
          if (mWifiApConfig == null) {
              /* Use default configuration. */
              Log.d(TAG, "Fallback to use default AP configuration");
              mWifiApConfig = getDefaultApConfiguration();
  
              /* Save the default configuration to persistent storage. */
              writeApConfiguration(mApConfigFile, mWifiApConfig);
          }
  
          IntentFilter filter = new IntentFilter();
          filter.addAction(ACTION_HOTSPOT_CONFIG_USER_TAPPED_CONTENT);
          mContext.registerReceiver(
                  mBroadcastReceiver, filter, null /* broadcastPermission */, mHandler);
      }
  
      private final BroadcastReceiver mBroadcastReceiver =
              new BroadcastReceiver() {
                  @Override
                  public void onReceive(Context context, Intent intent) {
                      // For now we only have one registered listener, but we easily could expand this
                      // to support multiple signals.  Starting off with a switch to support trivial
                      // expansion.
                      switch(intent.getAction()) {
                          case ACTION_HOTSPOT_CONFIG_USER_TAPPED_CONTENT:
                              handleUserHotspotConfigTappedContent();
                              break;
                          default:
                              Log.e(TAG, "Unknown action " + intent.getAction());
                      }
                  }
              };
  
      /**
       * Return the current soft access point configuration.
       */
      public synchronized WifiConfiguration getApConfiguration() {
          WifiConfiguration config = apBandCheckConvert(mWifiApConfig);
          if (mWifiApConfig != config) {
              Log.d(TAG, "persisted config was converted, need to resave it");
              mWifiApConfig = config;
              persistConfigAndTriggerBackupManagerProxy(mWifiApConfig);
          }
          return mWifiApConfig;
      }
  
      /**
       * 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 ClientModeImpl thread (CMD_START_AP).
       */
      public synchronized void setApConfiguration(WifiConfiguration config) {
          if (config == null) {
              mWifiApConfig = getDefaultApConfiguration();
          } else {
              mWifiApConfig = apBandCheckConvert(config);
          }
          persistConfigAndTriggerBackupManagerProxy(mWifiApConfig);
      }
  
      public ArrayList<Integer> getAllowed2GChannel() {
          return mAllowed2GChannel;
      }
  
      /**
       * Helper method to create and send notification to user of apBand conversion.
       *
       * @param packageName name of the calling app
       */
      public void notifyUserOfApBandConversion(String packageName) {
          Log.w(TAG, "ready to post notification - triggered by " + packageName);
          Notification notification = createConversionNotification();
          NotificationManager notificationManager = (NotificationManager)
                      mContext.getSystemService(Context.NOTIFICATION_SERVICE);
          notificationManager.notify(SystemMessage.NOTE_SOFTAP_CONFIG_CHANGED, notification);
      }
  
      private Notification createConversionNotification() {
          CharSequence title =
                  mContext.getResources().getText(R.string.wifi_softap_config_change);
          CharSequence contentSummary =
                  mContext.getResources().getText(R.string.wifi_softap_config_change_summary);
          CharSequence content =
                  mContext.getResources().getText(R.string.wifi_softap_config_change_detailed);
          int color =
                  mContext.getResources().getColor(
                          R.color.system_notification_accent_color, mContext.getTheme());
  
          return new Notification.Builder(mContext, SystemNotificationChannels.NETWORK_STATUS)
                  .setSmallIcon(R.drawable.ic_wifi_settings)
                  .setPriority(Notification.PRIORITY_HIGH)
                  .setCategory(Notification.CATEGORY_SYSTEM)
                  .setContentTitle(title)
                  .setContentText(contentSummary)
                  .setContentIntent(getPrivateBroadcast(ACTION_HOTSPOT_CONFIG_USER_TAPPED_CONTENT))
                  .setTicker(title)
                  .setShowWhen(false)
                  .setLocalOnly(true)
                  .setColor(color)
                  .setStyle(new Notification.BigTextStyle().bigText(content)
                                                           .setBigContentTitle(title)
                                                           .setSummaryText(contentSummary))
                  .build();
      }
  
      private WifiConfiguration apBandCheckConvert(WifiConfiguration config) {
          if (mRequiresApBandConversion) {
              // some devices are unable to support 5GHz only operation, check for 5GHz and
              // move to ANY if apBand conversion is required.
              if (config.apBand == WifiConfiguration.AP_BAND_5GHZ) {
                  Log.w(TAG, "Supplied ap config band was 5GHz only, converting to ANY");
                  WifiConfiguration convertedConfig = new WifiConfiguration(config);
                  convertedConfig.apBand = WifiConfiguration.AP_BAND_ANY;
                  convertedConfig.apChannel = AP_CHANNEL_DEFAULT;
                  return convertedConfig;
              }
          } else {
              // this is a single mode device, we do not support ANY.  Convert all ANY to 5GHz
              if (config.apBand == WifiConfiguration.AP_BAND_ANY) {
                  Log.w(TAG, "Supplied ap config band was ANY, converting to 5GHz");
                  WifiConfiguration convertedConfig = new WifiConfiguration(config);
                  convertedConfig.apBand = WifiConfiguration.AP_BAND_5GHZ;
                  convertedConfig.apChannel = AP_CHANNEL_DEFAULT;
                  return convertedConfig;
              }
          }
          return config;
      }
  
      private void persistConfigAndTriggerBackupManagerProxy(WifiConfiguration config) {
          writeApConfiguration(mApConfigFile, mWifiApConfig);
          // Stage the backup of the SettingsProvider package which backs this up
          mBackupManagerProxy.notifyDataChanged();
      }
  
      /**
       * Load AP configuration from persistent storage.
       */
      private static WifiConfiguration loadApConfiguration(final String filename) {
          WifiConfiguration config = null;
          DataInputStream in = null;
          try {
              config = new WifiConfiguration();
              in = new DataInputStream(
                      new BufferedInputStream(new FileInputStream(filename)));
  
              int version = in.readInt();
              if (version < 1 || version > AP_CONFIG_FILE_VERSION) {
                  Log.e(TAG, "Bad version on hotspot configuration file");
                  return null;
              }
              config.SSID = in.readUTF();
  
              if (version >= 2) {
                  config.apBand = in.readInt();
                  config.apChannel = in.readInt();
              }
  
              if (version >= 3) {
                  config.hiddenSSID = in.readBoolean();
              }
  
              int authType = in.readInt();
              config.allowedKeyManagement.set(authType);
              if (authType != KeyMgmt.NONE) {
                  config.preSharedKey = in.readUTF();
              }
          } catch (IOException e) {
              Log.e(TAG, "Error reading hotspot configuration " + e);
              config = null;
          } finally {
              if (in != null) {
                  try {
                      in.close();
                  } catch (IOException e) {
                      Log.e(TAG, "Error closing hotspot configuration during read" + e);
                  }
              }
          }
          return config;
      }
  
      /**
       * Write AP configuration to persistent storage.
       */
      private static void writeApConfiguration(final String filename,
                                               final WifiConfiguration config) {
          try (DataOutputStream out = new DataOutputStream(new BufferedOutputStream(
                          new FileOutputStream(filename)))) {
              out.writeInt(AP_CONFIG_FILE_VERSION);
              out.writeUTF(config.SSID);
              out.writeInt(config.apBand);
              out.writeInt(config.apChannel);
              out.writeBoolean(config.hiddenSSID);
              int authType = config.getAuthType();
              out.writeInt(authType);
              if (authType != KeyMgmt.NONE) {
                  out.writeUTF(config.preSharedKey);
              }
          } catch (IOException e) {
              Log.e(TAG, "Error writing hotspot configuration" + e);
          }
      }
  
      /**
       * Generate a default 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 WifiConfiguration getDefaultApConfiguration() {
          WifiConfiguration config = new WifiConfiguration();
          config.apBand = WifiConfiguration.AP_BAND_2GHZ;
          config.SSID = mContext.getResources().getString(
                  R.string.wifi_tether_configure_ssid_default) + "_" + getRandomIntForDefaultSsid();
          config.allowedKeyManagement.set(KeyMgmt.WPA2_PSK);
          String randomUUID = UUID.randomUUID().toString();
          //first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
          config.preSharedKey = randomUUID.substring(0, 8) + randomUUID.substring(9, 13);
          return config;
      }
  
      private static int getRandomIntForDefaultSsid() {
          Random random = new Random();
          return random.nextInt((RAND_SSID_INT_MAX - RAND_SSID_INT_MIN) + 1) + RAND_SSID_INT_MIN;
      }
  
      /**
       * 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 static WifiConfiguration generateLocalOnlyHotspotConfig(Context context, int apBand) {
          WifiConfiguration config = new WifiConfiguration();
  
          config.SSID = context.getResources().getString(
                R.string.wifi_localhotspot_configure_ssid_default) + "_"
                        + getRandomIntForDefaultSsid();
          config.apBand = apBand;
          config.allowedKeyManagement.set(KeyMgmt.WPA2_PSK);
          config.networkId = WifiConfiguration.LOCAL_ONLY_NETWORK_ID;
          String randomUUID = UUID.randomUUID().toString();
          // first 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
          config.preSharedKey = randomUUID.substring(0, 8) + randomUUID.substring(9, 13);
          return config;
      }
  
      /**
       * Verify provided SSID for existence, length and conversion to bytes
       *
       * @param ssid String ssid name
       * @return boolean indicating ssid met requirements
       */
      private static boolean validateApConfigSsid(String ssid) {
          if (TextUtils.isEmpty(ssid)) {
              Log.d(TAG, "SSID for softap configuration must be set.");
              return false;
          }
  
          try {
              byte[] ssid_bytes = ssid.getBytes(StandardCharsets.UTF_8);
  
              if (ssid_bytes.length < SSID_MIN_LEN || ssid_bytes.length > SSID_MAX_LEN) {
                  Log.d(TAG, "softap SSID is defined as UTF-8 and it must be at least "
                          + SSID_MIN_LEN + " byte and not more than " + SSID_MAX_LEN + " bytes");
                  return false;
              }
          } catch (IllegalArgumentException e) {
              Log.e(TAG, "softap config SSID verification failed: malformed string " + ssid);
              return false;
          }
          return true;
      }
  
      /**
       * Verify provided preSharedKey in ap config for WPA2_PSK network meets requirements.
       */
      private static boolean validateApConfigPreSharedKey(String preSharedKey) {
          if (preSharedKey.length() < PSK_MIN_LEN || preSharedKey.length() > PSK_MAX_LEN) {
              Log.d(TAG, "softap network password string size must be at least " + PSK_MIN_LEN
                      + " and no more than " + PSK_MAX_LEN);
              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 WifiConfiguration is properly configured for use by SoftApManager.
       *
       * This method checks the length of the SSID and for sanity between security settings (if it
       * requires a password, was one provided?).
       *
       * @param apConfig {@link WifiConfiguration} to use for softap mode
       * @return boolean true if the provided config meets the minimum set of details, false
       * otherwise.
       */
      static boolean validateApWifiConfiguration(@NonNull WifiConfiguration apConfig) {
          // first check the SSID
          if (!validateApConfigSsid(apConfig.SSID)) {
              // failed SSID verificiation checks
              return false;
          }
  
          // now check security settings: settings app allows open and WPA2 PSK
          if (apConfig.allowedKeyManagement == null) {
              Log.d(TAG, "softap config key management bitset was null");
              return false;
          }
  
          String preSharedKey = apConfig.preSharedKey;
          boolean hasPreSharedKey = !TextUtils.isEmpty(preSharedKey);
          int authType;
  
          try {
              authType = apConfig.getAuthType();
          } catch (IllegalStateException e) {
              Log.d(TAG, "Unable to get AuthType for softap config: " + e.getMessage());
              return false;
          }
  
          if (authType == KeyMgmt.NONE) {
              // 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 == KeyMgmt.WPA2_PSK) {
              // 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 (!validateApConfigPreSharedKey(preSharedKey)) {
                  // failed preSharedKey checks
                  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;
          }
  
          return true;
      }
  
      /**
       * Helper method to start up settings on the softap config page.
       */
      private void startSoftApSettings() {
          mContext.startActivity(
                  new Intent("com.android.settings.WIFI_TETHER_SETTINGS")
                          .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
      }
  
      /**
       * Helper method to trigger settings to open the softap config page
       */
      private void handleUserHotspotConfigTappedContent() {
          startSoftApSettings();
          NotificationManager notificationManager = (NotificationManager)
                  mContext.getSystemService(Context.NOTIFICATION_SERVICE);
          notificationManager.cancel(SystemMessage.NOTE_SOFTAP_CONFIG_CHANGED);
      }
  
      private PendingIntent getPrivateBroadcast(String action) {
          Intent intent = new Intent(action).setPackage("android");
          return mFrameworkFacade.getBroadcast(
                  mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
      }
  }
  