/* * Copyright (C) 2014 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 android.net.wifi.passpoint; import android.content.Context; import android.net.wifi.ScanResult; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Messenger; import android.os.Parcel; import android.os.Parcelable; import android.os.RemoteException; import android.util.Log; import com.android.internal.util.AsyncChannel; import com.android.internal.util.Protocol; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; /** * Provides APIs for managing Wifi Passpoint credentials. * @hide */ public class WifiPasspointManager { private static final String TAG = "PasspointManager"; private static final boolean DBG = true; /* Passpoint states values */ /** Passpoint is in an unknown state. This should only occur in boot time */ public static final int PASSPOINT_STATE_UNKNOWN = 0; /** Passpoint is disabled. This occurs when wifi is disabled */ public static final int PASSPOINT_STATE_DISABLED = 1; /** Passpoint is enabled and in discovery state */ public static final int PASSPOINT_STATE_DISCOVERY = 2; /** Passpoint is enabled and in access state */ public static final int PASSPOINT_STATE_ACCESS = 3; /** Passpoint is enabled and in provisioning state */ public static final int PASSPOINT_STATE_PROVISION = 4; /* Passpoint callback error codes */ /** Indicates that the operation failed due to an internal error */ public static final int REASON_ERROR = 0; /** Indicates that the operation failed because wifi is disabled */ public static final int REASON_WIFI_DISABLED = 1; /** Indicates that the operation failed because the framework is busy */ public static final int REASON_BUSY = 2; /** Indicates that the operation failed because parameter is invalid */ public static final int REASON_INVALID_PARAMETER = 3; /** Indicates that the operation failed because the server is not trusted */ public static final int REASON_NOT_TRUSTED = 4; /** * protocol supported for Passpoint */ public static final String PROTOCOL_DM = "OMA-DM-ClientInitiated"; /** * protocol supported for Passpoint */ public static final String PROTOCOL_SOAP = "SPP-ClientInitiated"; /* Passpoint broadcasts */ /** * Broadcast intent action indicating that the state of Passpoint * connectivity has changed */ public static final String PASSPOINT_STATE_CHANGED_ACTION = "android.net.wifi.passpoint.STATE_CHANGE"; /** * Broadcast intent action indicating that the saved Passpoint credential * list has changed */ public static final String PASSPOINT_CRED_CHANGED_ACTION = "android.net.wifi.passpoint.CRED_CHANGE"; /** * Broadcast intent action indicating that Passpoint online sign up is * avaiable. */ public static final String PASSPOINT_OSU_AVAILABLE_ACTION = "android.net.wifi.passpoint.OSU_AVAILABLE"; /** * Broadcast intent action indicating that user remediation is required */ public static final String PASSPOINT_USER_REM_REQ_ACTION = "android.net.wifi.passpoint.USER_REM_REQ"; /** * Interface for callback invocation when framework channel is lost */ public interface ChannelListener { /** * The channel to the framework has been disconnected. Application could * try re-initializing using {@link #initialize} */ public void onChannelDisconnected(); } /** * Interface for callback invocation on an application action */ public interface ActionListener { /** The operation succeeded */ public void onSuccess(); /** * The operation failed * * @param reason The reason for failure could be one of * {@link #WIFI_DISABLED}, {@link #ERROR} or {@link #BUSY} */ public void onFailure(int reason); } /** * Interface for callback invocation when doing OSU or user remediation */ public interface OsuRemListener { /** The operation succeeded */ public void onSuccess(); /** * The operation failed * * @param reason The reason for failure could be one of * {@link #WIFI_DISABLED}, {@link #ERROR} or {@link #BUSY} */ public void onFailure(int reason); /** * Browser launch is requried for user interaction. When this callback * is called, app should launch browser / webview to the given URI. * * @param uri URI for browser launch */ public void onBrowserLaunch(String uri); /** * When this is called, app should dismiss the previously lanched browser. */ public void onBrowserDismiss(); } /** * A channel that connects the application to the wifi passpoint framework. * Most passpoint operations require a Channel as an argument. * An instance of Channel is obtained by doing a call on {@link #initialize} */ public static class Channel { private final static int INVALID_LISTENER_KEY = 0; private ChannelListener mChannelListener; private HashMap<Integer, Object> mListenerMap = new HashMap<Integer, Object>(); private HashMap<Integer, Integer> mListenerMapCount = new HashMap<Integer, Integer>(); private Object mListenerMapLock = new Object(); private int mListenerKey = 0; private List<ScanResult> mAnqpRequest = new LinkedList<ScanResult>(); private Object mAnqpRequestLock = new Object(); private AsyncChannel mAsyncChannel; private PasspointHandler mHandler; Context mContext; Channel(Context context, Looper looper, ChannelListener l) { mAsyncChannel = new AsyncChannel(); mHandler = new PasspointHandler(looper); mChannelListener = l; mContext = context; } private int putListener(Object listener) { return putListener(listener, 1); } private int putListener(Object listener, int count) { if (listener == null || count <= 0) return INVALID_LISTENER_KEY; int key; synchronized (mListenerMapLock) { do { key = mListenerKey++; } while (key == INVALID_LISTENER_KEY); mListenerMap.put(key, listener); mListenerMapCount.put(key, count); } return key; } private Object peekListener(int key) { Log.d(TAG, "peekListener() key=" + key); if (key == INVALID_LISTENER_KEY) return null; synchronized (mListenerMapLock) { return mListenerMap.get(key); } } private Object getListener(int key, boolean forceRemove) { Log.d(TAG, "getListener() key=" + key + " force=" + forceRemove); if (key == INVALID_LISTENER_KEY) return null; synchronized (mListenerMapLock) { if (!forceRemove) { int count = mListenerMapCount.get(key); Log.d(TAG, "count=" + count); mListenerMapCount.put(key, --count); if (count > 0) return null; } Log.d(TAG, "remove key"); mListenerMapCount.remove(key); return mListenerMap.remove(key); } } private void anqpRequestStart(ScanResult sr) { Log.d(TAG, "anqpRequestStart sr.bssid=" + sr.BSSID); synchronized (mAnqpRequestLock) { mAnqpRequest.add(sr); } } private void anqpRequestFinish(WifiPasspointInfo result) { Log.d(TAG, "anqpRequestFinish pi.bssid=" + result.bssid); synchronized (mAnqpRequestLock) { for (ScanResult sr : mAnqpRequest) if (sr.BSSID.equals(result.bssid)) { Log.d(TAG, "find hit " + result.bssid); /* sr.passpoint = result; */ mAnqpRequest.remove(sr); Log.d(TAG, "mAnqpRequest.len=" + mAnqpRequest.size()); break; } } } private void anqpRequestFinish(ScanResult sr) { Log.d(TAG, "anqpRequestFinish sr.bssid=" + sr.BSSID); synchronized (mAnqpRequestLock) { for (ScanResult sr1 : mAnqpRequest) if (sr1.BSSID.equals(sr.BSSID)) { mAnqpRequest.remove(sr1); break; } } } class PasspointHandler extends Handler { PasspointHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message message) { Object listener = null; switch (message.what) { case AsyncChannel.CMD_CHANNEL_DISCONNECTED: if (mChannelListener != null) { mChannelListener.onChannelDisconnected(); mChannelListener = null; } break; case REQUEST_ANQP_INFO_SUCCEEDED: WifiPasspointInfo result = (WifiPasspointInfo) message.obj; anqpRequestFinish(result); listener = getListener(message.arg2, false); if (listener != null) { ((ActionListener) listener).onSuccess(); } break; case REQUEST_ANQP_INFO_FAILED: anqpRequestFinish((ScanResult) message.obj); listener = getListener(message.arg2, false); if (listener == null) getListener(message.arg2, true); if (listener != null) { ((ActionListener) listener).onFailure(message.arg1); } break; case START_OSU_SUCCEEDED: listener = getListener(message.arg2, true); if (listener != null) { ((OsuRemListener) listener).onSuccess(); } break; case START_OSU_FAILED: listener = getListener(message.arg2, true); if (listener != null) { ((OsuRemListener) listener).onFailure(message.arg1); } break; case START_OSU_BROWSER: listener = peekListener(message.arg2); if (listener != null) { ParcelableString str = (ParcelableString) message.obj; if (str == null || str.string == null) ((OsuRemListener) listener).onBrowserDismiss(); else ((OsuRemListener) listener).onBrowserLaunch(str.string); } break; default: Log.d(TAG, "Ignored " + message); break; } } } } public static class ParcelableString implements Parcelable { public String string; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel out, int flags) { out.writeString(string); } public static final Parcelable.Creator<ParcelableString> CREATOR = new Parcelable.Creator<ParcelableString>() { @Override public ParcelableString createFromParcel(Parcel in) { ParcelableString ret = new ParcelableString(); ret.string = in.readString(); return ret; } @Override public ParcelableString[] newArray(int size) { return new ParcelableString[size]; } }; } private static final int BASE = Protocol.BASE_WIFI_PASSPOINT_MANAGER; public static final int REQUEST_ANQP_INFO = BASE + 1; public static final int REQUEST_ANQP_INFO_FAILED = BASE + 2; public static final int REQUEST_ANQP_INFO_SUCCEEDED = BASE + 3; public static final int REQUEST_OSU_ICON = BASE + 4; public static final int REQUEST_OSU_ICON_FAILED = BASE + 5; public static final int REQUEST_OSU_ICON_SUCCEEDED = BASE + 6; public static final int START_OSU = BASE + 7; public static final int START_OSU_BROWSER = BASE + 8; public static final int START_OSU_FAILED = BASE + 9; public static final int START_OSU_SUCCEEDED = BASE + 10; private Context mContext; IWifiPasspointManager mService; /** * TODO: doc * @param context * @param service */ public WifiPasspointManager(Context context, IWifiPasspointManager service) { mContext = context; mService = service; } /** * Registers the application with the framework. This function must be the * first to be called before any async passpoint operations are performed. * * @param srcContext is the context of the source * @param srcLooper is the Looper on which the callbacks are receivied * @param listener for callback at loss of framework communication. Can be * null. * @return Channel instance that is necessary for performing any further * passpoint operations * */ public Channel initialize(Context srcContext, Looper srcLooper, ChannelListener listener) { Messenger messenger = getMessenger(); if (messenger == null) return null; Channel c = new Channel(srcContext, srcLooper, listener); if (c.mAsyncChannel.connectSync(srcContext, c.mHandler, messenger) == AsyncChannel.STATUS_SUCCESSFUL) { return c; } else { return null; } } /** * STOPSHIP: temp solution, should use supplicant manager instead, check * with b/13931972 */ public Messenger getMessenger() { try { return mService.getMessenger(); } catch (RemoteException e) { return null; } } public int getPasspointState() { try { return mService.getPasspointState(); } catch (RemoteException e) { return PASSPOINT_STATE_UNKNOWN; } } public void requestAnqpInfo(Channel c, List<ScanResult> requested, int mask, ActionListener listener) { Log.d(TAG, "requestAnqpInfo start"); Log.d(TAG, "requested.size=" + requested.size()); checkChannel(c); List<ScanResult> list = new ArrayList<ScanResult>(); for (ScanResult sr : requested) if (sr.capabilities.contains("[HS20]")) { list.add(sr); c.anqpRequestStart(sr); Log.d(TAG, "adding " + sr.BSSID); } int count = list.size(); Log.d(TAG, "after filter, count=" + count); if (count == 0) { if (DBG) Log.d(TAG, "ANQP info request contains no HS20 APs, skipped"); listener.onSuccess(); return; } int key = c.putListener(listener, count); for (ScanResult sr : list) c.mAsyncChannel.sendMessage(REQUEST_ANQP_INFO, mask, key, sr); Log.d(TAG, "requestAnqpInfo end"); } public void requestOsuIcons(Channel c, List<WifiPasspointOsuProvider> requested, int resolution, ActionListener listener) { } public List<WifiPasspointPolicy> requestCredentialMatch(List<ScanResult> requested) { try { return mService.requestCredentialMatch(requested); } catch (RemoteException e) { return null; } } /** * Get a list of saved Passpoint credentials. Only those credentials owned * by the caller will be returned. * * @return The list of credentials */ public List<WifiPasspointCredential> getCredentials() { try { return mService.getCredentials(); } catch (RemoteException e) { return null; } } /** * Add a new Passpoint credential. * * @param cred The credential to be added * @return {@code true} if the operation succeeds, {@code false} otherwise */ public boolean addCredential(WifiPasspointCredential cred) { try { return mService.addCredential(cred); } catch (RemoteException e) { return false; } } /** * Update an existing Passpoint credential. Only system or the owner of this * credential has the permission to do this. * * @param cred The credential to be updated * @return {@code true} if the operation succeeds, {@code false} otherwise */ public boolean updateCredential(WifiPasspointCredential cred) { try { return mService.updateCredential(cred); } catch (RemoteException e) { return false; } } /** * Remove an existing Passpoint credential. Only system or the owner of this * credential has the permission to do this. * * @param cred The credential to be removed * @return {@code true} if the operation succeeds, {@code false} otherwise */ public boolean removeCredential(WifiPasspointCredential cred) { try { return mService.removeCredential(cred); } catch (RemoteException e) { return false; } } public void startOsu(Channel c, WifiPasspointOsuProvider osu, OsuRemListener listener) { Log.d(TAG, "startOsu start"); checkChannel(c); int key = c.putListener(listener); c.mAsyncChannel.sendMessage(START_OSU, 0, key, osu); Log.d(TAG, "startOsu end"); } public void startRemediation(Channel c, OsuRemListener listener) { } public void connect(WifiPasspointPolicy policy) { } private static void checkChannel(Channel c) { if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); } }