1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.net.wifi.passpoint; 18 19 import android.content.Context; 20 import android.net.wifi.ScanResult; 21 import android.os.Handler; 22 import android.os.Looper; 23 import android.os.Message; 24 import android.os.Messenger; 25 import android.os.Parcel; 26 import android.os.Parcelable; 27 import android.os.RemoteException; 28 import android.util.Log; 29 30 import com.android.internal.util.AsyncChannel; 31 import com.android.internal.util.Protocol; 32 33 import java.util.ArrayList; 34 import java.util.HashMap; 35 import java.util.LinkedList; 36 import java.util.List; 37 38 /** 39 * Provides APIs for managing Wifi Passpoint credentials. 40 * @hide 41 */ 42 public class WifiPasspointManager { 43 44 private static final String TAG = "PasspointManager"; 45 46 private static final boolean DBG = true; 47 48 /* Passpoint states values */ 49 50 /** Passpoint is in an unknown state. This should only occur in boot time */ 51 public static final int PASSPOINT_STATE_UNKNOWN = 0; 52 53 /** Passpoint is disabled. This occurs when wifi is disabled */ 54 public static final int PASSPOINT_STATE_DISABLED = 1; 55 56 /** Passpoint is enabled and in discovery state */ 57 public static final int PASSPOINT_STATE_DISCOVERY = 2; 58 59 /** Passpoint is enabled and in access state */ 60 public static final int PASSPOINT_STATE_ACCESS = 3; 61 62 /** Passpoint is enabled and in provisioning state */ 63 public static final int PASSPOINT_STATE_PROVISION = 4; 64 65 /* Passpoint callback error codes */ 66 67 /** Indicates that the operation failed due to an internal error */ 68 public static final int REASON_ERROR = 0; 69 70 /** Indicates that the operation failed because wifi is disabled */ 71 public static final int REASON_WIFI_DISABLED = 1; 72 73 /** Indicates that the operation failed because the framework is busy */ 74 public static final int REASON_BUSY = 2; 75 76 /** Indicates that the operation failed because parameter is invalid */ 77 public static final int REASON_INVALID_PARAMETER = 3; 78 79 /** Indicates that the operation failed because the server is not trusted */ 80 public static final int REASON_NOT_TRUSTED = 4; 81 82 /** 83 * protocol supported for Passpoint 84 */ 85 public static final String PROTOCOL_DM = "OMA-DM-ClientInitiated"; 86 87 /** 88 * protocol supported for Passpoint 89 */ 90 public static final String PROTOCOL_SOAP = "SPP-ClientInitiated"; 91 92 /* Passpoint broadcasts */ 93 94 /** 95 * Broadcast intent action indicating that the state of Passpoint 96 * connectivity has changed 97 */ 98 public static final String PASSPOINT_STATE_CHANGED_ACTION = 99 "android.net.wifi.passpoint.STATE_CHANGE"; 100 101 /** 102 * Broadcast intent action indicating that the saved Passpoint credential 103 * list has changed 104 */ 105 public static final String PASSPOINT_CRED_CHANGED_ACTION = 106 "android.net.wifi.passpoint.CRED_CHANGE"; 107 108 /** 109 * Broadcast intent action indicating that Passpoint online sign up is 110 * avaiable. 111 */ 112 public static final String PASSPOINT_OSU_AVAILABLE_ACTION = 113 "android.net.wifi.passpoint.OSU_AVAILABLE"; 114 115 /** 116 * Broadcast intent action indicating that user remediation is required 117 */ 118 public static final String PASSPOINT_USER_REM_REQ_ACTION = 119 "android.net.wifi.passpoint.USER_REM_REQ"; 120 121 /** 122 * Interface for callback invocation when framework channel is lost 123 */ 124 public interface ChannelListener { 125 /** 126 * The channel to the framework has been disconnected. Application could 127 * try re-initializing using {@link #initialize} 128 */ onChannelDisconnected()129 public void onChannelDisconnected(); 130 } 131 132 /** 133 * Interface for callback invocation on an application action 134 */ 135 public interface ActionListener { 136 /** The operation succeeded */ onSuccess()137 public void onSuccess(); 138 139 /** 140 * The operation failed 141 * 142 * @param reason The reason for failure could be one of 143 * {@link #WIFI_DISABLED}, {@link #ERROR} or {@link #BUSY} 144 */ onFailure(int reason)145 public void onFailure(int reason); 146 } 147 148 /** 149 * Interface for callback invocation when doing OSU or user remediation 150 */ 151 public interface OsuRemListener { 152 /** The operation succeeded */ onSuccess()153 public void onSuccess(); 154 155 /** 156 * The operation failed 157 * 158 * @param reason The reason for failure could be one of 159 * {@link #WIFI_DISABLED}, {@link #ERROR} or {@link #BUSY} 160 */ onFailure(int reason)161 public void onFailure(int reason); 162 163 /** 164 * Browser launch is requried for user interaction. When this callback 165 * is called, app should launch browser / webview to the given URI. 166 * 167 * @param uri URI for browser launch 168 */ onBrowserLaunch(String uri)169 public void onBrowserLaunch(String uri); 170 171 /** 172 * When this is called, app should dismiss the previously lanched browser. 173 */ onBrowserDismiss()174 public void onBrowserDismiss(); 175 } 176 177 /** 178 * A channel that connects the application to the wifi passpoint framework. 179 * Most passpoint operations require a Channel as an argument. 180 * An instance of Channel is obtained by doing a call on {@link #initialize} 181 */ 182 public static class Channel { 183 private final static int INVALID_LISTENER_KEY = 0; 184 185 private ChannelListener mChannelListener; 186 187 private HashMap<Integer, Object> mListenerMap = new HashMap<Integer, Object>(); 188 private HashMap<Integer, Integer> mListenerMapCount = new HashMap<Integer, Integer>(); 189 private Object mListenerMapLock = new Object(); 190 private int mListenerKey = 0; 191 192 private List<ScanResult> mAnqpRequest = new LinkedList<ScanResult>(); 193 private Object mAnqpRequestLock = new Object(); 194 195 private AsyncChannel mAsyncChannel; 196 private PasspointHandler mHandler; 197 Context mContext; 198 Channel(Context context, Looper looper, ChannelListener l)199 Channel(Context context, Looper looper, ChannelListener l) { 200 mAsyncChannel = new AsyncChannel(); 201 mHandler = new PasspointHandler(looper); 202 mChannelListener = l; 203 mContext = context; 204 } 205 putListener(Object listener)206 private int putListener(Object listener) { 207 return putListener(listener, 1); 208 } 209 putListener(Object listener, int count)210 private int putListener(Object listener, int count) { 211 if (listener == null || count <= 0) 212 return INVALID_LISTENER_KEY; 213 int key; 214 synchronized (mListenerMapLock) { 215 do { 216 key = mListenerKey++; 217 } while (key == INVALID_LISTENER_KEY); 218 mListenerMap.put(key, listener); 219 mListenerMapCount.put(key, count); 220 } 221 return key; 222 } 223 peekListener(int key)224 private Object peekListener(int key) { 225 Log.d(TAG, "peekListener() key=" + key); 226 if (key == INVALID_LISTENER_KEY) 227 return null; 228 synchronized (mListenerMapLock) { 229 return mListenerMap.get(key); 230 } 231 } 232 233 getListener(int key, boolean forceRemove)234 private Object getListener(int key, boolean forceRemove) { 235 Log.d(TAG, "getListener() key=" + key + " force=" + forceRemove); 236 if (key == INVALID_LISTENER_KEY) 237 return null; 238 synchronized (mListenerMapLock) { 239 if (!forceRemove) { 240 int count = mListenerMapCount.get(key); 241 Log.d(TAG, "count=" + count); 242 mListenerMapCount.put(key, --count); 243 if (count > 0) 244 return null; 245 } 246 Log.d(TAG, "remove key"); 247 mListenerMapCount.remove(key); 248 return mListenerMap.remove(key); 249 } 250 } 251 anqpRequestStart(ScanResult sr)252 private void anqpRequestStart(ScanResult sr) { 253 Log.d(TAG, "anqpRequestStart sr.bssid=" + sr.BSSID); 254 synchronized (mAnqpRequestLock) { 255 mAnqpRequest.add(sr); 256 } 257 } 258 anqpRequestFinish(WifiPasspointInfo result)259 private void anqpRequestFinish(WifiPasspointInfo result) { 260 Log.d(TAG, "anqpRequestFinish pi.bssid=" + result.bssid); 261 synchronized (mAnqpRequestLock) { 262 for (ScanResult sr : mAnqpRequest) 263 if (sr.BSSID.equals(result.bssid)) { 264 Log.d(TAG, "find hit " + result.bssid); 265 /* sr.passpoint = result; */ 266 mAnqpRequest.remove(sr); 267 Log.d(TAG, "mAnqpRequest.len=" + mAnqpRequest.size()); 268 break; 269 } 270 } 271 } 272 anqpRequestFinish(ScanResult sr)273 private void anqpRequestFinish(ScanResult sr) { 274 Log.d(TAG, "anqpRequestFinish sr.bssid=" + sr.BSSID); 275 synchronized (mAnqpRequestLock) { 276 for (ScanResult sr1 : mAnqpRequest) 277 if (sr1.BSSID.equals(sr.BSSID)) { 278 mAnqpRequest.remove(sr1); 279 break; 280 } 281 } 282 } 283 284 class PasspointHandler extends Handler { PasspointHandler(Looper looper)285 PasspointHandler(Looper looper) { 286 super(looper); 287 } 288 289 @Override handleMessage(Message message)290 public void handleMessage(Message message) { 291 Object listener = null; 292 293 switch (message.what) { 294 case AsyncChannel.CMD_CHANNEL_DISCONNECTED: 295 if (mChannelListener != null) { 296 mChannelListener.onChannelDisconnected(); 297 mChannelListener = null; 298 } 299 break; 300 301 case REQUEST_ANQP_INFO_SUCCEEDED: 302 WifiPasspointInfo result = (WifiPasspointInfo) message.obj; 303 anqpRequestFinish(result); 304 listener = getListener(message.arg2, false); 305 if (listener != null) { 306 ((ActionListener) listener).onSuccess(); 307 } 308 break; 309 310 case REQUEST_ANQP_INFO_FAILED: 311 anqpRequestFinish((ScanResult) message.obj); 312 listener = getListener(message.arg2, false); 313 if (listener == null) 314 getListener(message.arg2, true); 315 if (listener != null) { 316 ((ActionListener) listener).onFailure(message.arg1); 317 } 318 break; 319 320 case START_OSU_SUCCEEDED: 321 listener = getListener(message.arg2, true); 322 if (listener != null) { 323 ((OsuRemListener) listener).onSuccess(); 324 } 325 break; 326 327 case START_OSU_FAILED: 328 listener = getListener(message.arg2, true); 329 if (listener != null) { 330 ((OsuRemListener) listener).onFailure(message.arg1); 331 } 332 break; 333 334 case START_OSU_BROWSER: 335 listener = peekListener(message.arg2); 336 if (listener != null) { 337 ParcelableString str = (ParcelableString) message.obj; 338 if (str == null || str.string == null) 339 ((OsuRemListener) listener).onBrowserDismiss(); 340 else 341 ((OsuRemListener) listener).onBrowserLaunch(str.string); 342 } 343 break; 344 345 default: 346 Log.d(TAG, "Ignored " + message); 347 break; 348 } 349 } 350 } 351 352 } 353 354 public static class ParcelableString implements Parcelable { 355 public String string; 356 357 @Override describeContents()358 public int describeContents() { 359 return 0; 360 } 361 362 @Override writeToParcel(Parcel out, int flags)363 public void writeToParcel(Parcel out, int flags) { 364 out.writeString(string); 365 } 366 367 public static final Parcelable.Creator<ParcelableString> CREATOR = 368 new Parcelable.Creator<ParcelableString>() { 369 @Override 370 public ParcelableString createFromParcel(Parcel in) { 371 ParcelableString ret = new ParcelableString(); 372 ret.string = in.readString(); 373 return ret; 374 } 375 @Override 376 public ParcelableString[] newArray(int size) { 377 return new ParcelableString[size]; 378 } 379 }; 380 } 381 382 private static final int BASE = Protocol.BASE_WIFI_PASSPOINT_MANAGER; 383 384 public static final int REQUEST_ANQP_INFO = BASE + 1; 385 public static final int REQUEST_ANQP_INFO_FAILED = BASE + 2; 386 public static final int REQUEST_ANQP_INFO_SUCCEEDED = BASE + 3; 387 public static final int REQUEST_OSU_ICON = BASE + 4; 388 public static final int REQUEST_OSU_ICON_FAILED = BASE + 5; 389 public static final int REQUEST_OSU_ICON_SUCCEEDED = BASE + 6; 390 public static final int START_OSU = BASE + 7; 391 public static final int START_OSU_BROWSER = BASE + 8; 392 public static final int START_OSU_FAILED = BASE + 9; 393 public static final int START_OSU_SUCCEEDED = BASE + 10; 394 395 private Context mContext; 396 IWifiPasspointManager mService; 397 398 /** 399 * TODO: doc 400 * @param context 401 * @param service 402 */ WifiPasspointManager(Context context, IWifiPasspointManager service)403 public WifiPasspointManager(Context context, IWifiPasspointManager service) { 404 mContext = context; 405 mService = service; 406 } 407 408 /** 409 * Registers the application with the framework. This function must be the 410 * first to be called before any async passpoint operations are performed. 411 * 412 * @param srcContext is the context of the source 413 * @param srcLooper is the Looper on which the callbacks are receivied 414 * @param listener for callback at loss of framework communication. Can be 415 * null. 416 * @return Channel instance that is necessary for performing any further 417 * passpoint operations 418 * 419 */ initialize(Context srcContext, Looper srcLooper, ChannelListener listener)420 public Channel initialize(Context srcContext, Looper srcLooper, ChannelListener listener) { 421 Messenger messenger = getMessenger(); 422 if (messenger == null) 423 return null; 424 425 Channel c = new Channel(srcContext, srcLooper, listener); 426 if (c.mAsyncChannel.connectSync(srcContext, c.mHandler, messenger) 427 == AsyncChannel.STATUS_SUCCESSFUL) { 428 return c; 429 } else { 430 return null; 431 } 432 } 433 434 /** 435 * STOPSHIP: temp solution, should use supplicant manager instead, check 436 * with b/13931972 437 */ getMessenger()438 public Messenger getMessenger() { 439 try { 440 return mService.getMessenger(); 441 } catch (RemoteException e) { 442 return null; 443 } 444 } 445 getPasspointState()446 public int getPasspointState() { 447 try { 448 return mService.getPasspointState(); 449 } catch (RemoteException e) { 450 return PASSPOINT_STATE_UNKNOWN; 451 } 452 } 453 requestAnqpInfo(Channel c, List<ScanResult> requested, int mask, ActionListener listener)454 public void requestAnqpInfo(Channel c, List<ScanResult> requested, int mask, 455 ActionListener listener) { 456 Log.d(TAG, "requestAnqpInfo start"); 457 Log.d(TAG, "requested.size=" + requested.size()); 458 checkChannel(c); 459 List<ScanResult> list = new ArrayList<ScanResult>(); 460 for (ScanResult sr : requested) 461 if (sr.capabilities.contains("[HS20]")) { 462 list.add(sr); 463 c.anqpRequestStart(sr); 464 Log.d(TAG, "adding " + sr.BSSID); 465 } 466 int count = list.size(); 467 Log.d(TAG, "after filter, count=" + count); 468 if (count == 0) { 469 if (DBG) 470 Log.d(TAG, "ANQP info request contains no HS20 APs, skipped"); 471 listener.onSuccess(); 472 return; 473 } 474 int key = c.putListener(listener, count); 475 for (ScanResult sr : list) 476 c.mAsyncChannel.sendMessage(REQUEST_ANQP_INFO, mask, key, sr); 477 Log.d(TAG, "requestAnqpInfo end"); 478 } 479 requestOsuIcons(Channel c, List<WifiPasspointOsuProvider> requested, int resolution, ActionListener listener)480 public void requestOsuIcons(Channel c, List<WifiPasspointOsuProvider> requested, 481 int resolution, ActionListener listener) { 482 } 483 requestCredentialMatch(List<ScanResult> requested)484 public List<WifiPasspointPolicy> requestCredentialMatch(List<ScanResult> requested) { 485 try { 486 return mService.requestCredentialMatch(requested); 487 } catch (RemoteException e) { 488 return null; 489 } 490 } 491 492 /** 493 * Get a list of saved Passpoint credentials. Only those credentials owned 494 * by the caller will be returned. 495 * 496 * @return The list of credentials 497 */ getCredentials()498 public List<WifiPasspointCredential> getCredentials() { 499 try { 500 return mService.getCredentials(); 501 } catch (RemoteException e) { 502 return null; 503 } 504 } 505 506 /** 507 * Add a new Passpoint credential. 508 * 509 * @param cred The credential to be added 510 * @return {@code true} if the operation succeeds, {@code false} otherwise 511 */ addCredential(WifiPasspointCredential cred)512 public boolean addCredential(WifiPasspointCredential cred) { 513 try { 514 return mService.addCredential(cred); 515 } catch (RemoteException e) { 516 return false; 517 } 518 } 519 520 /** 521 * Update an existing Passpoint credential. Only system or the owner of this 522 * credential has the permission to do this. 523 * 524 * @param cred The credential to be updated 525 * @return {@code true} if the operation succeeds, {@code false} otherwise 526 */ updateCredential(WifiPasspointCredential cred)527 public boolean updateCredential(WifiPasspointCredential cred) { 528 try { 529 return mService.updateCredential(cred); 530 } catch (RemoteException e) { 531 return false; 532 } 533 } 534 535 /** 536 * Remove an existing Passpoint credential. Only system or the owner of this 537 * credential has the permission to do this. 538 * 539 * @param cred The credential to be removed 540 * @return {@code true} if the operation succeeds, {@code false} otherwise 541 */ removeCredential(WifiPasspointCredential cred)542 public boolean removeCredential(WifiPasspointCredential cred) { 543 try { 544 return mService.removeCredential(cred); 545 } catch (RemoteException e) { 546 return false; 547 } 548 } 549 startOsu(Channel c, WifiPasspointOsuProvider osu, OsuRemListener listener)550 public void startOsu(Channel c, WifiPasspointOsuProvider osu, OsuRemListener listener) { 551 Log.d(TAG, "startOsu start"); 552 checkChannel(c); 553 int key = c.putListener(listener); 554 c.mAsyncChannel.sendMessage(START_OSU, 0, key, osu); 555 Log.d(TAG, "startOsu end"); 556 } 557 startRemediation(Channel c, OsuRemListener listener)558 public void startRemediation(Channel c, OsuRemListener listener) { 559 } 560 connect(WifiPasspointPolicy policy)561 public void connect(WifiPasspointPolicy policy) { 562 } 563 checkChannel(Channel c)564 private static void checkChannel(Channel c) { 565 if (c == null) throw new IllegalArgumentException("Channel needs to be initialized"); 566 } 567 } 568