/*
|
* 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);
|
}
|
}
|