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