diff options
13 files changed, 735 insertions, 102 deletions
diff --git a/service/Android.mk b/service/Android.mk index 21d776855..cf5cd8d1d 100644 --- a/service/Android.mk +++ b/service/Android.mk @@ -70,7 +70,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ android.hardware.wifi.hostapd-V1.0-java \ android.hardware.wifi.supplicant-V1.0-java \ android.hardware.wifi.supplicant-V1.1-java \ - ksoap2 + ksoap2 \ + libnanohttpd LOCAL_REQUIRED_MODULES := \ services \ diff --git a/service/java/com/android/server/wifi/hotspot2/OsuNetworkConnection.java b/service/java/com/android/server/wifi/hotspot2/OsuNetworkConnection.java index ecfcc5a51..73dcca505 100644 --- a/service/java/com/android/server/wifi/hotspot2/OsuNetworkConnection.java +++ b/service/java/com/android/server/wifi/hotspot2/OsuNetworkConnection.java @@ -69,7 +69,7 @@ public class OsuNetworkConnection { void onDisconnected(); /** - * Invoked when a timer tracking connection request is not reset by successfull connection. + * Invoked when a timer tracking connection request is not reset by successful connection. */ void onTimeOut(); @@ -84,10 +84,6 @@ public class OsuNetworkConnection { void onWifiDisabled(); } - /** - * Create an instance of {@link NetworkConnection} for the specified Wi-Fi network. - * @param context The application context - */ public OsuNetworkConnection(Context context) { mContext = context; } @@ -146,6 +142,7 @@ public class OsuNetworkConnection { /** * Register for network and Wifi state events + * * @param callbacks The callbacks to be invoked on network change events */ public void setEventCallback(Callbacks callbacks) { @@ -206,6 +203,7 @@ public class OsuNetworkConnection { /** * Method to update logging level in this class + * * @param verbose more than 0 enables verbose logging */ public void enableVerboseLogging(int verbose) { diff --git a/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java b/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java index ca05cd017..fd993e599 100644 --- a/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java +++ b/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java @@ -18,10 +18,14 @@ package com.android.server.wifi.hotspot2; import android.annotation.NonNull; import android.net.Network; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; import android.text.TextUtils; import android.util.Log; import android.util.Pair; +import com.android.internal.annotations.VisibleForTesting; import com.android.org.conscrypt.TrustManagerImpl; import com.android.server.wifi.hotspot2.soap.HttpsServiceConnection; import com.android.server.wifi.hotspot2.soap.HttpsTransport; @@ -65,12 +69,21 @@ public class OsuServerConnection { private HttpsTransport mHttpsTransport; private HttpsServiceConnection mServiceConnection = null; private HttpsURLConnection mUrlConnection = null; + private HandlerThread mOsuServerHandlerThread; + private Handler mHandler; private PasspointProvisioner.OsuServerCallbacks mOsuServerCallbacks; private boolean mSetupComplete = false; private boolean mVerboseLoggingEnabled = false; + private Looper mLooper; + + @VisibleForTesting + /* package */ OsuServerConnection(Looper looper) { + mLooper = looper; + } /** * Sets up callback for event + * * @param callbacks OsuServerCallbacks to be invoked for server related events */ public void setEventCallback(PasspointProvisioner.OsuServerCallbacks callbacks) { @@ -79,6 +92,7 @@ public class OsuServerConnection { /** * Initialize socket factory for server connection using HTTPS + * * @param tlsContext SSLContext that will be used for HTTPS connection * @param trustManagerImpl TrustManagerImpl delegate to validate certs */ @@ -96,10 +110,19 @@ public class OsuServerConnection { return; } mSetupComplete = true; + + // If mLooper is already set by unit test, don't overwrite it. + if (mLooper == null) { + mOsuServerHandlerThread = new HandlerThread("OsuServerHandler"); + mOsuServerHandlerThread.start(); + mLooper = mOsuServerHandlerThread.getLooper(); + } + mHandler = new Handler(mLooper); } /** * Provides the capability to run OSU server validation + * * @return boolean true if capability available */ public boolean canValidateServer() { @@ -108,6 +131,7 @@ public class OsuServerConnection { /** * Enables verbose logging + * * @param verbose a value greater than zero enables verbose logging */ public void enableVerboseLogging(int verbose) { @@ -116,11 +140,12 @@ public class OsuServerConnection { /** * Connect to the OSU server + * * @param url Osu Server's URL * @param network current network connection * @return boolean value, true if connection was successful * - * Relies on the caller to ensure that the capability to validate the OSU + * Note: Relies on the caller to ensure that the capability to validate the OSU * Server is available. */ public boolean connect(URL url, Network network) { @@ -180,24 +205,30 @@ public class OsuServerConnection { * The helper method to exchange a SOAP message. * * @param soapEnvelope the soap message to be sent. - * @return {@link SppResponseMessage} parsed, {@code null} in any failure + * @return {@code true} if {@link Network} is valid and {@code soapEnvelope} is not null, + * {@code false} otherwise. */ - public SppResponseMessage exchangeSoapMessage(@NonNull SoapSerializationEnvelope soapEnvelope) { + public boolean exchangeSoapMessage(@NonNull SoapSerializationEnvelope soapEnvelope) { if (mNetwork == null) { Log.e(TAG, "Network is not established"); - return null; + return false; + } + + if (mUrlConnection == null) { + Log.e(TAG, "Server certificate is not validated"); + return false; } if (soapEnvelope == null) { Log.e(TAG, "soapEnvelope is null"); - return null; + return false; } - if (mUrlConnection == null) { - Log.e(TAG, "Server certificate is not validated"); - return null; - } + mHandler.post(() -> performSoapMessageExchange(soapEnvelope)); + return true; + } + private void performSoapMessageExchange(@NonNull SoapSerializationEnvelope soapEnvelope) { if (mServiceConnection != null) { mServiceConnection.disconnect(); } @@ -205,21 +236,32 @@ public class OsuServerConnection { mServiceConnection = getServiceConnection(); if (mServiceConnection == null) { Log.e(TAG, "ServiceConnection for https is null"); - return null; + if (mOsuServerCallbacks != null) { + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), null); + return; + } } - SppResponseMessage sppResponse; + SppResponseMessage sppResponse = null; try { // Sending the SOAP message mHttpsTransport.call("", soapEnvelope); Object response = soapEnvelope.bodyIn; if (response == null) { Log.e(TAG, "SoapObject is null"); - return null; + if (mOsuServerCallbacks != null) { + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), + null); + return; + } } if (!(response instanceof SoapObject)) { Log.e(TAG, "Not a SoapObject instance"); - return null; + if (mOsuServerCallbacks != null) { + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), + null); + return; + } } SoapObject soapResponse = (SoapObject) response; if (mVerboseLoggingEnabled) { @@ -239,12 +281,19 @@ public class OsuServerConnection { } else { Log.e(TAG, "Failed to exchange the SOAP message"); } - return null; + if (mOsuServerCallbacks != null) { + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), null); + return; + } } finally { mServiceConnection.disconnect(); mServiceConnection = null; } - return sppResponse; + + if (mOsuServerCallbacks != null) { + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), + sppResponse); + } } /** @@ -331,6 +380,7 @@ public class OsuServerConnection { /** * Returns the OSU certificate matching the FQDN of the OSU server + * * @return {@link X509Certificate} OSU certificate matching FQDN of OSU server */ public X509Certificate getProviderCert() { diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java b/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java index 953107cf7..f743882e3 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java @@ -130,7 +130,7 @@ public class PasspointObjectFactory{ * @return {@link OsuServerConnection} */ public OsuServerConnection makeOsuServerConnection() { - return new OsuServerConnection(); + return new OsuServerConnection(null); } diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java b/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java index 0c8a6a72e..d47f5eece 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java @@ -16,14 +16,18 @@ package com.android.server.wifi.hotspot2; +import android.annotation.Nullable; import android.content.Context; +import android.content.Intent; import android.net.Network; +import android.net.wifi.WifiManager; import android.net.wifi.hotspot2.IProvisioningCallback; import android.net.wifi.hotspot2.OsuProvider; import android.net.wifi.hotspot2.ProvisioningCallback; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; +import android.os.UserHandle; import android.util.Log; import com.android.server.wifi.WifiNative; @@ -51,6 +55,7 @@ public class PasspointProvisioner { // TLS version to be used for HTTPS connection with OSU server private static final String TLS_VERSION = "TLSv1"; + private static final String OSU_APP_PACKAGE = "com.android.hotspot2"; private final Context mContext; private final ProvisioningStateMachine mProvisioningStateMachine; @@ -86,7 +91,7 @@ public class PasspointProvisioner { mOsuNetworkConnection.init(mProvisioningStateMachine.getHandler()); // Offload the heavy load job to another thread mProvisioningStateMachine.getHandler().post(() -> { - mRedirectListener = RedirectListener.createInstance(); + mRedirectListener = RedirectListener.createInstance(looper); mWfaKeyStore.load(); mOsuServerConnection.init(mObjectFactory.getSSLContext(TLS_VERSION), mObjectFactory.getTrustManagerImpl(mWfaKeyStore.get())); @@ -140,6 +145,7 @@ public class PasspointProvisioner { static final int STATE_OSU_AP_CONNECTED = 3; static final int STATE_OSU_SERVER_CONNECTED = 4; static final int STATE_WAITING_FOR_FIRST_SOAP_RESPONSE = 5; + static final int STATE_WAITING_FOR_REDIRECT_RESPONSE = 6; private OsuProvider mOsuProvider; private IProvisioningCallback mProvisioningCallback; @@ -159,6 +165,7 @@ public class PasspointProvisioner { /** * Returns the handler on which a runnable can be posted + * * @return Handler State Machine's handler */ public Handler getHandler() { @@ -167,8 +174,10 @@ public class PasspointProvisioner { /** * Start Provisioning with the Osuprovider and invoke callbacks + * * @param provider OsuProvider to provision with * @param callback IProvisioningCallback to invoke callbacks on + * Note: Called on main thread (WifiService thread). */ public void startProvisioning(OsuProvider provider, IProvisioningCallback callback) { if (mVerboseLoggingEnabled) { @@ -198,6 +207,7 @@ public class PasspointProvisioner { mServerUrl = serverUrl; mProvisioningCallback = callback; mOsuProvider = provider; + // Register for network and wifi state events during provisioning flow mOsuNetworkConnection.setEventCallback(mOsuNetworkCallbacks); @@ -216,6 +226,8 @@ public class PasspointProvisioner { /** * Handle Wifi Disable event + * + * Note: Called on main thread (WifiService thread). */ public void handleWifiDisabled() { if (mVerboseLoggingEnabled) { @@ -230,6 +242,8 @@ public class PasspointProvisioner { /** * Handle server validation failure + * + * Note: Called on main thread (WifiService thread). */ public void handleServerValidationFailure(int sessionId) { if (mVerboseLoggingEnabled) { @@ -249,6 +263,8 @@ public class PasspointProvisioner { /** * Handle status of server validation success + * + * Note: Called on main thread (WifiService thread). */ public void handleServerValidationSuccess(int sessionId) { if (mVerboseLoggingEnabled) { @@ -284,12 +300,6 @@ public class PasspointProvisioner { } invokeProvisioningCallback(PROVISIONING_STATUS, ProvisioningCallback.OSU_STATUS_SERVICE_PROVIDER_VERIFIED); - - invokeProvisioningCallback(PROVISIONING_STATUS, - ProvisioningCallback.OSU_STATUS_INIT_SOAP_EXCHANGE); - - // Move to initiate soap exchange - changeState(STATE_WAITING_FOR_FIRST_SOAP_RESPONSE); mProvisioningStateMachine.getHandler().post(() -> initSoapExchange()); } @@ -301,71 +311,207 @@ public class PasspointProvisioner { Log.v(TAG, "Initiates soap message exchange in state =" + mState); } - if (mState != STATE_WAITING_FOR_FIRST_SOAP_RESPONSE) { + if (mState != STATE_OSU_SERVER_CONNECTED) { Log.e(TAG, "Initiates soap message exchange in wrong state=" + mState); resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } // Redirect uri used for signal of completion for registration process. - final URL redirectUri = mRedirectListener.getURL(); - if (redirectUri == null) { - Log.e(TAG, "redirectUri is not valid"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); - return; - } + final URL redirectUri = mRedirectListener.getServerUrl(); // Sending the first sppPostDevDataRequest message. - SppResponseMessage sppResponse = mOsuServerConnection.exchangeSoapMessage( + if (mOsuServerConnection.exchangeSoapMessage( PostDevDataMessage.serializeToSoapEnvelope(mContext, mSystemInfo, redirectUri.toString(), - SppConstants.SppReason.SUBSCRIPTION_REGISTRATION, - null)); - if (sppResponse == null) { - Log.e(TAG, "failed to send the sppPostDevData message"); + SppConstants.SppReason.SUBSCRIPTION_REGISTRATION, null))) { + invokeProvisioningCallback(PROVISIONING_STATUS, + ProvisioningCallback.OSU_STATUS_INIT_SOAP_EXCHANGE); + // Move to initiate soap exchange + changeState(STATE_WAITING_FOR_FIRST_SOAP_RESPONSE); + } else { + Log.e(TAG, "HttpsConnection is not established for soap message exchange"); resetStateMachine(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); return; } + } + + private void launchOsuWebView() { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "launch Osu webview in state =" + mState); + } - if (sppResponse.getMessageType() - != SppResponseMessage.MessageType.POST_DEV_DATA_RESPONSE) { - Log.e(TAG, "Expected a PostDevDataResponse, but got " - + sppResponse.getMessageType()); - resetStateMachine( - ProvisioningCallback.OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_TYPE); + if (mState != STATE_WAITING_FOR_FIRST_SOAP_RESPONSE) { + Log.e(TAG, "launch Osu webview in wrong state =" + mState); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } - PostDevDataResponse devDataResponse = (PostDevDataResponse) sppResponse; - mSessionId = devDataResponse.getSessionID(); - if (devDataResponse.getSppCommand().getExecCommandId() - != SppCommand.ExecCommandId.BROWSER) { - Log.e(TAG, "Expected a launchBrowser command, but got " - + devDataResponse.getSppCommand().getExecCommandId()); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_UNEXPECTED_COMMAND_TYPE); + // Start the redirect server to listen the HTTP redirect response from server + // as completion of user input. + if (!mRedirectListener.startServer(new RedirectListener.RedirectCallback() { + /** Called on different thread (RedirectListener thread). */ + @Override + public void onRedirectReceived() { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "Received HTTP redirect response"); + } + mProvisioningStateMachine.getHandler().post(() -> handleRedirectResponse()); + } + + /** Called on main thread (WifiService thread). */ + @Override + public void onRedirectTimedOut() { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "Timed out to receive a HTTP redirect response"); + } + mProvisioningStateMachine.handleTimeOutForRedirectResponse(); + } + })) { + Log.e(TAG, "fails to start redirect listener"); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_START_REDIRECT_LISTENER); return; } - Log.d(TAG, "Exec: " + devDataResponse.getSppCommand().getExecCommandId() + ", for '" - + devDataResponse.getSppCommand().getCommandData() + "'"); + Intent intent = new Intent(WifiManager.ACTION_PASSPOINT_LAUNCH_OSU_VIEW); + intent.setPackage(OSU_APP_PACKAGE); + intent.putExtra(WifiManager.EXTRA_OSU_NETWORK, mNetwork); + intent.putExtra(WifiManager.EXTRA_URL, mWebUrl); - mWebUrl = ((BrowserUri) devDataResponse.getSppCommand().getCommandData()).getUri(); - if (mWebUrl == null) { - Log.e(TAG, "No Web-Url"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_INVALID_SERVER_URL); + intent.setFlags( + Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); + + // Verify that the intent will resolve to an activity + if (intent.resolveActivity(mContext.getPackageManager()) != null) { + mContext.startActivityAsUser(intent, UserHandle.CURRENT); + invokeProvisioningCallback(PROVISIONING_STATUS, + ProvisioningCallback.OSU_STATUS_WAITING_FOR_REDIRECT_RESPONSE); + changeState(STATE_WAITING_FOR_REDIRECT_RESPONSE); + } else { + Log.e(TAG, "can't resolve the activity for the intent"); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_NO_OSU_ACTIVITY_FOUND); return; } + } + + /** + * Initiates the second SOAP message exchange with sending the sppPostDevData message. + */ + private void secondSoapExchange() { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "Initiates the second soap message exchange in state =" + mState); + } - if (!mWebUrl.toLowerCase(Locale.US).contains(mSessionId.toLowerCase(Locale.US))) { - Log.e(TAG, "Bad or Missing session ID in webUrl"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_INVALID_SERVER_URL); + if (mState != STATE_WAITING_FOR_REDIRECT_RESPONSE) { + Log.e(TAG, "Initiates the second soap message exchange in wrong state=" + mState); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } + // TODO(b/74244324): Implement a routine to transmit second SOAP message. + } + + /** + * Handles SOAP message response sent by server + * + * @param sessionId indicating current session ID + * @param responseMessage SOAP SPP response, or {@code null} in any failure. + * Note: Called on main thread (WifiService thread). + */ + public void handleSoapMessageResponse(int sessionId, + @Nullable SppResponseMessage responseMessage) { + if (sessionId != mCurrentSessionId) { + Log.w(TAG, "Expected soapMessageResponse callback for currentSessionId=" + + mCurrentSessionId); + return; + } + + if (responseMessage == null) { + Log.e(TAG, "failed to send the sppPostDevData message"); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); + return; + } + + if (mState == STATE_WAITING_FOR_FIRST_SOAP_RESPONSE) { + if (responseMessage.getMessageType() + != SppResponseMessage.MessageType.POST_DEV_DATA_RESPONSE) { + Log.e(TAG, "Expected a PostDevDataResponse, but got " + + responseMessage.getMessageType()); + resetStateMachine( + ProvisioningCallback.OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_TYPE); + return; + } + + PostDevDataResponse devDataResponse = (PostDevDataResponse) responseMessage; + mSessionId = devDataResponse.getSessionID(); + if (devDataResponse.getSppCommand().getExecCommandId() + != SppCommand.ExecCommandId.BROWSER) { + Log.e(TAG, "Expected a launchBrowser command, but got " + + devDataResponse.getSppCommand().getExecCommandId()); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_UNEXPECTED_COMMAND_TYPE); + return; + } + + Log.d(TAG, "Exec: " + devDataResponse.getSppCommand().getExecCommandId() + ", for '" + + devDataResponse.getSppCommand().getCommandData() + "'"); + + mWebUrl = ((BrowserUri) devDataResponse.getSppCommand().getCommandData()).getUri(); + if (mWebUrl == null) { + Log.e(TAG, "No Web-Url"); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_INVALID_SERVER_URL); + return; + } + + if (!mWebUrl.toLowerCase(Locale.US).contains(mSessionId.toLowerCase(Locale.US))) { + Log.e(TAG, "Bad or Missing session ID in webUrl"); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_INVALID_SERVER_URL); + return; + } + launchOsuWebView(); + } + } + + /** + * Handles next step once receiving a HTTP redirect response. + * + * Note: Called on main thread (WifiService thread). + */ + public void handleRedirectResponse() { + if (mState != STATE_WAITING_FOR_REDIRECT_RESPONSE) { + Log.e(TAG, "Received redirect request in wrong state=" + mState); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); + return; + } + + invokeProvisioningCallback(PROVISIONING_STATUS, + ProvisioningCallback.OSU_STATUS_REDIRECT_RESPONSE_RECEIVED); + mRedirectListener.stopServer(); + secondSoapExchange(); + } + + /** + * Handles next step when timeout occurs because {@link RedirectListener} doesn't + * receive a HTTP redirect response. + * + * Note: Called on main thread (WifiService thread). + */ + public void handleTimeOutForRedirectResponse() { + Log.e(TAG, "Timed out for HTTP redirect response"); + + if (mState != STATE_WAITING_FOR_REDIRECT_RESPONSE) { + Log.e(TAG, "Received timeout error for HTTP redirect response in wrong state=" + + mState); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); + return; + } + mRedirectListener.stopServer(); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_TIMED_OUT_REDIRECT_LISTENER); } /** * Connected event received + * * @param network Network object for this connection + * Note: Called on main thread (WifiService thread). */ public void handleConnectedEvent(Network network) { if (mVerboseLoggingEnabled) { @@ -402,6 +548,8 @@ public class PasspointProvisioner { /** * Disconnect event received + * + * Note: Called on main thread (WifiService thread). */ public void handleDisconnect() { if (mVerboseLoggingEnabled) { @@ -444,6 +592,7 @@ public class PasspointProvisioner { private void resetStateMachine(int failureCode) { invokeProvisioningCallback(PROVISIONING_FAILURE, failureCode); + mRedirectListener.stopServer(); mOsuNetworkConnection.setEventCallback(null); mOsuNetworkConnection.disconnectIfNeeded(); mOsuServerConnection.setEventCallback(null); @@ -454,6 +603,8 @@ public class PasspointProvisioner { /** * Callbacks for network and wifi events + * + * Note: Called on main thread (WifiService thread). */ class OsuNetworkCallbacks implements OsuNetworkConnection.Callbacks { @@ -505,6 +656,8 @@ public class PasspointProvisioner { /** * Defines the callbacks expected from OsuServerConnection + * + * Note: Called on main thread (WifiService thread). */ public class OsuServerCallbacks { private final int mSessionId; @@ -515,6 +668,7 @@ public class PasspointProvisioner { /** * Returns the session ID corresponding to this callback + * * @return int sessionID */ public int getSessionId() { @@ -523,6 +677,7 @@ public class PasspointProvisioner { /** * Provides a server validation status for the session ID + * * @param sessionId integer indicating current session ID * @param succeeded boolean indicating success/failure of server validation */ @@ -541,8 +696,22 @@ public class PasspointProvisioner { } } + /** + * Callback when soap message is received from server. + * + * @param sessionId indicating current session ID + * @param responseMessage SOAP SPP response parsed or {@code null} in any failure + * Note: Called on different thread (OsuServer Thread)! + */ + public void onReceivedSoapMessage(int sessionId, + @Nullable SppResponseMessage responseMessage) { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "onReceivedSoapMessage with sessionId=" + sessionId); + } + mProvisioningStateMachine.getHandler().post(() -> + mProvisioningStateMachine.handleSoapMessageResponse(sessionId, + responseMessage)); + } } } - - diff --git a/service/java/com/android/server/wifi/hotspot2/soap/RedirectListener.java b/service/java/com/android/server/wifi/hotspot2/soap/RedirectListener.java index 6b7873d3e..5c9391150 100644 --- a/service/java/com/android/server/wifi/hotspot2/soap/RedirectListener.java +++ b/service/java/com/android/server/wifi/hotspot2/soap/RedirectListener.java @@ -16,42 +16,91 @@ package com.android.server.wifi.hotspot2.soap; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; + import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.URL; import java.util.Random; +import fi.iki.elonen.NanoHTTPD; + /** - * Redirect message listener to listen for the redirect message from server. + * Server for listening for redirect request from the OSU server to indicate the completion + * of user input. + * + * A HTTP server will be started in the {@link RedirectListener#startServer} of {@link + * RedirectListener}, so the caller will need to invoke {@link RedirectListener#stop} once the + * redirect server no longer needed. */ -public class RedirectListener extends Thread { +public class RedirectListener extends NanoHTTPD { + // 4 minutes for the maximum wait time. + @VisibleForTesting + static final int USER_TIMEOUT_MILLIS = 4 * 60 * 1000; + private static final String TAG = "RedirectListener"; - private final ServerSocket mServerSocket; + private final String mPath; - private final URL mURL; + private final URL mServerUrl; + private final Handler mStartStopHandler; + private final Handler mHandler; + private Runnable mTimeOutTask; + private RedirectCallback mRedirectCallback; + + /** + * Listener interface for handling redirect events. + */ + public interface RedirectCallback { + + /** + * Invoked when HTTP redirect response is received. + */ + void onRedirectReceived(); + + /** + * Invoked when timeout occurs on receiving HTTP redirect response. + */ + void onRedirectTimedOut(); + } + + @VisibleForTesting + /* package */ RedirectListener(Looper looper, @Nullable Looper startStopLooper, int port) + throws IOException { + super(InetAddress.getLocalHost().getHostAddress(), port); - private RedirectListener() throws IOException { - mServerSocket = new ServerSocket(0, 5, InetAddress.getLocalHost()); Random rnd = new Random(System.currentTimeMillis()); + mPath = "rnd" + Integer.toString(Math.abs(rnd.nextInt()), Character.MAX_RADIX); - mURL = new URL("http", mServerSocket.getInetAddress().getHostAddress(), - mServerSocket.getLocalPort(), mPath); - setName("HS20-Redirect-Listener"); - setDaemon(true); + mServerUrl = new URL("http", getHostname(), port, mPath); + mHandler = new Handler(looper); + mTimeOutTask = () -> mRedirectCallback.onRedirectTimedOut(); + if (startStopLooper == null) { + HandlerThread redirectHandlerThread = new HandlerThread("RedirectListenerHandler"); + redirectHandlerThread.start(); + startStopLooper = redirectHandlerThread.getLooper(); + } + mStartStopHandler = new Handler(startStopLooper); } /** * Create an instance of {@link RedirectListener} * + * @param looper Looper on which the {@link RedirectCallback} will be called. * @return Instance of {@link RedirectListener}, {@code null} in any failure. */ - public static RedirectListener createInstance() { + public static RedirectListener createInstance(@NonNull Looper looper) { RedirectListener redirectListener; try { - redirectListener = new RedirectListener(); + redirectListener = new RedirectListener(looper, null, + new ServerSocket(0, 1, InetAddress.getLocalHost()).getLocalPort()); } catch (IOException e) { Log.e(TAG, "fails to create an instance: " + e); return null; @@ -59,7 +108,73 @@ public class RedirectListener extends Thread { return redirectListener; } - public URL getURL() { - return mURL; + /** + * Start redirect listener + * + * @param callback to be notified when the redirect request is received or timed out. + * @return {@code true} in success, {@code false} if the {@code callback} is {@code null} or the + * server is already running. + */ + public boolean startServer(@NonNull RedirectCallback callback) { + if (callback == null) { + return false; + } + + if (isAlive()) { + Log.e(TAG, "redirect listener is already running"); + return false; + } + mRedirectCallback = callback; + + mStartStopHandler.post(() -> { + try { + start(); + } catch (IOException e) { + Log.e(TAG, "unable to start redirect listener: " + e); + } + }); + mHandler.postDelayed(mTimeOutTask, USER_TIMEOUT_MILLIS); + return true; + } + + /** + * Stop redirect listener + */ + public void stopServer() { + if (isServerAlive()) { + mStartStopHandler.post(() -> stop()); + } + } + + /** + * Check if the server is alive or not. + * + * @return {@code true} if the server is alive. + */ + public boolean isServerAlive() { + return isAlive(); + } + + /** + * Get URL to which the local redirect server listens + * + * @return The URL for the local redirect server. + */ + public URL getServerUrl() { + return mServerUrl; + } + + @Override + public Response serve(IHTTPSession session) { + + // Ignore all other requests except for a HTTP request that has the server url path with + // GET method. + if (session.getMethod() != Method.GET || !mServerUrl.getPath().equals(session.getUri())) { + return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_HTML, ""); + } + + mHandler.removeCallbacks(mTimeOutTask); + mRedirectCallback.onRedirectReceived(); + return newFixedLengthResponse(""); } } diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java index c91b1bcb5..8a123c2f2 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java @@ -16,9 +16,7 @@ package com.android.server.wifi.hotspot2; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.isNull; @@ -30,6 +28,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.net.Network; +import android.os.test.TestLooper; import android.support.test.filters.SmallTest; import android.util.Pair; @@ -78,6 +77,7 @@ public class OsuServerConnectionTest { private static final int ENABLE_VERBOSE_LOGGING = 1; private static final int TEST_SESSION_ID = 1; + private TestLooper mLooper = new TestLooper(); private OsuServerConnection mOsuServerConnection; private URL mValidServerUrl; private List<Pair<Locale, String>> mProviderIdentities = new ArrayList<>(); @@ -98,7 +98,7 @@ public class OsuServerConnectionTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mOsuServerConnection = new OsuServerConnection(); + mOsuServerConnection = new OsuServerConnection(mLooper.getLooper()); mOsuServerConnection.enableVerboseLogging(ENABLE_VERBOSE_LOGGING); mProviderIdentities.add(Pair.create(Locale.US, PROVIDER_NAME_VALID)); mValidServerUrl = new URL(TEST_VALID_URL); @@ -249,16 +249,16 @@ public class OsuServerConnectionTest { } /** - * Verifies {@code ExchangeSoapMessage} should return {@code null} if there is no connection. + * Verifies {@code ExchangeSoapMessage} should return {@code false} if there is no connection. */ @Test public void verifyExchangeSoapMessageWithoutConnection() { - assertNull(mOsuServerConnection.exchangeSoapMessage( + assertFalse(mOsuServerConnection.exchangeSoapMessage( new SoapSerializationEnvelope(SoapEnvelope.VER12))); } /** - * Verifies {@code ExchangeSoapMessage} should return {@code null} if {@code soapEnvelope} is + * Verifies {@code ExchangeSoapMessage} should return {@code false} if {@code soapEnvelope} is * {@code null} */ @Test @@ -267,12 +267,12 @@ public class OsuServerConnectionTest { mOsuServerConnection.setEventCallback(mOsuServerCallbacks); assertTrue(mOsuServerConnection.connect(mValidServerUrl, mNetwork)); - assertNull(mOsuServerConnection.exchangeSoapMessage(null)); + assertFalse(mOsuServerConnection.exchangeSoapMessage(null)); } /** - * Verifies {@code ExchangeSoapMessage} should return {@code null} if exception occurs during - * soap exchange. + * Verifies {@code ExchangeSoapMessage} should get {@code null} message if exception occurs + * during soap exchange. */ @Test public void verifyExchangeSoapMessageWithException() throws Exception { @@ -280,14 +280,19 @@ public class OsuServerConnectionTest { MockitoSession session = ExtendedMockito.mockitoSession().mockStatic( HttpsTransport.class).startMocking(); try { + mOsuServerConnection.init(mTlsContext, mDelegate); + mOsuServerConnection.setEventCallback(mOsuServerCallbacks); when(HttpsTransport.createInstance(any(Network.class), any(URL.class))).thenReturn( mHttpsTransport); doThrow(new IOException()).when(mHttpsTransport).call(any(String.class), any(SoapSerializationEnvelope.class)); assertTrue(mOsuServerConnection.connect(mValidServerUrl, mNetwork)); - assertNull(mOsuServerConnection.exchangeSoapMessage( + assertTrue(mOsuServerConnection.exchangeSoapMessage( new SoapSerializationEnvelope(SoapEnvelope.VER12))); + + mLooper.dispatchAll(); + verify(mOsuServerCallbacks).onReceivedSoapMessage(anyInt(), isNull()); } finally { session.finishMocking(); } @@ -302,6 +307,8 @@ public class OsuServerConnectionTest { MockitoSession session = ExtendedMockito.mockitoSession().mockStatic( HttpsTransport.class).startMocking(); try { + mOsuServerConnection.init(mTlsContext, mDelegate); + mOsuServerConnection.setEventCallback(mOsuServerCallbacks); when(HttpsTransport.createInstance(any(Network.class), any(URL.class))).thenReturn( mHttpsTransport); assertTrue(mOsuServerConnection.connect(mValidServerUrl, mNetwork)); @@ -314,7 +321,10 @@ public class OsuServerConnectionTest { envelope.bodyIn = new SoapObject(); when(SoapParser.getResponse(any(SoapObject.class))).thenReturn(mSppResponseMessage); - assertEquals(mSppResponseMessage, mOsuServerConnection.exchangeSoapMessage(envelope)); + assertTrue(mOsuServerConnection.exchangeSoapMessage(envelope)); + + mLooper.dispatchAll(); + verify(mOsuServerCallbacks).onReceivedSoapMessage(anyInt(), eq(mSppResponseMessage)); } finally { session.finishMocking(); } diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java index 161bd2a86..5e73cd652 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java @@ -29,6 +29,11 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.net.Network; import android.net.wifi.WifiManager; import android.net.wifi.WifiSsid; @@ -42,19 +47,23 @@ import android.os.test.TestLooper; import android.support.test.filters.SmallTest; import android.telephony.TelephonyManager; +import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.org.conscrypt.TrustManagerImpl; import com.android.server.wifi.WifiNative; import com.android.server.wifi.hotspot2.soap.PostDevDataResponse; +import com.android.server.wifi.hotspot2.soap.RedirectListener; import com.android.server.wifi.hotspot2.soap.SppResponseMessage; import com.android.server.wifi.hotspot2.soap.command.BrowserUri; import com.android.server.wifi.hotspot2.soap.command.SppCommand; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.ksoap2.serialization.SoapSerializationEnvelope; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; import java.net.URL; import java.security.KeyStore; @@ -71,6 +80,7 @@ public class PasspointProvisionerTest { private static final int STEP_INIT = 0; private static final int STEP_AP_CONNECT = 1; private static final int STEP_SERVER_CONNECT = 2; + private static final int STEP_WAIT_FOR_REDIRECT_RESPONSE = 3; private static final String TEST_DEV_ID = "12312341"; private static final String TEST_MANUFACTURER = Build.MANUFACTURER; @@ -84,20 +94,27 @@ public class PasspointProvisionerTest { private static final String TEST_SW_VERSION = "Android Test 1.0"; private static final String TEST_FW_VERSION = "Test FW 1.0"; private static final String TEST_REDIRECT_URL = "http://127.0.0.1:12345/index.htm"; + private static final String OSU_APP_PACKAGE = "com.android.hotspot2"; + private static final String OSU_APP_NAME = "OsuLogin"; private PasspointProvisioner mPasspointProvisioner; private TestLooper mLooper = new TestLooper(); private Handler mHandler; private OsuNetworkConnection.Callbacks mOsuNetworkCallbacks; private PasspointProvisioner.OsuServerCallbacks mOsuServerCallbacks; + private RedirectListener.RedirectCallback mRedirectReceivedListener; private ArgumentCaptor<OsuNetworkConnection.Callbacks> mOsuNetworkCallbacksCaptor = ArgumentCaptor.forClass(OsuNetworkConnection.Callbacks.class); private ArgumentCaptor<PasspointProvisioner.OsuServerCallbacks> mOsuServerCallbacksCaptor = ArgumentCaptor.forClass(PasspointProvisioner.OsuServerCallbacks.class); + private ArgumentCaptor<RedirectListener.RedirectCallback> + mOnRedirectReceivedArgumentCaptor = + ArgumentCaptor.forClass(RedirectListener.RedirectCallback.class); private ArgumentCaptor<Handler> mHandlerCaptor = ArgumentCaptor.forClass(Handler.class); private OsuProvider mOsuProvider; private TrustManagerImpl mDelegate; private URL mTestUrl; + private MockitoSession mSession; @Mock PasspointObjectFactory mObjectFactory; @Mock Context mContext; @@ -110,17 +127,27 @@ public class PasspointProvisionerTest { @Mock KeyStore mKeyStore; @Mock SSLContext mTlsContext; @Mock WifiNative mWifiNative; - @Mock SoapSerializationEnvelope mSoapEnvelope; @Mock PostDevDataResponse mSppResponseMessage; @Mock SystemInfo mSystemInfo; @Mock TelephonyManager mTelephonyManager; @Mock SppCommand mSppCommand; @Mock BrowserUri mBrowserUri; + @Mock RedirectListener mRedirectListener; + @Mock PackageManager mPackageManager; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mTestUrl = new URL(TEST_REDIRECT_URL); + mSession = ExtendedMockito.mockitoSession().mockStatic( + RedirectListener.class).startMocking(); + + when(RedirectListener.createInstance(mLooper.getLooper())).thenReturn( + mRedirectListener); + when(mRedirectListener.getServerUrl()).thenReturn(new URL(TEST_REDIRECT_URL)); + when(mRedirectListener.startServer( + any(RedirectListener.RedirectCallback.class))).thenReturn(true); + when(mRedirectListener.isAlive()).thenReturn(true); when(mWifiManager.isWifiEnabled()).thenReturn(true); when(mObjectFactory.makeOsuNetworkConnection(any(Context.class))) .thenReturn(mOsuNetworkConnection); @@ -162,8 +189,20 @@ public class PasspointProvisionerTest { when(mSppCommand.getCommandData()).thenReturn(mBrowserUri); when(mBrowserUri.getUri()).thenReturn(TEST_URL); when(mOsuServerConnection.exchangeSoapMessage( - any(SoapSerializationEnvelope.class))).thenReturn( - mSppResponseMessage); + any(SoapSerializationEnvelope.class))).thenReturn(true); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = new ActivityInfo(); + resolveInfo.activityInfo.applicationInfo = new ApplicationInfo(); + resolveInfo.activityInfo.name = OSU_APP_NAME; + resolveInfo.activityInfo.applicationInfo.packageName = OSU_APP_PACKAGE; + when(mPackageManager.resolveActivity(any(Intent.class), + eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(resolveInfo); + } + + @After + public void cleanUp() { + mSession.finishMocking(); } private void initAndStartProvisioning() { @@ -205,6 +244,30 @@ public class PasspointProvisionerTest { } else if (step == STEP_SERVER_CONNECT) { verify(mCallback).onProvisioningStatus( ProvisioningCallback.OSU_STATUS_SERVER_CONNECTED); + } else if (step == STEP_WAIT_FOR_REDIRECT_RESPONSE) { + // Server validation passed + mOsuServerCallbacks.onServerValidationStatus(mOsuServerCallbacks.getSessionId(), + true); + mLooper.dispatchAll(); + + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_SERVER_VALIDATED); + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_SERVICE_PROVIDER_VERIFIED); + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_INIT_SOAP_EXCHANGE); + + // Received soapMessageResponse + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), + mSppResponseMessage); + mLooper.dispatchAll(); + + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_WAITING_FOR_REDIRECT_RESPONSE); + verify(mRedirectListener, atLeastOnce()) + .startServer(mOnRedirectReceivedArgumentCaptor.capture()); + mRedirectReceivedListener = mOnRedirectReceivedArgumentCaptor.getValue(); + verifyNoMoreInteractions(mCallback); } } } @@ -426,7 +489,7 @@ public class PasspointProvisionerTest { public void verifyExchangingSoapMessageFailure() throws RemoteException { // Fail to exchange the SOAP message when(mOsuServerConnection.exchangeSoapMessage( - any(SoapSerializationEnvelope.class))).thenReturn(null); + any(SoapSerializationEnvelope.class))).thenReturn(false); stopAfterStep(STEP_SERVER_CONNECT); // Server validation passed @@ -436,19 +499,21 @@ public class PasspointProvisionerTest { verify(mCallback).onProvisioningStatus(ProvisioningCallback.OSU_STATUS_SERVER_VALIDATED); verify(mCallback).onProvisioningStatus( ProvisioningCallback.OSU_STATUS_SERVICE_PROVIDER_VERIFIED); - verify(mCallback).onProvisioningStatus(ProvisioningCallback.OSU_STATUS_INIT_SOAP_EXCHANGE); verify(mCallback).onProvisioningFailure( ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); - // Osu provider verification is the last current step in the flow, no more runnables posted. + // No further runnables posted verifyNoMoreInteractions(mCallback); } /** - * Verifies that the right provisioning callbacks are invoked as the provisioner progresses - * to the end as successful case. + * Verifies that the right provisioning callbacks are invoked when there is no OSU activity for + * the intent */ @Test - public void verifyProvisioningFlowForSuccessfulCase() throws RemoteException { + public void verifyNoOsuActivityFoundFailure() throws RemoteException { + // There is no activity found for the intent + when(mPackageManager.resolveActivity(any(Intent.class), + eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(null); stopAfterStep(STEP_SERVER_CONNECT); // Server validation passed @@ -459,8 +524,53 @@ public class PasspointProvisionerTest { verify(mCallback).onProvisioningStatus( ProvisioningCallback.OSU_STATUS_SERVICE_PROVIDER_VERIFIED); verify(mCallback).onProvisioningStatus(ProvisioningCallback.OSU_STATUS_INIT_SOAP_EXCHANGE); - // Osu provider verification is the last current step in the flow, no more runnables posted. + + // Received soapMessageResponse + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), + mSppResponseMessage); + mLooper.dispatchAll(); + + verify(mCallback).onProvisioningFailure( + ProvisioningCallback.OSU_FAILURE_NO_OSU_ACTIVITY_FOUND); + // No further runnables posted verifyNoMoreInteractions(mCallback); } -} + /** + * Verifies that the right provisioning callbacks are invoked when timeout occurs for HTTP + * redirect response. + */ + @Test + public void verifyRedirectResponseTimeout() throws RemoteException { + stopAfterStep(STEP_WAIT_FOR_REDIRECT_RESPONSE); + + // Timed out for HTTP redirect response. + mRedirectReceivedListener.onRedirectTimedOut(); + mLooper.dispatchAll(); + + verify(mRedirectListener, atLeastOnce()).stopServer(); + verify(mCallback).onProvisioningFailure( + ProvisioningCallback.OSU_FAILURE_TIMED_OUT_REDIRECT_LISTENER); + // No further runnables posted + verifyNoMoreInteractions(mCallback); + } + + /** + * Verifies that the right provisioning callbacks are invoked as the provisioner progresses + * to the end as successful case. + */ + @Test + public void verifyProvisioningFlowForSuccessfulCase() throws RemoteException { + stopAfterStep(STEP_WAIT_FOR_REDIRECT_RESPONSE); + + // Received HTTP redirect response. + mRedirectReceivedListener.onRedirectReceived(); + mLooper.dispatchAll(); + + verify(mRedirectListener, atLeastOnce()).stopServer(); + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_REDIRECT_RESPONSE_RECEIVED); + // No further runnables posted + verifyNoMoreInteractions(mCallback); + } +} diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataMessageTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataMessageTest.java index de95cc8ca..179ac68a6 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataMessageTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataMessageTest.java @@ -43,7 +43,6 @@ import org.ksoap2.serialization.SoapPrimitive; import org.ksoap2.serialization.SoapSerializationEnvelope; import org.mockito.Mock; - /** * Unit tests for {@link PostDevDataMessage}. */ diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataResponseTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataResponseTest.java index 25e91cba8..d5b827d8f 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataResponseTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataResponseTest.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.MockitoAnnotations.initMocks; +import android.support.test.filters.SmallTest; + import com.android.server.wifi.hotspot2.soap.command.SppCommand; import org.junit.Before; @@ -30,6 +32,7 @@ import org.ksoap2.serialization.SoapObject; /** * Unit tests for {@link PostDevDataResponse}. */ +@SmallTest public class PostDevDataResponseTest { private static final String EXEC = "exec"; private static final String BROWSER_COMMAND = "launchBrowserToURI"; diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/RedirectListenerTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/RedirectListenerTest.java new file mode 100644 index 000000000..c4fb3fd71 --- /dev/null +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/RedirectListenerTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2018 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.hotspot2.soap; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.os.Looper; +import android.os.test.TestLooper; +import android.support.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import java.io.IOException; +import java.net.URL; + +import fi.iki.elonen.NanoHTTPD; + +/** + * Unit tests for {@link RedirectListener}. + */ +@SmallTest +public class RedirectListenerTest { + private static final int TEST_PORT = 1010; + + private RedirectListenerSpy mRedirectListener; + private URL mServerUrl; + private TestLooper mLooper = new TestLooper(); + + @Mock RedirectListener.RedirectCallback mListener; + @Mock NanoHTTPD.IHTTPSession mIHTTPSession; + + /** Spy class to avoid start/stop {@link NanoHTTPD} server */ + private class RedirectListenerSpy extends RedirectListener { + boolean mIsStart = false; + RedirectListenerSpy(Looper looper, int port) throws IOException { + super(looper, looper, port); + } + + @Override + public void start() { + mIsStart = true; + } + + @Override + public void stop() { + mIsStart = false; + } + + @Override + public boolean isServerAlive() { + return mIsStart; + } + } + + /** + * Sets up test. + */ + @Before + public void setUp() throws Exception { + initMocks(this); + + mRedirectListener = new RedirectListenerSpy(mLooper.getLooper(), TEST_PORT); + mServerUrl = mRedirectListener.getServerUrl(); + } + + private void verifyStartServer() { + mRedirectListener.startServer(mListener); + mLooper.dispatchAll(); + + assertTrue(mRedirectListener.mIsStart); + } + + private void verifyStopServer() { + mRedirectListener.stopServer(); + mLooper.dispatchAll(); + + assertFalse(mRedirectListener.mIsStart); + } + + /** + * Verifies that Timeout handler will be invoked when There is no a known GET request received + * in a {@link RedirectListener#USER_TIMEOUT_MILLIS}. + */ + @Test + public void timeOutForKnownGetRequest() { + when(mIHTTPSession.getMethod()).thenReturn(NanoHTTPD.Method.PUT); + verifyStartServer(); + mRedirectListener.serve(mIHTTPSession); + + verify(mListener, never()).onRedirectReceived(); + + // Timeout has expired. + mLooper.moveTimeForward(RedirectListener.USER_TIMEOUT_MILLIS); + mLooper.dispatchAll(); + + verify(mListener).onRedirectTimedOut(); + verifyStopServer(); + } + + /** + * Verifies that {@link RedirectListener.RedirectCallback#onRedirectReceived()} will not be + * invoked when receiving a GET request with an unexpected path. + */ + @Test + public void receiveUnknownGetRequest() { + when(mIHTTPSession.getMethod()).thenReturn(NanoHTTPD.Method.GET); + when(mIHTTPSession.getUri()).thenReturn("/test"); + verifyStartServer(); + + mRedirectListener.serve(mIHTTPSession); + + verify(mListener, never()).onRedirectReceived(); + + // Timeout has expired. + mLooper.moveTimeForward(RedirectListener.USER_TIMEOUT_MILLIS); + mLooper.dispatchAll(); + + verify(mListener).onRedirectTimedOut(); + verifyStopServer(); + } + + /** + * Verifies that a {@link RedirectListener.RedirectCallback#onRedirectReceived()} callback will + * be invoked when receiving a GET request with an expected path. + */ + @Test + public void receiveKnownGetRequest() { + when(mIHTTPSession.getMethod()).thenReturn(NanoHTTPD.Method.GET); + when(mIHTTPSession.getUri()).thenReturn(mServerUrl.getPath()); + verifyStartServer(); + + mRedirectListener.serve(mIHTTPSession); + + verify(mListener).onRedirectReceived(); + + mLooper.moveTimeForward(RedirectListener.USER_TIMEOUT_MILLIS); + mLooper.dispatchAll(); + + // TimeoutTask is cancelled once receiving HTTP redirect response. + verify(mListener, never()).onRedirectTimedOut(); + verifyStopServer(); + } +} + + + + + + + + diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SoapParserTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SoapParserTest.java index 344d1f93f..3bb63a157 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SoapParserTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SoapParserTest.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.MockitoAnnotations.initMocks; +import android.support.test.filters.SmallTest; + import org.junit.Before; import org.junit.Test; import org.ksoap2.serialization.PropertyInfo; @@ -28,6 +30,7 @@ import org.ksoap2.serialization.SoapObject; /** * Unit tests for {@link SoapParser}. */ +@SmallTest public class SoapParserTest { private static final String EXEC = "exec"; private static final String BROWSER_COMMAND = "launchBrowserToURI"; diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SppResponseMessageTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SppResponseMessageTest.java index 5253946b2..5e6ed070c 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SppResponseMessageTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SppResponseMessageTest.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.MockitoAnnotations.initMocks; +import android.support.test.filters.SmallTest; + import org.junit.Before; import org.junit.Test; import org.ksoap2.serialization.SoapObject; @@ -30,6 +32,7 @@ import java.util.Map; /** * Unit tests for {@link SppResponseMessage}. */ +@SmallTest public class SppResponseMessageTest { private static final String TEST_STATUS = "OK"; private static final String TEST_ERROR_STATUS = "Error occurred"; |