Imported Upstream version 1.1.0
[platform/upstream/iotivity.git] / service / easy-setup / sampleapp / mediator / android / EasySetup / app / src / main / java / org / iotivity / service / easysetup / MainActivity.java
1 /**\r
2  * ***************************************************************\r
3  * <p/>\r
4  * Copyright 2015 Samsung Electronics All Rights Reserved.\r
5  * <p/>\r
6  * <p/>\r
7  * <p/>\r
8  * Licensed under the Apache License, Version 2.0 (the "License");\r
9  * you may not use this file except in compliance with the License.\r
10  * You may obtain a copy of the License at\r
11  * <p/>\r
12  * http://www.apache.org/licenses/LICENSE-2.0\r
13  * <p/>\r
14  * Unless required by applicable law or agreed to in writing, software\r
15  * distributed under the License is distributed on an "AS IS" BASIS,\r
16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
17  * See the License for the specific language governing permissions and\r
18  * limitations under the License.\r
19  * <p/>\r
20  * ****************************************************************\r
21  */\r
22 \r
23 package org.iotivity.service.easysetup;\r
24 \r
25 import android.app.Activity;\r
26 import android.app.AlertDialog;\r
27 import android.content.DialogInterface;\r
28 import android.content.Intent;\r
29 import android.content.SharedPreferences;\r
30 import android.database.sqlite.SQLiteDatabase;\r
31 import android.net.ConnectivityManager;\r
32 import android.net.NetworkInfo;\r
33 import android.net.wifi.WifiConfiguration;\r
34 import android.net.wifi.WifiManager;\r
35 import android.os.Bundle;\r
36 import android.os.Handler;\r
37 import android.os.Message;\r
38 import android.preference.PreferenceManager;\r
39 import android.util.Log;\r
40 import android.view.View;\r
41 import android.view.View.OnClickListener;\r
42 import android.widget.Button;\r
43 import android.widget.EditText;\r
44 import android.widget.LinearLayout;\r
45 import android.widget.ProgressBar;\r
46 import android.widget.RadioButton;\r
47 import android.widget.RelativeLayout;\r
48 import android.widget.TextView;\r
49 import android.widget.Toast;\r
50 \r
51 import org.iotivity.base.ModeType;\r
52 import org.iotivity.base.OcException;\r
53 import org.iotivity.base.OcPlatform;\r
54 import org.iotivity.base.OcProvisioning;\r
55 import org.iotivity.base.PlatformConfig;\r
56 import org.iotivity.base.QualityOfService;\r
57 import org.iotivity.base.ServiceType;\r
58 import org.iotivity.service.easysetup.mediator.EasySetupService;\r
59 import org.iotivity.service.easysetup.mediator.EasySetupStatus;\r
60 import org.iotivity.service.easysetup.mediator.EnrolleeDevice;\r
61 import org.iotivity.service.easysetup.mediator.IpOnBoardingConnection;\r
62 import org.iotivity.service.easysetup.mediator.EnrolleeDeviceFactory;\r
63 import org.iotivity.service.easysetup.mediator.WiFiOnBoardingConfig;\r
64 import org.iotivity.service.easysetup.mediator.WiFiProvConfig;\r
65 \r
66 import java.io.File;\r
67 import java.io.FileNotFoundException;\r
68 import java.io.FileOutputStream;\r
69 import java.io.IOException;\r
70 import java.io.InputStream;\r
71 import java.io.OutputStream;\r
72 \r
73 \r
74 public class MainActivity extends Activity {\r
75     private static final String TAG = "Easysetup Mediator: ";\r
76 \r
77     /* Status to update the UI */\r
78     public static final int SUCCESS       = 0;\r
79     public static final int FAILED        = 1;\r
80     public static final int STATE_CHANGED = 2;\r
81 \r
82     public static final String OIC_CLIENT_JSON_DB_FILE =  "oic_svr_db_client.dat";\r
83     public static final String OIC_SQL_DB_FILE =  "PDM.db";\r
84 \r
85     private static final int BUFFER_SIZE = 1024;\r
86     private String filePath = "";\r
87     private boolean isSecurityEnabled = false;\r
88     //create platform config\r
89     PlatformConfig cfg;\r
90 \r
91 \r
92     String                  mSoftAPSsid;\r
93     String                  mSoftAPPassword;\r
94     String                  mEnrollerSsid;\r
95     String                  mEnrollerPassword;\r
96 \r
97     EditText                mSoftAPSsidText;\r
98     EditText                mSoftAPPassText;\r
99     EditText                mEnrollerSsidText;\r
100     EditText                mEnrollerPasswordPassText;\r
101 \r
102     TextView                mDeviceIpTextView;\r
103     TextView                mDeviceMacTextView;\r
104     TextView                mResultTextView;\r
105 \r
106     ProgressBar             mProgressbar;\r
107 \r
108     Button                  mStartButton;\r
109     Button                  mStopButton;\r
110 \r
111     RadioButton             mEnrollee;\r
112     RadioButton             mMediator;\r
113     RadioButton             mEnableSecurity;\r
114 \r
115     LinearLayout            mSoftAP;\r
116     RelativeLayout          mDeviceInfo;\r
117     TextView                mDeviceText;\r
118 \r
119     Handler                 mHandler      = new ThreadHandler();\r
120 \r
121     /**\r
122      * Objects to be instantiated by the programmer\r
123      */\r
124     WiFiProvConfig          mWiFiProvConfig;\r
125     WiFiOnBoardingConfig    mWiFiOnBoardingConfig;\r
126     EasySetupService        mEasySetupService;\r
127     EnrolleeDeviceFactory   mDeviceFactory;\r
128     EnrolleeDevice          mDevice;\r
129 \r
130     @Override\r
131     protected void onCreate(Bundle savedInstanceState) {\r
132         super.onCreate(savedInstanceState);\r
133         setContentView(R.layout.activity_main);\r
134 \r
135         /*\r
136          * Initialize widgets to get user input for target network's SSID &\r
137          * password\r
138          */\r
139         mSoftAP = (LinearLayout) findViewById(R.id.softAP);\r
140         mDeviceInfo = (RelativeLayout) findViewById(R.id.deviceInfo);\r
141         mDeviceText = (TextView) findViewById(R.id.textViewDeviceinfo);\r
142 \r
143         mSoftAPSsidText = (EditText) findViewById(R.id.ssid);\r
144         mSoftAPPassText = (EditText) findViewById(R.id.password);\r
145         mEnrollerSsidText = (EditText) findViewById(R.id.enrolleeSsid);\r
146         mEnrollerPasswordPassText = (EditText) findViewById(R.id.enrolleePass);\r
147         mDeviceIpTextView = (TextView) findViewById(R.id.ipAddr);\r
148         mDeviceMacTextView = (TextView) findViewById(R.id.hardAddr);\r
149 \r
150         mResultTextView = (TextView) findViewById(R.id.status);\r
151         mProgressbar = (ProgressBar) findViewById(R.id.progressBar);\r
152 \r
153         mEnrollee = (RadioButton) findViewById(R.id.enrollee);\r
154         mMediator = (RadioButton) findViewById(R.id.mediator);\r
155         mEnableSecurity = (RadioButton) findViewById(R.id.enablesecurity);\r
156 \r
157         mStartButton = (Button) findViewById(R.id.startSetup);\r
158 \r
159         mEnrollee.setChecked(false);\r
160         mMediator.setChecked(false);\r
161         mStartButton.setEnabled(false);\r
162 \r
163         /* Create EnrolleeDevice Factory instance */\r
164         mDeviceFactory = EnrolleeDeviceFactory\r
165                 .newInstance(getApplicationContext());\r
166 \r
167         addListenerForStartAP();\r
168         addListenerForStopAP();\r
169 \r
170         // default values for target network\r
171         mEnrollerSsidText.setText(R.string.target_default_ssid);\r
172         mEnrollerPasswordPassText.setText(R.string.target_default_pwd);\r
173         mSoftAPSsidText.setEnabled(false);\r
174         mSoftAPPassText.setEnabled(false);\r
175 \r
176         mEnrollee.setOnClickListener(new OnClickListener() {\r
177             @Override\r
178             public void onClick(View v) {\r
179 \r
180                 mMediator.setChecked(false);\r
181                 AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(\r
182                         MainActivity.this);\r
183                 alertDialogBuilder.setTitle("Network selection");\r
184                 alertDialogBuilder\r
185                         .setMessage("Proceed to select the network!")\r
186                         .setCancelable(false)\r
187                         .setPositiveButton("Yes",\r
188                                 new DialogInterface.OnClickListener() {\r
189                                     public void onClick(DialogInterface dialog,\r
190                                                         int id) {\r
191                                         MainActivity.this\r
192                                                 .startActivity(new Intent(\r
193                                                         WifiManager.ACTION_PICK_WIFI_NETWORK));\r
194 \r
195                                         mStartButton.setEnabled(true);\r
196                                         mSoftAP.setVisibility(View.INVISIBLE);\r
197                                         mDeviceInfo\r
198                                                 .setVisibility(View.INVISIBLE);\r
199                                         mDeviceText\r
200                                                 .setVisibility(View.INVISIBLE);\r
201                                     }\r
202                                 })\r
203                         .setNegativeButton("No",\r
204                                 new DialogInterface.OnClickListener() {\r
205                                     public void onClick(DialogInterface dialog,\r
206                                                         int id) {\r
207                                         mEnrollee.setChecked(false);\r
208                                         mStartButton.setEnabled(false);\r
209                                         dialog.cancel();\r
210                                     }\r
211                                 });\r
212                 alertDialogBuilder.create().show();\r
213             }\r
214         });\r
215 \r
216         mMediator.setOnClickListener(new OnClickListener() {\r
217             @Override\r
218             public void onClick(View v) {\r
219                 mStartButton.setEnabled(true);\r
220                 mEnrollee.setChecked(false);\r
221                 mSoftAPSsidText.setEnabled(false);\r
222                 mSoftAPPassText.setEnabled(false);\r
223                 mSoftAPSsidText.setText(R.string.softAP_ssid);\r
224                 mSoftAPPassText.setText(R.string.softAP_pwd);\r
225                 mDeviceIpTextView.setText(R.string.not_available);\r
226                 mDeviceMacTextView.setText(R.string.not_available);\r
227                 mResultTextView.setText(R.string.not_started);\r
228                 mSoftAP.setVisibility(View.VISIBLE);\r
229                 mDeviceInfo.setVisibility(View.VISIBLE);\r
230                 mDeviceText.setVisibility(View.VISIBLE);\r
231             }\r
232         });\r
233 \r
234         mEnableSecurity.setOnClickListener(new OnClickListener() {\r
235             @Override\r
236             public void onClick(View v) {\r
237                 filePath = getFilesDir().getPath() + "/";\r
238                 if (isSecurityEnabled) {\r
239                     isSecurityEnabled = false;\r
240                     mEnableSecurity.setChecked(false);\r
241                 }\r
242                 else {\r
243                     isSecurityEnabled = true;\r
244                     mEnableSecurity.setChecked(true);\r
245                 }\r
246                 //copy json when application runs first time\r
247                 SharedPreferences wmbPreference = PreferenceManager.getDefaultSharedPreferences\r
248                         (getApplicationContext());\r
249                 boolean isFirstRun = wmbPreference.getBoolean("FIRSTRUN", true);\r
250                 if (isFirstRun) {\r
251                     copyJsonFromAsset();\r
252                     SharedPreferences.Editor editor = wmbPreference.edit();\r
253                     editor.putBoolean("FIRSTRUN", false);\r
254                     editor.commit();\r
255                 }\r
256 \r
257                 initOICStack();\r
258             }\r
259         });\r
260         /* Create Easy Setup Service instance */\r
261         mEasySetupService = EasySetupService.getInstance(\r
262                 getApplicationContext(), new EasySetupStatus() {\r
263 \r
264                     @Override\r
265                     public void onFinished(final EnrolleeDevice enrolledevice) {\r
266                         Log.i("MainActivity", "onFinished() is received "\r
267                                 + enrolledevice.isSetupSuccessful());\r
268                         if (enrolledevice.isSetupSuccessful()) {\r
269                             mHandler.sendEmptyMessage(SUCCESS);\r
270                         } else {\r
271                             mHandler.sendEmptyMessage(FAILED);\r
272                         }\r
273                     }\r
274 \r
275                     @Override\r
276                     public void onProgress(EnrolleeDevice enrolleeDevice) {\r
277                         Log.i("MainActivity", "onProgress() is received ");\r
278                         mHandler.sendEmptyMessage(STATE_CHANGED);\r
279                     }\r
280                 });\r
281     }\r
282 \r
283     /**\r
284      * configure OIC platform and call findResource\r
285      */\r
286     private void initOICStack() {\r
287         cfg = new PlatformConfig(\r
288                 this,\r
289                 ServiceType.IN_PROC,\r
290                 ModeType.CLIENT_SERVER,\r
291                 "0.0.0.0", // bind to all available interfaces\r
292                 0,\r
293                 QualityOfService.LOW, filePath + OIC_CLIENT_JSON_DB_FILE);\r
294         try {\r
295             /*\r
296              * Initialize DataBase\r
297              */\r
298 \r
299             OcPlatform.Configure(cfg);\r
300 \r
301             String sqlDbPath = getFilesDir().getAbsolutePath().replace("files", "databases") +\r
302                     File.separator;\r
303             File file = new File(sqlDbPath);\r
304             //check files directory exists\r
305             if (!(file.isDirectory())) {\r
306                 file.mkdirs();\r
307                 Log.d(TAG, "Sql db directory created at " + sqlDbPath);\r
308             }\r
309             Log.d(TAG, "Sql db directory exists at " + sqlDbPath);\r
310 \r
311             //SQLiteDatabase.openOrCreateDatabase(sqlDbPath+ OIC_SQL_DB_FILE, null);\r
312             OcProvisioning.provisionInit(sqlDbPath + OIC_SQL_DB_FILE);\r
313         } catch (OcException e) {\r
314             logMessage(TAG + "provisionInit error: " + e.getMessage());\r
315             Log.e(TAG, e.getMessage());\r
316         } catch (UnsatisfiedLinkError e) {\r
317 \r
318            // Note : Easy setup is built with SECURED = 0, but user still selects Security feature\r
319            // while running the Mediator App it couldn't find "libocprovision.so".\r
320            // As per the programmer guide, security feature should be invoked only if build is done with SECURED = 1.\r
321             Log.e(TAG, " Easy setup is built with secured  = 0, but executed with security feature");\r
322             Toast.makeText(this,"Security is not enabled [Easy setup is built with SECURED = 0]",\r
323                                                                    Toast.LENGTH_LONG).show();\r
324             mEnableSecurity.setChecked(false);\r
325         }\r
326     }\r
327     /**\r
328      * Copy svr db json file from assets folder to app data files dir\r
329      */\r
330     private void copyJsonFromAsset() {\r
331         InputStream inputStream = null;\r
332         OutputStream outputStream = null;\r
333         int length;\r
334         byte[] buffer = new byte[BUFFER_SIZE];\r
335         try {\r
336             inputStream = getAssets().open(OIC_CLIENT_JSON_DB_FILE);\r
337             File file = new File(filePath);\r
338             //check files directory exists\r
339             if (!(file.exists() && file.isDirectory())) {\r
340                 file.mkdirs();\r
341             }\r
342             outputStream = new FileOutputStream(filePath + OIC_CLIENT_JSON_DB_FILE);\r
343             while ((length = inputStream.read(buffer)) != -1) {\r
344                 outputStream.write(buffer, 0, length);\r
345             }\r
346         } catch (NullPointerException e) {\r
347             logMessage(TAG + "Null pointer exception " + e.getMessage());\r
348             Log.e(TAG, e.getMessage());\r
349         } catch (FileNotFoundException e) {\r
350             logMessage(TAG + "Json svr db file not found " + e.getMessage());\r
351             Log.e(TAG, e.getMessage());\r
352         } catch (IOException e) {\r
353             logMessage(TAG + OIC_CLIENT_JSON_DB_FILE + " file copy failed");\r
354             Log.e(TAG, e.getMessage());\r
355         } finally {\r
356             if (inputStream != null) {\r
357                 try {\r
358                     inputStream.close();\r
359                 } catch (IOException e) {\r
360                     Log.e(TAG, e.getMessage());\r
361                 }\r
362             }\r
363             if (outputStream != null) {\r
364                 try {\r
365                     outputStream.close();\r
366                 } catch (IOException e) {\r
367                     Log.e(TAG, e.getMessage());\r
368                 }\r
369             }\r
370         }\r
371     }\r
372 \r
373     public void logMessage(String text) {\r
374 \r
375     }\r
376 \r
377 \r
378     public void onDestroy() {\r
379         super.onDestroy();\r
380         /* Reset the Easy setup process */\r
381         if (mEasySetupService != null) {\r
382             mEasySetupService.finish();\r
383         }\r
384     }\r
385 \r
386     public void addListenerForStartAP() {\r
387         mStartButton = (Button) findViewById(R.id.startSetup);\r
388         mStartButton.setOnClickListener(new OnClickListener() {\r
389             @Override\r
390             public void onClick(View arg0) {\r
391                 try {\r
392                     if (mEnrollee.isChecked()) {\r
393                         // Check the wifi connectivity\r
394                         if (!isConnectedTowifi()) {\r
395                             return;\r
396                         }\r
397 \r
398                         mEnrollerSsid = mEnrollerSsidText.getText().toString();\r
399                         mEnrollerPassword = mEnrollerPasswordPassText.getText()\r
400                                 .toString();\r
401 \r
402                         mWiFiProvConfig = new WiFiProvConfig(mEnrollerSsid,\r
403                                 mEnrollerPassword);\r
404                         mWiFiProvConfig.setSecured(isSecurityEnabled);\r
405                         mDevice = mDeviceFactory\r
406                                 .newEnrolleeDevice(mWiFiProvConfig);\r
407                             Thread thread = new Thread() {\r
408                             @Override\r
409                             public void run() {\r
410                                 try {\r
411                                        mEasySetupService.startSetup(mDevice);\r
412                                 }catch (Exception e) {\r
413                                     e.printStackTrace();\r
414                                 }\r
415                             }\r
416                         };\r
417                         thread.start();\r
418                     } else {\r
419                         mSoftAPSsid = mSoftAPSsidText.getText().toString();\r
420                         mSoftAPPassword = mSoftAPPassText.getText().toString();\r
421                         mEnrollerSsid = mEnrollerSsidText.getText().toString();\r
422                         mEnrollerPassword = mEnrollerPasswordPassText.getText()\r
423                                 .toString();\r
424 \r
425                         mWiFiProvConfig = new WiFiProvConfig(mEnrollerSsid,\r
426                                 mEnrollerPassword);\r
427                         mWiFiProvConfig.setSecured(isSecurityEnabled);\r
428                         mWiFiOnBoardingConfig = new WiFiOnBoardingConfig();\r
429 \r
430                         /*\r
431                          * Provide the target credentials to be provisioned to\r
432                          * the Enrollee by Mediator\r
433                          */\r
434                         mWiFiOnBoardingConfig.setSSId("EasySetup123");\r
435                         mWiFiOnBoardingConfig.setSharedKey("EasySetup123");\r
436                         mWiFiOnBoardingConfig\r
437                                 .setAuthAlgo(WifiConfiguration.AuthAlgorithm.OPEN);\r
438                         mWiFiOnBoardingConfig\r
439                                 .setKms(WifiConfiguration.KeyMgmt.WPA_PSK);\r
440                         mDevice = mDeviceFactory.newEnrolleeDevice(\r
441                                 mWiFiProvConfig, mWiFiOnBoardingConfig);\r
442                         mEasySetupService.startSetup(mDevice);\r
443                     }\r
444                     mProgressbar.setVisibility(View.VISIBLE);\r
445                     mProgressbar.setIndeterminate(true);\r
446                     mStartButton.setEnabled(false);\r
447                     mResultTextView.setText(R.string.running);\r
448 \r
449                     // Reset Device information\r
450                     mDeviceIpTextView.setText(R.string.not_available);\r
451                     mDeviceMacTextView.setText(R.string.not_available);\r
452                     mStopButton.setEnabled(true);\r
453 \r
454                 } catch (Exception e) {\r
455                     e.printStackTrace();\r
456                 }\r
457             }\r
458         });\r
459     }\r
460 \r
461     public void addListenerForStopAP() {\r
462         mStopButton = (Button) findViewById(R.id.stopSetup);\r
463         mStopButton.setOnClickListener(new OnClickListener() {\r
464             @Override\r
465             public void onClick(View arg0) {\r
466                 mStartButton.setEnabled(true);\r
467                 mStopButton.setEnabled(false);\r
468                 mResultTextView.setText(R.string.stopped);\r
469                 mProgressbar.setIndeterminate(false);\r
470                 mProgressbar.setVisibility(View.INVISIBLE);\r
471                 try {\r
472                     Thread thread = new Thread() {\r
473                         @Override\r
474                         public void run() {\r
475                             try {\r
476                                 mEasySetupService.stopSetup(mDevice);\r
477                             }catch (Exception e) {\r
478                                 e.printStackTrace();\r
479                             }\r
480                         }\r
481                     };\r
482                     thread.start();\r
483 \r
484                 }catch(Exception e){\r
485                     Log.i("Stop setup", "Exception");\r
486                 }\r
487             }\r
488         });\r
489     }\r
490 \r
491     private boolean isConnectedTowifi() {\r
492 \r
493         AlertDialog dialog;\r
494         ConnectivityManager connManager;\r
495         NetworkInfo wifi;\r
496 \r
497         // Check the wifi connectivity\r
498         connManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);\r
499         wifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);\r
500         if (false == wifi.isConnected()) {\r
501 \r
502             AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);\r
503             dialogBuilder.setTitle("Error");\r
504             dialogBuilder\r
505                     .setMessage("WiFi is not enabled/connected! Please connect the WiFi..");\r
506             dialogBuilder.setCancelable(false);\r
507             dialogBuilder.setPositiveButton("OK",\r
508                     new DialogInterface.OnClickListener() {\r
509                         @Override\r
510                         public void onClick(DialogInterface dialog, int which) {\r
511 \r
512                             MainActivity.this.startActivity(new Intent(\r
513                                     WifiManager.ACTION_PICK_WIFI_NETWORK));\r
514 \r
515                         }\r
516                     });\r
517             dialogBuilder.setNegativeButton("Cancel",\r
518                     new DialogInterface.OnClickListener() {\r
519                         @Override\r
520                         public void onClick(DialogInterface dialog, int which) {\r
521                         }\r
522                     });\r
523 \r
524             dialog = dialogBuilder.create();\r
525             dialog.show();\r
526             return false;\r
527         }\r
528         return true;\r
529     }\r
530 \r
531     class ThreadHandler extends Handler {\r
532         @Override\r
533         public void handleMessage(Message msg) {\r
534 \r
535             switch (msg.what) {\r
536                 case SUCCESS: {\r
537 \r
538                     mProgressbar.setIndeterminate(false);\r
539                     mStopButton.setEnabled(false);\r
540                     mStartButton.setEnabled(true);\r
541                     mProgressbar.setVisibility(View.INVISIBLE);\r
542                     String resultMsg = "Device configured successfully";\r
543                     mResultTextView.setText(R.string.success);\r
544 \r
545                     /* Update device information on the Ui */\r
546                     IpOnBoardingConnection connection = (IpOnBoardingConnection) mDevice\r
547                             .getConnection();\r
548                     mDeviceIpTextView.setText(connection.getIp());\r
549                     mDeviceMacTextView.setText(connection.getHardwareAddress());\r
550 \r
551                     Toast.makeText(getApplicationContext(), resultMsg,\r
552                             Toast.LENGTH_SHORT).show();\r
553                     break;\r
554                 }\r
555                 case FAILED: {\r
556 \r
557                     mProgressbar.setIndeterminate(false);\r
558                     mStopButton.setEnabled(false);\r
559                     mStartButton.setEnabled(true);\r
560                     mProgressbar.setVisibility(View.INVISIBLE);\r
561                     String resultMsg = "Device configuration failed";\r
562                     mResultTextView.setText(R.string.failed);\r
563                     Toast.makeText(getApplicationContext(), resultMsg,\r
564                             Toast.LENGTH_SHORT).show();\r
565                     break;\r
566                 }\r
567 \r
568                 case STATE_CHANGED: {\r
569                     String resultMsg = "Device state changed";\r
570                     Toast.makeText(getApplicationContext(), resultMsg,\r
571                             Toast.LENGTH_SHORT).show();\r
572                     break;\r
573                 }\r
574             }\r
575         }\r
576     }\r
577 }\r