blob: 69c787f481d6cda136efd83a4972a66e7ae5b3a4 [file] [log] [blame]
/*
* Copyright (C) 2017 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.content.Context;
import android.hardware.wifi.hostapd.V1_0.HostapdStatus;
import android.hardware.wifi.hostapd.V1_0.HostapdStatusCode;
import android.hardware.wifi.hostapd.V1_0.IHostapd;
import android.hidl.manager.V1_0.IServiceManager;
import android.hidl.manager.V1_0.IServiceNotification;
import android.net.wifi.WifiConfiguration;
import android.os.Handler;
import android.os.HwRemoteBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wifi.WifiNative.HostapdDeathEventHandler;
import com.android.server.wifi.util.NativeUtil;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.NoSuchElementException;
import javax.annotation.concurrent.ThreadSafe;
/**
* To maintain thread-safety, the locking protocol is that every non-static method (regardless of
* access level) acquires mLock.
*/
@ThreadSafe
public class HostapdHal {
private static final String TAG = "HostapdHal";
@VisibleForTesting
public static final String HAL_INSTANCE_NAME = "default";
private final Object mLock = new Object();
private boolean mVerboseLoggingEnabled = false;
private final Handler mEventHandler;
private final boolean mEnableAcs;
private final boolean mEnableIeee80211AC;
private final List<android.hardware.wifi.hostapd.V1_1.IHostapd.AcsChannelRange>
mAcsChannelRanges;
// Hostapd HAL interface objects
private IServiceManager mIServiceManager = null;
private IHostapd mIHostapd;
private HashMap<String, WifiNative.SoftApListener> mSoftApListeners = new HashMap<>();
private HostapdDeathEventHandler mDeathEventHandler;
private ServiceManagerDeathRecipient mServiceManagerDeathRecipient;
private HostapdDeathRecipient mHostapdDeathRecipient;
// Death recipient cookie registered for current supplicant instance.
private long mDeathRecipientCookie = 0;
private final IServiceNotification mServiceNotificationCallback =
new IServiceNotification.Stub() {
public void onRegistration(String fqName, String name, boolean preexisting) {
synchronized (mLock) {
if (mVerboseLoggingEnabled) {
Log.i(TAG, "IServiceNotification.onRegistration for: " + fqName
+ ", " + name + " preexisting=" + preexisting);
}
if (!initHostapdService()) {
Log.e(TAG, "initalizing IHostapd failed.");
hostapdServiceDiedHandler(mDeathRecipientCookie);
} else {
Log.i(TAG, "Completed initialization of IHostapd.");
}
}
}
};
private class ServiceManagerDeathRecipient implements HwRemoteBinder.DeathRecipient {
@Override
public void serviceDied(long cookie) {
mEventHandler.post(() -> {
synchronized (mLock) {
Log.w(TAG, "IServiceManager died: cookie=" + cookie);
hostapdServiceDiedHandler(mDeathRecipientCookie);
mIServiceManager = null; // Will need to register a new ServiceNotification
}
});
}
}
private class HostapdDeathRecipient implements HwRemoteBinder.DeathRecipient {
@Override
public void serviceDied(long cookie) {
mEventHandler.post(() -> {
synchronized (mLock) {
Log.w(TAG, "IHostapd/IHostapd died: cookie=" + cookie);
hostapdServiceDiedHandler(cookie);
}
});
}
}
public HostapdHal(Context context, Looper looper) {
mEventHandler = new Handler(looper);
mEnableAcs = context.getResources().getBoolean(R.bool.config_wifi_softap_acs_supported);
mEnableIeee80211AC =
context.getResources().getBoolean(R.bool.config_wifi_softap_ieee80211ac_supported);
mAcsChannelRanges = toAcsChannelRanges(context.getResources().getString(
R.string.config_wifi_softap_acs_supported_channel_list));
mServiceManagerDeathRecipient = new ServiceManagerDeathRecipient();
mHostapdDeathRecipient = new HostapdDeathRecipient();
}
/**
* Enable/Disable verbose logging.
*
* @param enable true to enable, false to disable.
*/
void enableVerboseLogging(boolean enable) {
synchronized (mLock) {
mVerboseLoggingEnabled = enable;
}
}
/**
* Uses the IServiceManager to check if the device is running V1_1 of the HAL from the VINTF for
* the device.
* @return true if supported, false otherwise.
*/
private boolean isV1_1() {
synchronized (mLock) {
if (mIServiceManager == null) {
Log.e(TAG, "isV1_1: called but mServiceManager is null!?");
return false;
}
try {
return (mIServiceManager.getTransport(
android.hardware.wifi.hostapd.V1_1.IHostapd.kInterfaceName,
HAL_INSTANCE_NAME)
!= IServiceManager.Transport.EMPTY);
} catch (RemoteException e) {
Log.e(TAG, "Exception while operating on IServiceManager: " + e);
handleRemoteException(e, "getTransport");
return false;
}
}
}
/**
* Link to death for IServiceManager object.
* @return true on success, false otherwise.
*/
private boolean linkToServiceManagerDeath() {
synchronized (mLock) {
if (mIServiceManager == null) return false;
try {
if (!mIServiceManager.linkToDeath(mServiceManagerDeathRecipient, 0)) {
Log.wtf(TAG, "Error on linkToDeath on IServiceManager");
hostapdServiceDiedHandler(mDeathRecipientCookie);
mIServiceManager = null; // Will need to register a new ServiceNotification
return false;
}
} catch (RemoteException e) {
Log.e(TAG, "IServiceManager.linkToDeath exception", e);
mIServiceManager = null; // Will need to register a new ServiceNotification
return false;
}
return true;
}
}
/**
* Registers a service notification for the IHostapd service, which triggers intialization of
* the IHostapd
* @return true if the service notification was successfully registered
*/
public boolean initialize() {
synchronized (mLock) {
if (mVerboseLoggingEnabled) {
Log.i(TAG, "Registering IHostapd service ready callback.");
}
mIHostapd = null;
if (mIServiceManager != null) {
// Already have an IServiceManager and serviceNotification registered, don't
// don't register another.
return true;
}
try {
mIServiceManager = getServiceManagerMockable();
if (mIServiceManager == null) {
Log.e(TAG, "Failed to get HIDL Service Manager");
return false;
}
if (!linkToServiceManagerDeath()) {
return false;
}
/* TODO(b/33639391) : Use the new IHostapd.registerForNotifications() once it
exists */
if (!mIServiceManager.registerForNotifications(
IHostapd.kInterfaceName, "", mServiceNotificationCallback)) {
Log.e(TAG, "Failed to register for notifications to "
+ IHostapd.kInterfaceName);
mIServiceManager = null; // Will need to register a new ServiceNotification
return false;
}
} catch (RemoteException e) {
Log.e(TAG, "Exception while trying to register a listener for IHostapd service: "
+ e);
hostapdServiceDiedHandler(mDeathRecipientCookie);
mIServiceManager = null; // Will need to register a new ServiceNotification
return false;
}
return true;
}
}
/**
* Link to death for IHostapd object.
* @return true on success, false otherwise.
*/
private boolean linkToHostapdDeath() {
synchronized (mLock) {
if (mIHostapd == null) return false;
try {
if (!mIHostapd.linkToDeath(mHostapdDeathRecipient, ++mDeathRecipientCookie)) {
Log.wtf(TAG, "Error on linkToDeath on IHostapd");
hostapdServiceDiedHandler(mDeathRecipientCookie);
return false;
}
} catch (RemoteException e) {
Log.e(TAG, "IHostapd.linkToDeath exception", e);
return false;
}
return true;
}
}
private boolean registerCallback(
android.hardware.wifi.hostapd.V1_1.IHostapdCallback callback) {
synchronized (mLock) {
String methodStr = "registerCallback_1_1";
try {
android.hardware.wifi.hostapd.V1_1.IHostapd iHostapdV1_1 = getHostapdMockableV1_1();
if (iHostapdV1_1 == null) return false;
HostapdStatus status = iHostapdV1_1.registerCallback(callback);
return checkStatusAndLogFailure(status, methodStr);
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
return false;
}
}
}
/**
* Initialize the IHostapd object.
* @return true on success, false otherwise.
*/
private boolean initHostapdService() {
synchronized (mLock) {
try {
mIHostapd = getHostapdMockable();
} catch (RemoteException e) {
Log.e(TAG, "IHostapd.getService exception: " + e);
return false;
} catch (NoSuchElementException e) {
Log.e(TAG, "IHostapd.getService exception: " + e);
return false;
}
if (mIHostapd == null) {
Log.e(TAG, "Got null IHostapd service. Stopping hostapd HIDL startup");
return false;
}
if (!linkToHostapdDeath()) {
mIHostapd = null;
return false;
}
// Register for callbacks for 1.1 hostapd.
if (isV1_1() && !registerCallback(new HostapdCallback())) {
mIHostapd = null;
return false;
}
}
return true;
}
/**
* Add and start a new access point.
*
* @param ifaceName Name of the interface.
* @param config Configuration to use for the AP.
* @param listener Callback for AP events.
* @return true on success, false otherwise.
*/
public boolean addAccessPoint(@NonNull String ifaceName, @NonNull WifiConfiguration config,
@NonNull WifiNative.SoftApListener listener) {
synchronized (mLock) {
final String methodStr = "addAccessPoint";
IHostapd.IfaceParams ifaceParams = new IHostapd.IfaceParams();
ifaceParams.ifaceName = ifaceName;
ifaceParams.hwModeParams.enable80211N = true;
ifaceParams.hwModeParams.enable80211AC = mEnableIeee80211AC;
try {
ifaceParams.channelParams.band = getBand(config);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Unrecognized apBand " + config.apBand);
return false;
}
if (mEnableAcs) {
ifaceParams.channelParams.enableAcs = true;
ifaceParams.channelParams.acsShouldExcludeDfs = true;
} else {
// Downgrade IHostapd.Band.BAND_ANY to IHostapd.Band.BAND_2_4_GHZ if ACS
// is not supported.
// We should remove this workaround once channel selection is moved from
// ApConfigUtil to here.
if (ifaceParams.channelParams.band == IHostapd.Band.BAND_ANY) {
Log.d(TAG, "ACS is not supported on this device, using 2.4 GHz band.");
ifaceParams.channelParams.band = IHostapd.Band.BAND_2_4_GHZ;
}
ifaceParams.channelParams.enableAcs = false;
ifaceParams.channelParams.channel = config.apChannel;
}
IHostapd.NetworkParams nwParams = new IHostapd.NetworkParams();
// TODO(b/67745880) Note that config.SSID is intended to be either a
// hex string or "double quoted".
// However, it seems that whatever is handing us these configurations does not obey
// this convention.
nwParams.ssid.addAll(NativeUtil.stringToByteArrayList(config.SSID));
nwParams.isHidden = config.hiddenSSID;
nwParams.encryptionType = getEncryptionType(config);
nwParams.pskPassphrase = (config.preSharedKey != null) ? config.preSharedKey : "";
if (!checkHostapdAndLogFailure(methodStr)) return false;
try {
HostapdStatus status;
if (isV1_1()) {
android.hardware.wifi.hostapd.V1_1.IHostapd.IfaceParams ifaceParams1_1 =
new android.hardware.wifi.hostapd.V1_1.IHostapd.IfaceParams();
ifaceParams1_1.V1_0 = ifaceParams;
if (mEnableAcs) {
ifaceParams1_1.channelParams.acsChannelRanges.addAll(mAcsChannelRanges);
}
android.hardware.wifi.hostapd.V1_1.IHostapd iHostapdV1_1 =
getHostapdMockableV1_1();
if (iHostapdV1_1 == null) return false;
status = iHostapdV1_1.addAccessPoint_1_1(ifaceParams1_1, nwParams);
} else {
status = mIHostapd.addAccessPoint(ifaceParams, nwParams);
}
if (!checkStatusAndLogFailure(status, methodStr)) {
return false;
}
mSoftApListeners.put(ifaceName, listener);
return true;
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
return false;
}
}
}
/**
* Remove a previously started access point.
*
* @param ifaceName Name of the interface.
* @return true on success, false otherwise.
*/
public boolean removeAccessPoint(@NonNull String ifaceName) {
synchronized (mLock) {
final String methodStr = "removeAccessPoint";
if (!checkHostapdAndLogFailure(methodStr)) return false;
try {
HostapdStatus status = mIHostapd.removeAccessPoint(ifaceName);
if (!checkStatusAndLogFailure(status, methodStr)) {
return false;
}
mSoftApListeners.remove(ifaceName);
return true;
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
return false;
}
}
}
/**
* Registers a death notification for hostapd.
* @return Returns true on success.
*/
public boolean registerDeathHandler(@NonNull HostapdDeathEventHandler handler) {
if (mDeathEventHandler != null) {
Log.e(TAG, "Death handler already present");
}
mDeathEventHandler = handler;
return true;
}
/**
* Deregisters a death notification for hostapd.
* @return Returns true on success.
*/
public boolean deregisterDeathHandler() {
if (mDeathEventHandler == null) {
Log.e(TAG, "No Death handler present");
}
mDeathEventHandler = null;
return true;
}
/**
* Clear internal state.
*/
private void clearState() {
synchronized (mLock) {
mIHostapd = null;
}
}
/**
* Handle hostapd death.
*/
private void hostapdServiceDiedHandler(long cookie) {
synchronized (mLock) {
if (mDeathRecipientCookie != cookie) {
Log.i(TAG, "Ignoring stale death recipient notification");
return;
}
clearState();
if (mDeathEventHandler != null) {
mDeathEventHandler.onDeath();
}
}
}
/**
* Signals whether Initialization completed successfully.
*/
public boolean isInitializationStarted() {
synchronized (mLock) {
return mIServiceManager != null;
}
}
/**
* Signals whether Initialization completed successfully.
*/
public boolean isInitializationComplete() {
synchronized (mLock) {
return mIHostapd != null;
}
}
/**
* Start the hostapd daemon.
*
* @return true on success, false otherwise.
*/
public boolean startDaemon() {
synchronized (mLock) {
try {
// This should startup hostapd daemon using the lazy start HAL mechanism.
getHostapdMockable();
} catch (RemoteException e) {
Log.e(TAG, "Exception while trying to start hostapd: "
+ e);
hostapdServiceDiedHandler(mDeathRecipientCookie);
return false;
} catch (NoSuchElementException e) {
// We're starting the daemon, so expect |NoSuchElementException|.
Log.d(TAG, "Successfully triggered start of hostapd using HIDL");
}
return true;
}
}
/**
* Terminate the hostapd daemon.
*/
public void terminate() {
synchronized (mLock) {
final String methodStr = "terminate";
if (!checkHostapdAndLogFailure(methodStr)) return;
try {
mIHostapd.terminate();
} catch (RemoteException e) {
handleRemoteException(e, methodStr);
}
}
}
/**
* Wrapper functions to access static HAL methods, created to be mockable in unit tests
*/
@VisibleForTesting
protected IServiceManager getServiceManagerMockable() throws RemoteException {
synchronized (mLock) {
return IServiceManager.getService();
}
}
@VisibleForTesting
protected IHostapd getHostapdMockable() throws RemoteException {
synchronized (mLock) {
return IHostapd.getService();
}
}
@VisibleForTesting
protected android.hardware.wifi.hostapd.V1_1.IHostapd getHostapdMockableV1_1()
throws RemoteException {
synchronized (mLock) {
try {
return android.hardware.wifi.hostapd.V1_1.IHostapd.castFrom(mIHostapd);
} catch (NoSuchElementException e) {
Log.e(TAG, "Failed to get IHostapd", e);
return null;
}
}
}
private static int getEncryptionType(WifiConfiguration localConfig) {
int encryptionType;
switch (localConfig.getAuthType()) {
case WifiConfiguration.KeyMgmt.NONE:
encryptionType = IHostapd.EncryptionType.NONE;
break;
case WifiConfiguration.KeyMgmt.WPA_PSK:
encryptionType = IHostapd.EncryptionType.WPA;
break;
case WifiConfiguration.KeyMgmt.WPA2_PSK:
encryptionType = IHostapd.EncryptionType.WPA2;
break;
default:
// We really shouldn't default to None, but this was how NetworkManagementService
// used to do this.
encryptionType = IHostapd.EncryptionType.NONE;
break;
}
return encryptionType;
}
private static int getBand(WifiConfiguration localConfig) {
int bandType;
switch (localConfig.apBand) {
case WifiConfiguration.AP_BAND_2GHZ:
bandType = IHostapd.Band.BAND_2_4_GHZ;
break;
case WifiConfiguration.AP_BAND_5GHZ:
bandType = IHostapd.Band.BAND_5_GHZ;
break;
case WifiConfiguration.AP_BAND_ANY:
bandType = IHostapd.Band.BAND_ANY;
break;
default:
throw new IllegalArgumentException();
}
return bandType;
}
/**
* Convert channel list string like '1-6,11' to list of AcsChannelRanges
*/
private List<android.hardware.wifi.hostapd.V1_1.IHostapd.AcsChannelRange>
toAcsChannelRanges(String channelListStr) {
ArrayList<android.hardware.wifi.hostapd.V1_1.IHostapd.AcsChannelRange> acsChannelRanges =
new ArrayList<>();
String[] channelRanges = channelListStr.split(",");
for (String channelRange : channelRanges) {
android.hardware.wifi.hostapd.V1_1.IHostapd.AcsChannelRange acsChannelRange =
new android.hardware.wifi.hostapd.V1_1.IHostapd.AcsChannelRange();
try {
if (channelRange.contains("-")) {
String[] channels = channelRange.split("-");
if (channels.length != 2) {
Log.e(TAG, "Unrecognized channel range, length is " + channels.length);
continue;
}
int start = Integer.parseInt(channels[0]);
int end = Integer.parseInt(channels[1]);
if (start > end) {
Log.e(TAG, "Invalid channel range, from " + start + " to " + end);
continue;
}
acsChannelRange.start = start;
acsChannelRange.end = end;
} else {
acsChannelRange.start = Integer.parseInt(channelRange);
acsChannelRange.end = acsChannelRange.start;
}
} catch (NumberFormatException e) {
// Ignore malformed value
Log.e(TAG, "Malformed channel value detected: " + e);
continue;
}
acsChannelRanges.add(acsChannelRange);
}
return acsChannelRanges;
}
/**
* Returns false if Hostapd is null, and logs failure to call methodStr
*/
private boolean checkHostapdAndLogFailure(String methodStr) {
synchronized (mLock) {
if (mIHostapd == null) {
Log.e(TAG, "Can't call " + methodStr + ", IHostapd is null");
return false;
}
return true;
}
}
/**
* Returns true if provided status code is SUCCESS, logs debug message and returns false
* otherwise
*/
private boolean checkStatusAndLogFailure(HostapdStatus status,
String methodStr) {
synchronized (mLock) {
if (status.code != HostapdStatusCode.SUCCESS) {
Log.e(TAG, "IHostapd." + methodStr + " failed: " + status.code
+ ", " + status.debugMessage);
return false;
} else {
if (mVerboseLoggingEnabled) {
Log.d(TAG, "IHostapd." + methodStr + " succeeded");
}
return true;
}
}
}
private void handleRemoteException(RemoteException e, String methodStr) {
synchronized (mLock) {
hostapdServiceDiedHandler(mDeathRecipientCookie);
Log.e(TAG, "IHostapd." + methodStr + " failed with exception", e);
}
}
private class HostapdCallback extends
android.hardware.wifi.hostapd.V1_1.IHostapdCallback.Stub {
@Override
public void onFailure(String ifaceName) {
Log.w(TAG, "Failure on iface " + ifaceName);
WifiNative.SoftApListener listener = mSoftApListeners.get(ifaceName);
if (listener != null) {
listener.onFailure();
}
}
}
}