Home | History | Annotate | Download | only in nsd
      1 /*
      2  * Copyright (C) 2012 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.nsd;
     18 
     19 import android.annotation.SdkConstant;
     20 import android.annotation.SdkConstant.SdkConstantType;
     21 import android.content.Context;
     22 import android.os.Binder;
     23 import android.os.IBinder;
     24 import android.os.Handler;
     25 import android.os.HandlerThread;
     26 import android.os.Looper;
     27 import android.os.Message;
     28 import android.os.RemoteException;
     29 import android.os.Messenger;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 import android.util.SparseArray;
     33 
     34 import java.util.concurrent.CountDownLatch;
     35 
     36 import com.android.internal.util.AsyncChannel;
     37 import com.android.internal.util.Protocol;
     38 
     39 /**
     40  * The Network Service Discovery Manager class provides the API to discover services
     41  * on a network. As an example, if device A and device B are connected over a Wi-Fi
     42  * network, a game registered on device A can be discovered by a game on device
     43  * B. Another example use case is an application discovering printers on the network.
     44  *
     45  * <p> The API currently supports DNS based service discovery and discovery is currently
     46  * limited to a local network over Multicast DNS. DNS service discovery is described at
     47  * http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt
     48  *
     49  * <p> The API is asynchronous and responses to requests from an application are on listener
     50  * callbacks on a seperate thread.
     51  *
     52  * <p> There are three main operations the API supports - registration, discovery and resolution.
     53  * <pre>
     54  *                          Application start
     55  *                                 |
     56  *                                 |
     57  *                                 |                  onServiceRegistered()
     58  *                     Register any local services  /
     59  *                      to be advertised with       \
     60  *                       registerService()            onRegistrationFailed()
     61  *                                 |
     62  *                                 |
     63  *                          discoverServices()
     64  *                                 |
     65  *                      Maintain a list to track
     66  *                        discovered services
     67  *                                 |
     68  *                                 |--------->
     69  *                                 |          |
     70  *                                 |      onServiceFound()
     71  *                                 |          |
     72  *                                 |     add service to list
     73  *                                 |          |
     74  *                                 |<----------
     75  *                                 |
     76  *                                 |--------->
     77  *                                 |          |
     78  *                                 |      onServiceLost()
     79  *                                 |          |
     80  *                                 |   remove service from list
     81  *                                 |          |
     82  *                                 |<----------
     83  *                                 |
     84  *                                 |
     85  *                                 | Connect to a service
     86  *                                 | from list ?
     87  *                                 |
     88  *                          resolveService()
     89  *                                 |
     90  *                         onServiceResolved()
     91  *                                 |
     92  *                     Establish connection to service
     93  *                     with the host and port information
     94  *
     95  * </pre>
     96  * An application that needs to advertise itself over a network for other applications to
     97  * discover it can do so with a call to {@link #registerService}. If Example is a http based
     98  * application that can provide HTML data to peer services, it can register a name "Example"
     99  * with service type "_http._tcp". A successful registration is notified with a callback to
    100  * {@link RegistrationListener#onServiceRegistered} and a failure to register is notified
    101  * over {@link RegistrationListener#onRegistrationFailed}
    102  *
    103  * <p> A peer application looking for http services can initiate a discovery for "_http._tcp"
    104  * with a call to {@link #discoverServices}. A service found is notified with a callback
    105  * to {@link DiscoveryListener#onServiceFound} and a service lost is notified on
    106  * {@link DiscoveryListener#onServiceLost}.
    107  *
    108  * <p> Once the peer application discovers the "Example" http srevice, and needs to receive data
    109  * from the "Example" application, it can initiate a resolve with {@link #resolveService} to
    110  * resolve the host and port details for the purpose of establishing a connection. A successful
    111  * resolve is notified on {@link ResolveListener#onServiceResolved} and a failure is notified
    112  * on {@link ResolveListener#onResolveFailed}.
    113  *
    114  * Applications can reserve for a service type at
    115  * http://www.iana.org/form/ports-service. Existing services can be found at
    116  * http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml
    117  *
    118  * Get an instance of this class by calling {@link android.content.Context#getSystemService(String)
    119  * Context.getSystemService(Context.NSD_SERVICE)}.
    120  *
    121  * {@see NsdServiceInfo}
    122  */
    123 public final class NsdManager {
    124     private static final String TAG = "NsdManager";
    125     INsdManager mService;
    126 
    127     /**
    128      * Broadcast intent action to indicate whether network service discovery is
    129      * enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state
    130      * information as int.
    131      *
    132      * @see #EXTRA_NSD_STATE
    133      */
    134     @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
    135     public static final String ACTION_NSD_STATE_CHANGED =
    136         "android.net.nsd.STATE_CHANGED";
    137 
    138     /**
    139      * The lookup key for an int that indicates whether network service discovery is enabled
    140      * or disabled. Retrieve it with {@link android.content.Intent#getIntExtra(String,int)}.
    141      *
    142      * @see #NSD_STATE_DISABLED
    143      * @see #NSD_STATE_ENABLED
    144      */
    145     public static final String EXTRA_NSD_STATE = "nsd_state";
    146 
    147     /**
    148      * Network service discovery is disabled
    149      *
    150      * @see #ACTION_NSD_STATE_CHANGED
    151      */
    152     public static final int NSD_STATE_DISABLED = 1;
    153 
    154     /**
    155      * Network service discovery is enabled
    156      *
    157      * @see #ACTION_NSD_STATE_CHANGED
    158      */
    159     public static final int NSD_STATE_ENABLED = 2;
    160 
    161     private static final int BASE = Protocol.BASE_NSD_MANAGER;
    162 
    163     /** @hide */
    164     public static final int DISCOVER_SERVICES                       = BASE + 1;
    165     /** @hide */
    166     public static final int DISCOVER_SERVICES_STARTED               = BASE + 2;
    167     /** @hide */
    168     public static final int DISCOVER_SERVICES_FAILED                = BASE + 3;
    169     /** @hide */
    170     public static final int SERVICE_FOUND                           = BASE + 4;
    171     /** @hide */
    172     public static final int SERVICE_LOST                            = BASE + 5;
    173 
    174     /** @hide */
    175     public static final int STOP_DISCOVERY                          = BASE + 6;
    176     /** @hide */
    177     public static final int STOP_DISCOVERY_FAILED                   = BASE + 7;
    178     /** @hide */
    179     public static final int STOP_DISCOVERY_SUCCEEDED                = BASE + 8;
    180 
    181     /** @hide */
    182     public static final int REGISTER_SERVICE                        = BASE + 9;
    183     /** @hide */
    184     public static final int REGISTER_SERVICE_FAILED                 = BASE + 10;
    185     /** @hide */
    186     public static final int REGISTER_SERVICE_SUCCEEDED              = BASE + 11;
    187 
    188     /** @hide */
    189     public static final int UNREGISTER_SERVICE                      = BASE + 12;
    190     /** @hide */
    191     public static final int UNREGISTER_SERVICE_FAILED               = BASE + 13;
    192     /** @hide */
    193     public static final int UNREGISTER_SERVICE_SUCCEEDED            = BASE + 14;
    194 
    195     /** @hide */
    196     public static final int RESOLVE_SERVICE                         = BASE + 18;
    197     /** @hide */
    198     public static final int RESOLVE_SERVICE_FAILED                  = BASE + 19;
    199     /** @hide */
    200     public static final int RESOLVE_SERVICE_SUCCEEDED               = BASE + 20;
    201 
    202     /** @hide */
    203     public static final int ENABLE                                  = BASE + 24;
    204     /** @hide */
    205     public static final int DISABLE                                 = BASE + 25;
    206 
    207     /** @hide */
    208     public static final int NATIVE_DAEMON_EVENT                     = BASE + 26;
    209 
    210     /** Dns based service discovery protocol */
    211     public static final int PROTOCOL_DNS_SD = 0x0001;
    212 
    213     private Context mContext;
    214 
    215     private static final int INVALID_LISTENER_KEY = 0;
    216     private int mListenerKey = 1;
    217     private final SparseArray mListenerMap = new SparseArray();
    218     private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<NsdServiceInfo>();
    219     private final Object mMapLock = new Object();
    220 
    221     private final AsyncChannel mAsyncChannel = new AsyncChannel();
    222     private ServiceHandler mHandler;
    223     private final CountDownLatch mConnected = new CountDownLatch(1);
    224 
    225     /**
    226      * Create a new Nsd instance. Applications use
    227      * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve
    228      * {@link android.content.Context#NSD_SERVICE Context.NSD_SERVICE}.
    229      * @param service the Binder interface
    230      * @hide - hide this because it takes in a parameter of type INsdManager, which
    231      * is a system private class.
    232      */
    233     public NsdManager(Context context, INsdManager service) {
    234         mService = service;
    235         mContext = context;
    236         init();
    237     }
    238 
    239     /**
    240      * Failures are passed with {@link RegistrationListener#onRegistrationFailed},
    241      * {@link RegistrationListener#onUnregistrationFailed},
    242      * {@link DiscoveryListener#onStartDiscoveryFailed},
    243      * {@link DiscoveryListener#onStopDiscoveryFailed} or {@link ResolveListener#onResolveFailed}.
    244      *
    245      * Indicates that the operation failed due to an internal error.
    246      */
    247     public static final int FAILURE_INTERNAL_ERROR               = 0;
    248 
    249     /**
    250      * Indicates that the operation failed because it is already active.
    251      */
    252     public static final int FAILURE_ALREADY_ACTIVE              = 3;
    253 
    254     /**
    255      * Indicates that the operation failed because the maximum outstanding
    256      * requests from the applications have reached.
    257      */
    258     public static final int FAILURE_MAX_LIMIT                   = 4;
    259 
    260     /** Interface for callback invocation for service discovery */
    261     public interface DiscoveryListener {
    262 
    263         public void onStartDiscoveryFailed(String serviceType, int errorCode);
    264 
    265         public void onStopDiscoveryFailed(String serviceType, int errorCode);
    266 
    267         public void onDiscoveryStarted(String serviceType);
    268 
    269         public void onDiscoveryStopped(String serviceType);
    270 
    271         public void onServiceFound(NsdServiceInfo serviceInfo);
    272 
    273         public void onServiceLost(NsdServiceInfo serviceInfo);
    274 
    275     }
    276 
    277     /** Interface for callback invocation for service registration */
    278     public interface RegistrationListener {
    279 
    280         public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode);
    281 
    282         public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode);
    283 
    284         public void onServiceRegistered(NsdServiceInfo serviceInfo);
    285 
    286         public void onServiceUnregistered(NsdServiceInfo serviceInfo);
    287     }
    288 
    289     /** Interface for callback invocation for service resolution */
    290     public interface ResolveListener {
    291 
    292         public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode);
    293 
    294         public void onServiceResolved(NsdServiceInfo serviceInfo);
    295     }
    296 
    297     private class ServiceHandler extends Handler {
    298         ServiceHandler(Looper looper) {
    299             super(looper);
    300         }
    301 
    302         @Override
    303         public void handleMessage(Message message) {
    304             switch (message.what) {
    305                 case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
    306                     mAsyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
    307                     return;
    308                 case AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED:
    309                     mConnected.countDown();
    310                     return;
    311                 case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
    312                     Log.e(TAG, "Channel lost");
    313                     return;
    314                 default:
    315                     break;
    316             }
    317             Object listener = getListener(message.arg2);
    318             if (listener == null) {
    319                 Log.d(TAG, "Stale key " + message.arg2);
    320                 return;
    321             }
    322             boolean listenerRemove = true;
    323             NsdServiceInfo ns = getNsdService(message.arg2);
    324             switch (message.what) {
    325                 case DISCOVER_SERVICES_STARTED:
    326                     String s = getNsdServiceInfoType((NsdServiceInfo) message.obj);
    327                     ((DiscoveryListener) listener).onDiscoveryStarted(s);
    328                     // Keep listener until stop discovery
    329                     listenerRemove = false;
    330                     break;
    331                 case DISCOVER_SERVICES_FAILED:
    332                     ((DiscoveryListener) listener).onStartDiscoveryFailed(getNsdServiceInfoType(ns),
    333                             message.arg1);
    334                     break;
    335                 case SERVICE_FOUND:
    336                     ((DiscoveryListener) listener).onServiceFound((NsdServiceInfo) message.obj);
    337                     // Keep listener until stop discovery
    338                     listenerRemove = false;
    339                     break;
    340                 case SERVICE_LOST:
    341                     ((DiscoveryListener) listener).onServiceLost((NsdServiceInfo) message.obj);
    342                     // Keep listener until stop discovery
    343                     listenerRemove = false;
    344                     break;
    345                 case STOP_DISCOVERY_FAILED:
    346                     ((DiscoveryListener) listener).onStopDiscoveryFailed(getNsdServiceInfoType(ns),
    347                             message.arg1);
    348                     break;
    349                 case STOP_DISCOVERY_SUCCEEDED:
    350                     ((DiscoveryListener) listener).onDiscoveryStopped(getNsdServiceInfoType(ns));
    351                     break;
    352                 case REGISTER_SERVICE_FAILED:
    353                     ((RegistrationListener) listener).onRegistrationFailed(ns, message.arg1);
    354                     break;
    355                 case REGISTER_SERVICE_SUCCEEDED:
    356                     ((RegistrationListener) listener).onServiceRegistered(
    357                             (NsdServiceInfo) message.obj);
    358                     // Keep listener until unregister
    359                     listenerRemove = false;
    360                     break;
    361                 case UNREGISTER_SERVICE_FAILED:
    362                     ((RegistrationListener) listener).onUnregistrationFailed(ns, message.arg1);
    363                     break;
    364                 case UNREGISTER_SERVICE_SUCCEEDED:
    365                     ((RegistrationListener) listener).onServiceUnregistered(ns);
    366                     break;
    367                 case RESOLVE_SERVICE_FAILED:
    368                     ((ResolveListener) listener).onResolveFailed(ns, message.arg1);
    369                     break;
    370                 case RESOLVE_SERVICE_SUCCEEDED:
    371                     ((ResolveListener) listener).onServiceResolved((NsdServiceInfo) message.obj);
    372                     break;
    373                 default:
    374                     Log.d(TAG, "Ignored " + message);
    375                     break;
    376             }
    377             if (listenerRemove) {
    378                 removeListener(message.arg2);
    379             }
    380         }
    381     }
    382 
    383     private int putListener(Object listener, NsdServiceInfo s) {
    384         if (listener == null) return INVALID_LISTENER_KEY;
    385         int key;
    386         synchronized (mMapLock) {
    387             do {
    388                 key = mListenerKey++;
    389             } while (key == INVALID_LISTENER_KEY);
    390             mListenerMap.put(key, listener);
    391             mServiceMap.put(key, s);
    392         }
    393         return key;
    394     }
    395 
    396     private Object getListener(int key) {
    397         if (key == INVALID_LISTENER_KEY) return null;
    398         synchronized (mMapLock) {
    399             return mListenerMap.get(key);
    400         }
    401     }
    402 
    403     private NsdServiceInfo getNsdService(int key) {
    404         synchronized (mMapLock) {
    405             return mServiceMap.get(key);
    406         }
    407     }
    408 
    409     private void removeListener(int key) {
    410         if (key == INVALID_LISTENER_KEY) return;
    411         synchronized (mMapLock) {
    412             mListenerMap.remove(key);
    413             mServiceMap.remove(key);
    414         }
    415     }
    416 
    417     private int getListenerKey(Object listener) {
    418         synchronized (mMapLock) {
    419             int valueIndex = mListenerMap.indexOfValue(listener);
    420             if (valueIndex != -1) {
    421                 return mListenerMap.keyAt(valueIndex);
    422             }
    423         }
    424         return INVALID_LISTENER_KEY;
    425     }
    426 
    427 
    428     private String getNsdServiceInfoType(NsdServiceInfo s) {
    429         if (s == null) return "?";
    430         return s.getServiceType();
    431     }
    432 
    433     /**
    434      * Initialize AsyncChannel
    435      */
    436     private void init() {
    437         final Messenger messenger = getMessenger();
    438         if (messenger == null) throw new RuntimeException("Failed to initialize");
    439         HandlerThread t = new HandlerThread("NsdManager");
    440         t.start();
    441         mHandler = new ServiceHandler(t.getLooper());
    442         mAsyncChannel.connect(mContext, mHandler, messenger);
    443         try {
    444             mConnected.await();
    445         } catch (InterruptedException e) {
    446             Log.e(TAG, "interrupted wait at init");
    447         }
    448     }
    449 
    450     /**
    451      * Register a service to be discovered by other services.
    452      *
    453      * <p> The function call immediately returns after sending a request to register service
    454      * to the framework. The application is notified of a success to initiate
    455      * discovery through the callback {@link RegistrationListener#onServiceRegistered} or a failure
    456      * through {@link RegistrationListener#onRegistrationFailed}.
    457      *
    458      * @param serviceInfo The service being registered
    459      * @param protocolType The service discovery protocol
    460      * @param listener The listener notifies of a successful registration and is used to
    461      * unregister this service through a call on {@link #unregisterService}. Cannot be null.
    462      */
    463     public void registerService(NsdServiceInfo serviceInfo, int protocolType,
    464             RegistrationListener listener) {
    465         if (TextUtils.isEmpty(serviceInfo.getServiceName()) ||
    466                 TextUtils.isEmpty(serviceInfo.getServiceType())) {
    467             throw new IllegalArgumentException("Service name or type cannot be empty");
    468         }
    469         if (serviceInfo.getPort() <= 0) {
    470             throw new IllegalArgumentException("Invalid port number");
    471         }
    472         if (listener == null) {
    473             throw new IllegalArgumentException("listener cannot be null");
    474         }
    475         if (protocolType != PROTOCOL_DNS_SD) {
    476             throw new IllegalArgumentException("Unsupported protocol");
    477         }
    478         mAsyncChannel.sendMessage(REGISTER_SERVICE, 0, putListener(listener, serviceInfo),
    479                 serviceInfo);
    480     }
    481 
    482     /**
    483      * Unregister a service registered through {@link #registerService}. A successful
    484      * unregister is notified to the application with a call to
    485      * {@link RegistrationListener#onServiceUnregistered}.
    486      *
    487      * @param listener This should be the listener object that was passed to
    488      * {@link #registerService}. It identifies the service that should be unregistered
    489      * and notifies of a successful unregistration.
    490      */
    491     public void unregisterService(RegistrationListener listener) {
    492         int id = getListenerKey(listener);
    493         if (id == INVALID_LISTENER_KEY) {
    494             throw new IllegalArgumentException("listener not registered");
    495         }
    496         if (listener == null) {
    497             throw new IllegalArgumentException("listener cannot be null");
    498         }
    499         mAsyncChannel.sendMessage(UNREGISTER_SERVICE, 0, id);
    500     }
    501 
    502     /**
    503      * Initiate service discovery to browse for instances of a service type. Service discovery
    504      * consumes network bandwidth and will continue until the application calls
    505      * {@link #stopServiceDiscovery}.
    506      *
    507      * <p> The function call immediately returns after sending a request to start service
    508      * discovery to the framework. The application is notified of a success to initiate
    509      * discovery through the callback {@link DiscoveryListener#onDiscoveryStarted} or a failure
    510      * through {@link DiscoveryListener#onStartDiscoveryFailed}.
    511      *
    512      * <p> Upon successful start, application is notified when a service is found with
    513      * {@link DiscoveryListener#onServiceFound} or when a service is lost with
    514      * {@link DiscoveryListener#onServiceLost}.
    515      *
    516      * <p> Upon failure to start, service discovery is not active and application does
    517      * not need to invoke {@link #stopServiceDiscovery}
    518      *
    519      * @param serviceType The service type being discovered. Examples include "_http._tcp" for
    520      * http services or "_ipp._tcp" for printers
    521      * @param protocolType The service discovery protocol
    522      * @param listener  The listener notifies of a successful discovery and is used
    523      * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
    524      * Cannot be null.
    525      */
    526     public void discoverServices(String serviceType, int protocolType, DiscoveryListener listener) {
    527         if (listener == null) {
    528             throw new IllegalArgumentException("listener cannot be null");
    529         }
    530         if (TextUtils.isEmpty(serviceType)) {
    531             throw new IllegalArgumentException("Service type cannot be empty");
    532         }
    533 
    534         if (protocolType != PROTOCOL_DNS_SD) {
    535             throw new IllegalArgumentException("Unsupported protocol");
    536         }
    537 
    538         NsdServiceInfo s = new NsdServiceInfo();
    539         s.setServiceType(serviceType);
    540         mAsyncChannel.sendMessage(DISCOVER_SERVICES, 0, putListener(listener, s), s);
    541     }
    542 
    543     /**
    544      * Stop service discovery initiated with {@link #discoverServices}. An active service
    545      * discovery is notified to the application with {@link DiscoveryListener#onDiscoveryStarted}
    546      * and it stays active until the application invokes a stop service discovery. A successful
    547      * stop is notified to with a call to {@link DiscoveryListener#onDiscoveryStopped}.
    548      *
    549      * <p> Upon failure to stop service discovery, application is notified through
    550      * {@link DiscoveryListener#onStopDiscoveryFailed}.
    551      *
    552      * @param listener This should be the listener object that was passed to {@link #discoverServices}.
    553      * It identifies the discovery that should be stopped and notifies of a successful stop.
    554      */
    555     public void stopServiceDiscovery(DiscoveryListener listener) {
    556         int id = getListenerKey(listener);
    557         if (id == INVALID_LISTENER_KEY) {
    558             throw new IllegalArgumentException("service discovery not active on listener");
    559         }
    560         if (listener == null) {
    561             throw new IllegalArgumentException("listener cannot be null");
    562         }
    563         mAsyncChannel.sendMessage(STOP_DISCOVERY, 0, id);
    564     }
    565 
    566     /**
    567      * Resolve a discovered service. An application can resolve a service right before
    568      * establishing a connection to fetch the IP and port details on which to setup
    569      * the connection.
    570      *
    571      * @param serviceInfo service to be resolved
    572      * @param listener to receive callback upon success or failure. Cannot be null.
    573      */
    574     public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) {
    575         if (TextUtils.isEmpty(serviceInfo.getServiceName()) ||
    576                 TextUtils.isEmpty(serviceInfo.getServiceType())) {
    577             throw new IllegalArgumentException("Service name or type cannot be empty");
    578         }
    579         if (listener == null) {
    580             throw new IllegalArgumentException("listener cannot be null");
    581         }
    582         mAsyncChannel.sendMessage(RESOLVE_SERVICE, 0, putListener(listener, serviceInfo),
    583                 serviceInfo);
    584     }
    585 
    586     /** Internal use only @hide */
    587     public void setEnabled(boolean enabled) {
    588         try {
    589             mService.setEnabled(enabled);
    590         } catch (RemoteException e) { }
    591     }
    592 
    593     /**
    594      * Get a reference to NetworkService handler. This is used to establish
    595      * an AsyncChannel communication with the service
    596      *
    597      * @return Messenger pointing to the NetworkService handler
    598      */
    599     private Messenger getMessenger() {
    600         try {
    601             return mService.getMessenger();
    602         } catch (RemoteException e) {
    603             return null;
    604         }
    605     }
    606 }
    607