altosdroid: Add quit. Restart. Show freq in title.
[fw/altos] / altosdroid / src / org / altusmetrum / AltosDroid / AltosDroid.java
1 /*
2  * Copyright © 2012-2013 Mike Beattie <mike@ethernal.org>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 2 of the License.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11  * General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
16  */
17
18 package org.altusmetrum.AltosDroid;
19
20 import java.lang.ref.WeakReference;
21 import java.util.ArrayList;
22 import java.util.Timer;
23 import java.util.TimerTask;
24
25 import android.app.Activity;
26 import android.bluetooth.BluetoothAdapter;
27 import android.bluetooth.BluetoothDevice;
28 import android.content.Intent;
29 import android.content.Context;
30 import android.content.ComponentName;
31 import android.content.ServiceConnection;
32 import android.content.DialogInterface;
33 import android.os.IBinder;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.Message;
37 import android.os.Messenger;
38 import android.os.RemoteException;
39 import android.support.v4.app.FragmentActivity;
40 import android.support.v4.app.FragmentManager;
41 import android.util.DisplayMetrics;
42 import android.util.Log;
43 import android.view.Menu;
44 import android.view.MenuInflater;
45 import android.view.MenuItem;
46 import android.view.Window;
47 import android.view.View;
48 import android.widget.TabHost;
49 import android.widget.TextView;
50 import android.widget.RelativeLayout;
51 import android.widget.Toast;
52 import android.app.AlertDialog;
53 import android.location.Location;
54
55 import org.altusmetrum.altoslib_5.*;
56
57 public class AltosDroid extends FragmentActivity {
58         // Debugging
59         static final String TAG = "AltosDroid";
60         static final boolean D = true;
61
62         // Message types received by our Handler
63         public static final int MSG_STATE_CHANGE    = 1;
64         public static final int MSG_TELEMETRY       = 2;
65         public static final int MSG_UPDATE_AGE      = 3;
66         public static final int MSG_LOCATION        = 4;
67         public static final int MSG_CRC_ERROR       = 5;
68         public static final int MSG_FREQUENCY       = 6;
69         public static final int MSG_TELEMETRY_RATE  = 7;
70
71         // Intent request codes
72         private static final int REQUEST_CONNECT_DEVICE = 1;
73         private static final int REQUEST_ENABLE_BT      = 2;
74
75         public static FragmentManager   fm;
76
77         // Layout Views
78         private TextView mTitle;
79
80         // Flight state values
81         private TextView mCallsignView;
82         private TextView mRSSIView;
83         private TextView mSerialView;
84         private TextView mFlightView;
85         private RelativeLayout mStateLayout;
86         private TextView mStateView;
87         private TextView mAgeView;
88
89         // field to display the version at the bottom of the screen
90         private TextView mVersion;
91
92         private double frequency;
93         private int telemetry_rate;
94
95         // Tabs
96         TabHost         mTabHost;
97         AltosViewPager  mViewPager;
98         TabsAdapter     mTabsAdapter;
99         ArrayList<AltosDroidTab> mTabs = new ArrayList<AltosDroidTab>();
100         int             tabHeight;
101
102         // Timer and Saved flight state for Age calculation
103         private Timer timer = new Timer();
104         AltosState saved_state;
105         Location saved_location;
106
107         // Service
108         private boolean mIsBound   = false;
109         private Messenger mService = null;
110         final Messenger mMessenger = new Messenger(new IncomingHandler(this));
111
112         // TeleBT Config data
113         private AltosConfigData mConfigData = null;
114         // Local Bluetooth adapter
115         private BluetoothAdapter mBluetoothAdapter = null;
116
117         // Text to Speech
118         private AltosVoice mAltosVoice = null;
119
120         // The Handler that gets information back from the Telemetry Service
121         static class IncomingHandler extends Handler {
122                 private final WeakReference<AltosDroid> mAltosDroid;
123                 IncomingHandler(AltosDroid ad) { mAltosDroid = new WeakReference<AltosDroid>(ad); }
124
125                 @Override
126                 public void handleMessage(Message msg) {
127                         AltosDroid ad = mAltosDroid.get();
128                         switch (msg.what) {
129                         case MSG_STATE_CHANGE:
130                                 if(D) Log.d(TAG, "MSG_STATE_CHANGE: " + msg.arg1);
131                                 switch (msg.arg1) {
132                                 case TelemetryService.STATE_CONNECTED:
133                                         ad.set_config_data((AltosConfigData) msg.obj);
134                                         break;
135                                 case TelemetryService.STATE_CONNECTING:
136                                         ad.mTitle.setText(R.string.title_connecting);
137                                         break;
138                                 case TelemetryService.STATE_READY:
139                                 case TelemetryService.STATE_NONE:
140                                         ad.mConfigData = null;
141                                         ad.mTitle.setText(R.string.title_not_connected);
142                                         String  active_device = AltosDroidPreferences.active_device();
143                                         if (active_device != null)
144                                                 ad.connectDevice(active_device);
145                                         break;
146                                 }
147                                 break;
148                         case MSG_TELEMETRY:
149                                 ad.update_ui((AltosState) msg.obj);
150                                 break;
151                         case MSG_LOCATION:
152                                 ad.set_location((Location) msg.obj);
153                                 break;
154                         case MSG_CRC_ERROR:
155                                 break;
156                         case MSG_UPDATE_AGE:
157                                 if (ad.saved_state != null) {
158                                         ad.mAgeView.setText(String.format("%d", (System.currentTimeMillis() - ad.saved_state.received_time + 500) / 1000));
159                                 }
160                                 break;
161                         case MSG_FREQUENCY:
162                                 ad.set_frequency((Double) msg.obj);
163                                 break;
164                         case MSG_TELEMETRY_RATE:
165                                 ad.set_telemetry_rate((Integer) msg.obj);
166                                 break;
167                         }
168                 }
169         };
170
171
172         private ServiceConnection mConnection = new ServiceConnection() {
173                 public void onServiceConnected(ComponentName className, IBinder service) {
174                         mService = new Messenger(service);
175                         try {
176                                 Message msg = Message.obtain(null, TelemetryService.MSG_REGISTER_CLIENT);
177                                 msg.replyTo = mMessenger;
178                                 mService.send(msg);
179                         } catch (RemoteException e) {
180                                 // In this case the service has crashed before we could even do anything with it
181                         }
182                 }
183
184                 public void onServiceDisconnected(ComponentName className) {
185                         // This is called when the connection with the service has been unexpectedly disconnected - process crashed.
186                         mService = null;
187                 }
188         };
189
190         void doBindService() {
191                 bindService(new Intent(this, TelemetryService.class), mConnection, Context.BIND_AUTO_CREATE);
192                 mIsBound = true;
193         }
194
195         void doUnbindService() {
196                 if (mIsBound) {
197                         // If we have received the service, and hence registered with it, then now is the time to unregister.
198                         if (mService != null) {
199                                 try {
200                                         Message msg = Message.obtain(null, TelemetryService.MSG_UNREGISTER_CLIENT);
201                                         msg.replyTo = mMessenger;
202                                         mService.send(msg);
203                                 } catch (RemoteException e) {
204                                         // There is nothing special we need to do if the service has crashed.
205                                 }
206                         }
207                         // Detach our existing connection.
208                         unbindService(mConnection);
209                         mIsBound = false;
210                 }
211         }
212
213         public void registerTab(AltosDroidTab mTab) {
214                 mTabs.add(mTab);
215         }
216
217         public void unregisterTab(AltosDroidTab mTab) {
218                 mTabs.remove(mTab);
219         }
220
221         void set_location(Location location) {
222                 saved_location = location;
223                 Log.d(TAG, "set_location");
224                 update_ui(saved_state);
225         }
226
227         void set_title() {
228                 if (mConfigData != null) {
229                         String str = String.format("S/N %d %6.3f MHz", mConfigData.serial, frequency);
230
231                         if (telemetry_rate != AltosLib.ao_telemetry_rate_38400)
232                                 str = str.concat(String.format(" %d bps", AltosLib.ao_telemetry_rate_values[telemetry_rate]));
233                         mTitle.setText(str);
234                 }
235         }
236
237         void set_frequency(double frequency) {
238                 if (D) Log.d(TAG, String.format("AltosDroid: set_frequency %f\n", frequency));
239                 this.frequency = frequency;
240                 set_title();
241         }
242
243         void set_telemetry_rate(int telemetry_rate) {
244                 if (D) Log.d(TAG, String.format("AltosDroid: set_telemetry_rate %d\n", telemetry_rate));
245                 this.telemetry_rate = telemetry_rate;
246                 set_title();
247         }
248
249         void set_config_data(AltosConfigData config_data) {
250                 mConfigData = config_data;
251                 set_title();
252         }
253
254         boolean same_string(String a, String b) {
255                 if (a == null) {
256                         if (b == null)
257                                 return true;
258                         return false;
259                 } else {
260                         if (b == null)
261                                 return false;
262                         return a.equals(b);
263                 }
264         }
265
266         void update_ui(AltosState state) {
267
268                 Log.d(TAG, "update_ui");
269
270                 int prev_state = AltosLib.ao_flight_invalid;
271
272                 if (saved_state != null)
273                         prev_state = saved_state.state;
274
275                 if (state != null) {
276                         Log.d(TAG, String.format("prev state %d new state  %d\n", prev_state, state.state));
277                         if (prev_state != state.state) {
278                                 String currentTab = mTabHost.getCurrentTabTag();
279                                 Log.d(TAG, "switch state");
280                                 switch (state.state) {
281                                 case AltosLib.ao_flight_boost:
282                                         if (currentTab.equals("pad")) mTabHost.setCurrentTabByTag("ascent");
283                                         break;
284                                 case AltosLib.ao_flight_drogue:
285                                         if (currentTab.equals("ascent")) mTabHost.setCurrentTabByTag("descent");
286                                         break;
287                                 case AltosLib.ao_flight_landed:
288                                         if (currentTab.equals("descent")) mTabHost.setCurrentTabByTag("landed");
289                                         break;
290                                 case AltosLib.ao_flight_stateless:
291                                         if (currentTab.equals("pad")) mTabHost.setCurrentTabByTag("descent");
292                                         break;
293                                 }
294                         }
295                 }
296
297                 AltosGreatCircle from_receiver = null;
298
299                 if (state != null && saved_location != null && state.gps != null && state.gps.locked) {
300                         double altitude = 0;
301                         if (saved_location.hasAltitude())
302                                 altitude = saved_location.getAltitude();
303                         from_receiver = new AltosGreatCircle(saved_location.getLatitude(),
304                                                              saved_location.getLongitude(),
305                                                              altitude,
306                                                              state.gps.lat,
307                                                              state.gps.lon,
308                                                              state.gps.alt);
309                 }
310
311                 if (state != null) {
312                         if (saved_state == null || !same_string(saved_state.callsign, state.callsign)) {
313                                 Log.d(TAG, "update callsign");
314                                 mCallsignView.setText(state.callsign);
315                         }
316                         if (saved_state == null || state.serial != saved_state.serial) {
317                                 Log.d(TAG, "update serial");
318                                 mSerialView.setText(String.format("%d", state.serial));
319                         }
320                         if (saved_state == null || state.flight != saved_state.flight) {
321                                 Log.d(TAG, "update flight");
322                                 mFlightView.setText(String.format("%d", state.flight));
323                         }
324                         if (saved_state == null || state.state != saved_state.state) {
325                                 Log.d(TAG, "update state");
326                                 if (state.state == AltosLib.ao_flight_stateless) {
327                                         mStateLayout.setVisibility(View.GONE);
328                                 } else {
329                                         mStateView.setText(state.state_name());
330                                         mStateLayout.setVisibility(View.VISIBLE);
331                                 }
332                         }
333                         if (saved_state == null || state.rssi != saved_state.rssi) {
334                                 Log.d(TAG, "update rssi");
335                                 mRSSIView.setText(String.format("%d", state.rssi));
336                         }
337                 }
338
339                 for (AltosDroidTab mTab : mTabs)
340                         mTab.update_ui(state, from_receiver, saved_location, mTab == mTabsAdapter.currentItem());
341
342                 if (state != null)
343                         mAltosVoice.tell(state);
344
345                 saved_state = state;
346         }
347
348         private void onTimerTick() {
349                 try {
350                         mMessenger.send(Message.obtain(null, MSG_UPDATE_AGE));
351                 } catch (RemoteException e) {
352                 }
353         }
354
355         static String pos(double p, String pos, String neg) {
356                 String  h = pos;
357                 if (p == AltosLib.MISSING)
358                         return "";
359                 if (p < 0) {
360                         h = neg;
361                         p = -p;
362                 }
363                 int deg = (int) Math.floor(p);
364                 double min = (p - Math.floor(p)) * 60.0;
365                 return String.format("%d°%9.4f\" %s", deg, min, h);
366         }
367
368         static String number(String format, double value) {
369                 if (value == AltosLib.MISSING)
370                         return "";
371                 return String.format(format, value);
372         }
373
374         static String integer(String format, int value) {
375                 if (value == AltosLib.MISSING)
376                         return "";
377                 return String.format(format, value);
378         }
379
380         @Override
381         public void onCreate(Bundle savedInstanceState) {
382                 super.onCreate(savedInstanceState);
383                 if(D) Log.e(TAG, "+++ ON CREATE +++");
384
385                 fm = getSupportFragmentManager();
386
387                 // Get local Bluetooth adapter
388                 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
389
390                 // If the adapter is null, then Bluetooth is not supported
391                 if (mBluetoothAdapter == null) {
392                         Toast.makeText(this, "Bluetooth is not available", Toast.LENGTH_LONG).show();
393                         finish();
394                         return;
395                 }
396
397                 // Initialise preferences
398                 AltosDroidPreferences.init(this);
399
400                 // Set up the window layout
401                 requestWindowFeature(Window.FEATURE_CUSTOM_TITLE);
402                 setContentView(R.layout.altosdroid);
403                 getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.custom_title);
404
405                 // Create the Tabs and ViewPager
406                 mTabHost = (TabHost)findViewById(android.R.id.tabhost);
407                 mTabHost.setup();
408
409                 mViewPager = (AltosViewPager)findViewById(R.id.pager);
410                 mViewPager.setOffscreenPageLimit(4);
411
412                 mTabsAdapter = new TabsAdapter(this, mTabHost, mViewPager);
413
414                 mTabsAdapter.addTab(mTabHost.newTabSpec("pad").setIndicator("Pad"), TabPad.class, null);
415                 mTabsAdapter.addTab(mTabHost.newTabSpec("ascent").setIndicator("Ascent"), TabAscent.class, null);
416                 mTabsAdapter.addTab(mTabHost.newTabSpec("descent").setIndicator("Descent"), TabDescent.class, null);
417                 mTabsAdapter.addTab(mTabHost.newTabSpec("landed").setIndicator("Landed"), TabLanded.class, null);
418                 mTabsAdapter.addTab(mTabHost.newTabSpec("map").setIndicator("Map"), TabMap.class, null);
419
420
421                 // Scale the size of the Tab bar for different screen densities
422                 // This probably won't be needed when we start supporting ICS+ tabs.
423                 DisplayMetrics metrics = new DisplayMetrics();
424                 getWindowManager().getDefaultDisplay().getMetrics(metrics);
425                 int density = metrics.densityDpi;
426
427                 if (density==DisplayMetrics.DENSITY_XHIGH)
428                         tabHeight = 65;
429                 else if (density==DisplayMetrics.DENSITY_HIGH)
430                         tabHeight = 45;
431                 else if (density==DisplayMetrics.DENSITY_MEDIUM)
432                         tabHeight = 35;
433                 else if (density==DisplayMetrics.DENSITY_LOW)
434                         tabHeight = 25;
435                 else
436                         tabHeight = 65;
437
438                 for (int i = 0; i < 5; i++)
439                         mTabHost.getTabWidget().getChildAt(i).getLayoutParams().height = tabHeight;
440
441
442                 // Set up the custom title
443                 mTitle = (TextView) findViewById(R.id.title_left_text);
444                 mTitle.setText(R.string.app_name);
445                 mTitle = (TextView) findViewById(R.id.title_right_text);
446
447                 // Display the Version
448                 mVersion = (TextView) findViewById(R.id.version);
449                 mVersion.setText("Version: " + BuildInfo.version +
450                                  "  Built: " + BuildInfo.builddate + " " + BuildInfo.buildtime + " " + BuildInfo.buildtz +
451                                  "  (" + BuildInfo.branch + "-" + BuildInfo.commitnum + "-" + BuildInfo.commithash + ")");
452
453                 mCallsignView  = (TextView) findViewById(R.id.callsign_value);
454                 mRSSIView      = (TextView) findViewById(R.id.rssi_value);
455                 mSerialView    = (TextView) findViewById(R.id.serial_value);
456                 mFlightView    = (TextView) findViewById(R.id.flight_value);
457                 mStateLayout   = (RelativeLayout) findViewById(R.id.state_container);
458                 mStateView     = (TextView) findViewById(R.id.state_value);
459                 mAgeView       = (TextView) findViewById(R.id.age_value);
460
461                 timer.scheduleAtFixedRate(new TimerTask(){ public void run() {onTimerTick();}}, 1000L, 100L);
462
463                 mAltosVoice = new AltosVoice(this);
464         }
465
466         @Override
467         public void onStart() {
468                 super.onStart();
469                 if(D) Log.e(TAG, "++ ON START ++");
470
471                 if (!mBluetoothAdapter.isEnabled()) {
472                         Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
473                         startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
474                 }
475
476                 // Start Telemetry Service
477                 startService(new Intent(AltosDroid.this, TelemetryService.class));
478
479                 doBindService();
480
481         }
482
483         @Override
484         public synchronized void onResume() {
485                 super.onResume();
486                 if(D) Log.e(TAG, "+ ON RESUME +");
487         }
488
489         @Override
490         public synchronized void onPause() {
491                 super.onPause();
492                 if(D) Log.e(TAG, "- ON PAUSE -");
493         }
494
495         @Override
496         public void onStop() {
497                 super.onStop();
498                 if(D) Log.e(TAG, "-- ON STOP --");
499
500                 doUnbindService();
501         }
502
503         @Override
504         public void onDestroy() {
505                 super.onDestroy();
506                 if(D) Log.e(TAG, "--- ON DESTROY ---");
507
508                 if (mAltosVoice != null) mAltosVoice.stop();
509         }
510
511         public void onActivityResult(int requestCode, int resultCode, Intent data) {
512                 if(D) Log.d(TAG, "onActivityResult " + resultCode);
513                 switch (requestCode) {
514                 case REQUEST_CONNECT_DEVICE:
515                         // When DeviceListActivity returns with a device to connect to
516                         if (resultCode == Activity.RESULT_OK) {
517                                 connectDevice(data);
518                         }
519                         break;
520                 case REQUEST_ENABLE_BT:
521                         // When the request to enable Bluetooth returns
522                         if (resultCode == Activity.RESULT_OK) {
523                                 // Bluetooth is now enabled, so set up a chat session
524                                 //setupChat();
525                         } else {
526                                 // User did not enable Bluetooth or an error occured
527                                 Log.e(TAG, "BT not enabled");
528                                 stopService(new Intent(AltosDroid.this, TelemetryService.class));
529                                 Toast.makeText(this, R.string.bt_not_enabled, Toast.LENGTH_SHORT).show();
530                                 finish();
531                         }
532                         break;
533                 }
534         }
535
536         private void connectDevice(String address) {
537                 // Get the BLuetoothDevice object
538                 BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
539                 // Attempt to connect to the device
540                 try {
541                         if (D) Log.d(TAG, "Connecting to " + device.getName());
542                         mService.send(Message.obtain(null, TelemetryService.MSG_CONNECT, device));
543                 } catch (RemoteException e) {
544                 }
545         }
546
547         private void connectDevice(Intent data) {
548                 // Get the device MAC address
549                 String address = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
550                 AltosDroidPreferences.set_active_device(address);
551                 connectDevice(address);
552         }
553
554         @Override
555         public boolean onCreateOptionsMenu(Menu menu) {
556                 MenuInflater inflater = getMenuInflater();
557                 inflater.inflate(R.menu.option_menu, menu);
558                 return true;
559         }
560
561         void setFrequency(double freq) {
562                 try {
563                         mService.send(Message.obtain(null, TelemetryService.MSG_SETFREQUENCY, freq));
564                 } catch (RemoteException e) {
565                 }
566         }
567
568         void setFrequency(String freq) {
569                 try {
570                         setFrequency (Double.parseDouble(freq.substring(11, 17)));
571                 } catch (NumberFormatException e) {
572                 }
573         }
574
575         void setBaud(int baud) {
576                 try {
577                         mService.send(Message.obtain(null, TelemetryService.MSG_SETBAUD, baud));
578                 } catch (RemoteException e) {
579                 }
580         }
581
582         void setBaud(String baud) {
583                 try {
584                         int     value = Integer.parseInt(baud);
585                         int     rate = AltosLib.ao_telemetry_rate_38400;
586                         switch (value) {
587                         case 2400:
588                                 rate = AltosLib.ao_telemetry_rate_2400;
589                                 break;
590                         case 9600:
591                                 rate = AltosLib.ao_telemetry_rate_9600;
592                                 break;
593                         case 38400:
594                                 rate = AltosLib.ao_telemetry_rate_38400;
595                                 break;
596                         }
597                         setBaud(rate);
598                 } catch (NumberFormatException e) {
599                 }
600         }
601
602         @Override
603         public boolean onOptionsItemSelected(MenuItem item) {
604                 Intent serverIntent = null;
605                 switch (item.getItemId()) {
606                 case R.id.connect_scan:
607                         // Launch the DeviceListActivity to see devices and do scan
608                         serverIntent = new Intent(this, DeviceListActivity.class);
609                         startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);
610                         return true;
611                 case R.id.quit:
612                         Log.d(TAG, "R.id.quit");
613                         stopService(new Intent(AltosDroid.this, TelemetryService.class));
614                         finish();
615                         return true;
616                 case R.id.select_freq:
617                         // Set the TBT radio frequency
618
619                         final String[] frequencies = {
620                                 "Channel 0 (434.550MHz)",
621                                 "Channel 1 (434.650MHz)",
622                                 "Channel 2 (434.750MHz)",
623                                 "Channel 3 (434.850MHz)",
624                                 "Channel 4 (434.950MHz)",
625                                 "Channel 5 (435.050MHz)",
626                                 "Channel 6 (435.150MHz)",
627                                 "Channel 7 (435.250MHz)",
628                                 "Channel 8 (435.350MHz)",
629                                 "Channel 9 (435.450MHz)"
630                         };
631
632                         AlertDialog.Builder builder_freq = new AlertDialog.Builder(this);
633                         builder_freq.setTitle("Pick a frequency");
634                         builder_freq.setItems(frequencies,
635                                          new DialogInterface.OnClickListener() {
636                                                  public void onClick(DialogInterface dialog, int item) {
637                                                          setFrequency(frequencies[item]);
638                                                  }
639                                          });
640                         AlertDialog alert_freq = builder_freq.create();
641                         alert_freq.show();
642                         return true;
643                 case R.id.select_rate:
644                         // Set the TBT baud rate
645
646                         final String[] rates = {
647                                 "38400",
648                                 "9600",
649                                 "2400",
650                         };
651
652                         AlertDialog.Builder builder_rate = new AlertDialog.Builder(this);
653                         builder_rate.setTitle("Pick a baud rate");
654                         builder_rate.setItems(rates,
655                                          new DialogInterface.OnClickListener() {
656                                                  public void onClick(DialogInterface dialog, int item) {
657                                                          setBaud(rates[item]);
658                                                  }
659                                          });
660                         AlertDialog alert_rate = builder_rate.create();
661                         alert_rate.show();
662                         return true;
663                 }
664                 return false;
665         }
666
667 }