1/*
2 * Copyright (C) 2008 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
17package com.android.calendar.agenda;
18
19import android.content.Context;
20import android.database.Cursor;
21import android.graphics.Typeface;
22import android.text.TextUtils;
23import android.text.format.DateUtils;
24import android.text.format.Time;
25import android.view.LayoutInflater;
26import android.view.View;
27import android.view.ViewGroup;
28import android.widget.BaseAdapter;
29import android.widget.TextView;
30
31import com.android.calendar.R;
32import com.android.calendar.Utils;
33import com.android.calendar.agenda.AgendaWindowAdapter.DayAdapterInfo;
34
35import java.util.ArrayList;
36import java.util.Formatter;
37import java.util.Iterator;
38import java.util.LinkedList;
39import java.util.Locale;
40
41public class AgendaByDayAdapter extends BaseAdapter {
42    private static final int TYPE_DAY = 0;
43    private static final int TYPE_MEETING = 1;
44    static final int TYPE_LAST = 2;
45
46    private final Context mContext;
47    private final AgendaAdapter mAgendaAdapter;
48    private final LayoutInflater mInflater;
49    private ArrayList<RowInfo> mRowInfo;
50    private int mTodayJulianDay;
51    private Time mTmpTime;
52    private String mTimeZone;
53    // Note: Formatter is not thread safe. Fine for now as it is only used by the main thread.
54    private final Formatter mFormatter;
55    private final StringBuilder mStringBuilder;
56
57    static class ViewHolder {
58        TextView dayView;
59        TextView dateView;
60        int julianDay;
61        boolean grayed;
62    }
63
64    private final Runnable mTZUpdater = new Runnable() {
65        @Override
66        public void run() {
67            mTimeZone = Utils.getTimeZone(mContext, this);
68            mTmpTime = new Time(mTimeZone);
69            notifyDataSetChanged();
70        }
71    };
72
73    public AgendaByDayAdapter(Context context) {
74        mContext = context;
75        mAgendaAdapter = new AgendaAdapter(context, R.layout.agenda_item);
76        mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
77        mStringBuilder = new StringBuilder(50);
78        mFormatter = new Formatter(mStringBuilder, Locale.getDefault());
79        mTimeZone = Utils.getTimeZone(context, mTZUpdater);
80        mTmpTime = new Time(mTimeZone);
81    }
82
83    public long getInstanceId(int position) {
84        if (mRowInfo == null || position >= mRowInfo.size()) {
85            return -1;
86        }
87        return mRowInfo.get(position).mInstanceId;
88    }
89
90    public long getStartTime(int position) {
91        if (mRowInfo == null || position >= mRowInfo.size()) {
92            return -1;
93        }
94        return mRowInfo.get(position).mEventStartTimeMilli;
95    }
96
97
98    // Returns the position of a header of a specific item
99    public int getHeaderPosition(int position) {
100        if (mRowInfo == null || position >= mRowInfo.size()) {
101            return -1;
102        }
103
104        for (int i = position; i >=0; i --) {
105            RowInfo row = mRowInfo.get(i);
106            if (row != null && row.mType == TYPE_DAY)
107                return i;
108        }
109        return -1;
110    }
111
112    // Returns the number of items in a section defined by a specific header location
113    public int getHeaderItemsCount(int position) {
114        if (mRowInfo == null) {
115            return -1;
116        }
117        int count = 0;
118        for (int i = position +1; i < mRowInfo.size(); i++) {
119            if (mRowInfo.get(i).mType != TYPE_MEETING) {
120                return count;
121            }
122            count ++;
123        }
124        return count;
125    }
126
127    @Override
128    public int getCount() {
129        if (mRowInfo != null) {
130            return mRowInfo.size();
131        }
132        return mAgendaAdapter.getCount();
133    }
134
135    @Override
136    public Object getItem(int position) {
137        if (mRowInfo != null) {
138            RowInfo row = mRowInfo.get(position);
139            if (row.mType == TYPE_DAY) {
140                return row;
141            } else {
142                return mAgendaAdapter.getItem(row.mPosition);
143            }
144        }
145        return mAgendaAdapter.getItem(position);
146    }
147
148    @Override
149    public long getItemId(int position) {
150        if (mRowInfo != null) {
151            RowInfo row = mRowInfo.get(position);
152            if (row.mType == TYPE_DAY) {
153                return -position;
154            } else {
155                return mAgendaAdapter.getItemId(row.mPosition);
156            }
157        }
158        return mAgendaAdapter.getItemId(position);
159    }
160
161    @Override
162    public int getViewTypeCount() {
163        return TYPE_LAST;
164    }
165
166    @Override
167    public int getItemViewType(int position) {
168        return mRowInfo != null && mRowInfo.size() > position ?
169                mRowInfo.get(position).mType : TYPE_DAY;
170    }
171
172    public boolean isDayHeaderView(int position) {
173        return (getItemViewType(position) == TYPE_DAY);
174    }
175
176    @Override
177    public View getView(int position, View convertView, ViewGroup parent) {
178        if ((mRowInfo == null) || (position > mRowInfo.size())) {
179            // If we have no row info, mAgendaAdapter returns the view.
180            return mAgendaAdapter.getView(position, convertView, parent);
181        }
182
183        RowInfo row = mRowInfo.get(position);
184        if (row.mType == TYPE_DAY) {
185            ViewHolder holder = null;
186            View agendaDayView = null;
187            if ((convertView != null) && (convertView.getTag() != null)) {
188                // Listview may get confused and pass in a different type of
189                // view since we keep shifting data around. Not a big problem.
190                Object tag = convertView.getTag();
191                if (tag instanceof ViewHolder) {
192                    agendaDayView = convertView;
193                    holder = (ViewHolder) tag;
194                    holder.julianDay = row.mDay;
195                }
196            }
197
198            if (holder == null) {
199                // Create a new AgendaView with a ViewHolder for fast access to
200                // views w/o calling findViewById()
201                holder = new ViewHolder();
202                agendaDayView = mInflater.inflate(R.layout.agenda_day, parent, false);
203                holder.dayView = (TextView) agendaDayView.findViewById(R.id.day);
204                holder.dateView = (TextView) agendaDayView.findViewById(R.id.date);
205                holder.julianDay = row.mDay;
206                holder.grayed = false;
207                agendaDayView.setTag(holder);
208            }
209
210            // Re-use the member variable "mTime" which is set to the local
211            // time zone.
212            // It's difficult to find and update all these adapters when the
213            // home tz changes so check it here and update if needed.
214            String tz = Utils.getTimeZone(mContext, mTZUpdater);
215            if (!TextUtils.equals(tz, mTmpTime.timezone)) {
216                mTimeZone = tz;
217                mTmpTime = new Time(tz);
218            }
219
220            // Build the text for the day of the week.
221            // Should be yesterday/today/tomorrow (if applicable) + day of the week
222
223            Time date = mTmpTime;
224            long millis = date.setJulianDay(row.mDay);
225            int flags = DateUtils.FORMAT_SHOW_WEEKDAY;
226            mStringBuilder.setLength(0);
227
228            String dayViewText = Utils.getDayOfWeekString(row.mDay, mTodayJulianDay, millis,
229                    mContext);
230
231            // Build text for the date
232            // Format should be month day
233
234            mStringBuilder.setLength(0);
235            flags = DateUtils.FORMAT_SHOW_DATE;
236            String dateViewText = DateUtils.formatDateRange(mContext, mFormatter, millis, millis,
237                    flags, mTimeZone).toString();
238
239            if (AgendaWindowAdapter.BASICLOG) {
240                dayViewText += " P:" + position;
241                dateViewText += " P:" + position;
242            }
243            holder.dayView.setText(dayViewText);
244            holder.dateView.setText(dateViewText);
245
246            // Set the background of the view, it is grayed for day that are in the past and today
247            if (row.mDay > mTodayJulianDay) {
248                agendaDayView.setBackgroundResource(R.drawable.agenda_item_bg_primary);
249                holder.grayed = false;
250            } else {
251                agendaDayView.setBackgroundResource(R.drawable.agenda_item_bg_secondary);
252                holder.grayed = true;
253            }
254            return agendaDayView;
255        } else if (row.mType == TYPE_MEETING) {
256            View itemView = mAgendaAdapter.getView(row.mPosition, convertView, parent);
257            AgendaAdapter.ViewHolder holder = ((AgendaAdapter.ViewHolder) itemView.getTag());
258            TextView title = holder.title;
259            // The holder in the view stores information from the cursor, but the cursor has no
260            // notion of multi-day event and the start time of each instance of a multi-day event
261            // is the same.  RowInfo has the correct info , so take it from there.
262            holder.startTimeMilli = row.mEventStartTimeMilli;
263            boolean allDay = holder.allDay;
264            if (AgendaWindowAdapter.BASICLOG) {
265                title.setText(title.getText() + " P:" + position);
266            } else {
267                title.setText(title.getText());
268            }
269
270            // if event in the past or started already, un-bold the title and set the background
271            if ((!allDay && row.mEventStartTimeMilli <= System.currentTimeMillis()) ||
272                    (allDay && row.mDay <= mTodayJulianDay)) {
273                itemView.setBackgroundResource(R.drawable.agenda_item_bg_secondary);
274                title.setTypeface(Typeface.DEFAULT);
275                holder.grayed = true;
276            } else {
277                itemView.setBackgroundResource(R.drawable.agenda_item_bg_primary);
278                title.setTypeface(Typeface.DEFAULT_BOLD);
279                holder.grayed = false;
280            }
281            holder.julianDay = row.mDay;
282            return itemView;
283        } else {
284            // Error
285            throw new IllegalStateException("Unknown event type:" + row.mType);
286        }
287    }
288
289    public void clearDayHeaderInfo() {
290        mRowInfo = null;
291    }
292
293    public void changeCursor(DayAdapterInfo info) {
294        calculateDays(info);
295        mAgendaAdapter.changeCursor(info.cursor);
296    }
297
298    public void calculateDays(DayAdapterInfo dayAdapterInfo) {
299        Cursor cursor = dayAdapterInfo.cursor;
300        ArrayList<RowInfo> rowInfo = new ArrayList<RowInfo>();
301        int prevStartDay = -1;
302
303        Time tempTime = new Time(mTimeZone);
304        long now = System.currentTimeMillis();
305        tempTime.set(now);
306        mTodayJulianDay = Time.getJulianDay(now, tempTime.gmtoff);
307
308        LinkedList<MultipleDayInfo> multipleDayList = new LinkedList<MultipleDayInfo>();
309        for (int position = 0; cursor.moveToNext(); position++) {
310            int startDay = cursor.getInt(AgendaWindowAdapter.INDEX_START_DAY);
311            long id = cursor.getLong(AgendaWindowAdapter.INDEX_EVENT_ID);
312            long startTime =  cursor.getLong(AgendaWindowAdapter.INDEX_BEGIN);
313            long endTime =  cursor.getLong(AgendaWindowAdapter.INDEX_END);
314            long instanceId = cursor.getLong(AgendaWindowAdapter.INDEX_INSTANCE_ID);
315            boolean allDay = cursor.getInt(AgendaWindowAdapter.INDEX_ALL_DAY) != 0;
316            if (allDay) {
317                startTime = Utils.convertAlldayUtcToLocal(tempTime, startTime, mTimeZone);
318                endTime = Utils.convertAlldayUtcToLocal(tempTime, endTime, mTimeZone);
319            }
320            // Skip over the days outside of the adapter's range
321            startDay = Math.max(startDay, dayAdapterInfo.start);
322            // Make sure event's start time is not before the start of the day
323            // (setJulianDay sets the time to 12:00am)
324            long adapterStartTime = tempTime.setJulianDay(startDay);
325            startTime = Math.max(startTime, adapterStartTime);
326
327            if (startDay != prevStartDay) {
328                // Check if we skipped over any empty days
329                if (prevStartDay == -1) {
330                    rowInfo.add(new RowInfo(TYPE_DAY, startDay));
331                } else {
332                    // If there are any multiple-day events that span the empty
333                    // range of days, then create day headers and events for
334                    // those multiple-day events.
335                    boolean dayHeaderAdded = false;
336                    for (int currentDay = prevStartDay + 1; currentDay <= startDay; currentDay++) {
337                        dayHeaderAdded = false;
338                        Iterator<MultipleDayInfo> iter = multipleDayList.iterator();
339                        while (iter.hasNext()) {
340                            MultipleDayInfo info = iter.next();
341                            // If this event has ended then remove it from the
342                            // list.
343                            if (info.mEndDay < currentDay) {
344                                iter.remove();
345                                continue;
346                            }
347
348                            // If this is the first event for the day, then
349                            // insert a day header.
350                            if (!dayHeaderAdded) {
351                                rowInfo.add(new RowInfo(TYPE_DAY, currentDay));
352                                dayHeaderAdded = true;
353                            }
354                            long nextMidnight = Utils.getNextMidnight(tempTime,
355                                    info.mEventStartTimeMilli, mTimeZone);
356
357                            long infoEndTime = (info.mEndDay == currentDay) ?
358                                    info.mEventEndTimeMilli : nextMidnight;
359                            rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition,
360                                    info.mEventId, info.mEventStartTimeMilli,
361                                    infoEndTime, info.mInstanceId, info.mAllDay));
362
363                            info.mEventStartTimeMilli = nextMidnight;
364                        }
365                    }
366
367                    // If the day header was not added for the start day, then
368                    // add it now.
369                    if (!dayHeaderAdded) {
370                        rowInfo.add(new RowInfo(TYPE_DAY, startDay));
371                    }
372                }
373                prevStartDay = startDay;
374            }
375
376            // If this event spans multiple days, then add it to the multipleDay
377            // list.
378            int endDay = cursor.getInt(AgendaWindowAdapter.INDEX_END_DAY);
379
380            // Skip over the days outside of the adapter's range
381            endDay = Math.min(endDay, dayAdapterInfo.end);
382            if (endDay > startDay) {
383                long nextMidnight = Utils.getNextMidnight(tempTime, startTime, mTimeZone);
384                multipleDayList.add(new MultipleDayInfo(position, endDay, id, nextMidnight,
385                        endTime, instanceId, allDay));
386                // Add in the event for this cursor position - since it is the start of a multi-day
387                // event, the end time is midnight
388                rowInfo.add(new RowInfo(TYPE_MEETING, startDay, position, id, startTime,
389                        nextMidnight, instanceId, allDay));
390            } else {
391                // Add in the event for this cursor position
392                rowInfo.add(new RowInfo(TYPE_MEETING, startDay, position, id, startTime, endTime,
393                        instanceId, allDay));
394            }
395        }
396
397        // There are no more cursor events but we might still have multiple-day
398        // events left.  So create day headers and events for those.
399        if (prevStartDay > 0) {
400            for (int currentDay = prevStartDay + 1; currentDay <= dayAdapterInfo.end;
401                    currentDay++) {
402                boolean dayHeaderAdded = false;
403                Iterator<MultipleDayInfo> iter = multipleDayList.iterator();
404                while (iter.hasNext()) {
405                    MultipleDayInfo info = iter.next();
406                    // If this event has ended then remove it from the
407                    // list.
408                    if (info.mEndDay < currentDay) {
409                        iter.remove();
410                        continue;
411                    }
412
413                    // If this is the first event for the day, then
414                    // insert a day header.
415                    if (!dayHeaderAdded) {
416                        rowInfo.add(new RowInfo(TYPE_DAY, currentDay));
417                        dayHeaderAdded = true;
418                    }
419                    long nextMidnight = Utils.getNextMidnight(tempTime, info.mEventStartTimeMilli,
420                            mTimeZone);
421                    long infoEndTime =
422                            (info.mEndDay == currentDay) ? info.mEventEndTimeMilli : nextMidnight;
423                    rowInfo.add(new RowInfo(TYPE_MEETING, currentDay, info.mPosition,
424                            info.mEventId, info.mEventStartTimeMilli, infoEndTime,
425                            info.mInstanceId, info.mAllDay));
426
427                    info.mEventStartTimeMilli = nextMidnight;
428                }
429            }
430        }
431        mRowInfo = rowInfo;
432    }
433
434    private static class RowInfo {
435        // mType is either a day header (TYPE_DAY) or an event (TYPE_MEETING)
436        final int mType;
437
438        final int mDay;          // Julian day
439        final int mPosition;     // cursor position (not used for TYPE_DAY)
440        // This is used to mark a day header as the first day with events that is "today"
441        // or later. This flag is used by the adapter to create a view with a visual separator
442        // between the past and the present/future
443        boolean mFirstDayAfterYesterday;
444        final long mEventId;
445        final long mEventStartTimeMilli;
446        final long mEventEndTimeMilli;
447        final long mInstanceId;
448        final boolean mAllDay;
449
450        RowInfo(int type, int julianDay, int position, long id, long startTime, long endTime,
451                long instanceId, boolean allDay) {
452            mType = type;
453            mDay = julianDay;
454            mPosition = position;
455            mEventId = id;
456            mEventStartTimeMilli = startTime;
457            mEventEndTimeMilli = endTime;
458            mFirstDayAfterYesterday = false;
459            mInstanceId = instanceId;
460            mAllDay = allDay;
461        }
462
463        RowInfo(int type, int julianDay) {
464            mType = type;
465            mDay = julianDay;
466            mPosition = 0;
467            mEventId = 0;
468            mEventStartTimeMilli = 0;
469            mEventEndTimeMilli = 0;
470            mFirstDayAfterYesterday = false;
471            mInstanceId = -1;
472            mAllDay = false;
473        }
474    }
475
476    private static class MultipleDayInfo {
477        final int mPosition;
478        final int mEndDay;
479        final long mEventId;
480        long mEventStartTimeMilli;
481        long mEventEndTimeMilli;
482        final long mInstanceId;
483        final boolean mAllDay;
484
485        MultipleDayInfo(int position, int endDay, long id, long startTime, long endTime,
486                long instanceId, boolean allDay) {
487            mPosition = position;
488            mEndDay = endDay;
489            mEventId = id;
490            mEventStartTimeMilli = startTime;
491            mEventEndTimeMilli = endTime;
492            mInstanceId = instanceId;
493            mAllDay = allDay;
494        }
495    }
496
497    /**
498     * Finds the position in the cursor of the event that best matches the time and Id.
499     * It will try to find the event that has the specified id and start time, if such event
500     * doesn't exist, it will return the event with a matching id that is closest to the start time.
501     * If the id doesn't exist, it will return the event with start time closest to the specified
502     * time.
503     * @param time - start of event in milliseconds (or any arbitrary time if event id is unknown)
504     * @param id - Event id (-1 if unknown).
505     * @return Position of event (if found) or position of nearest event according to the time.
506     *         Zero if no event found
507     */
508    public int findEventPositionNearestTime(Time time, long id) {
509        if (mRowInfo == null) {
510            return 0;
511        }
512        long millis = time.toMillis(false /* use isDst */);
513        long minDistance =  Integer.MAX_VALUE;  // some big number
514        long idFoundMinDistance =  Integer.MAX_VALUE;  // some big number
515        int minIndex = 0;
516        int idFoundMinIndex = 0;
517        int eventInTimeIndex = -1;
518        int allDayEventInTimeIndex = -1;
519        int allDayEventDay = 0;
520        int minDay = 0;
521        boolean idFound = false;
522        int len = mRowInfo.size();
523
524        // Loop through the events and find the best match
525        // 1. Event id and start time matches requested id and time
526        // 2. Event id matches and closest time
527        // 3. No event id match , time matches a all day event (midnight)
528        // 4. No event id match , time is between event start and end
529        // 5. No event id match , all day event
530        // 6. The closest event to the requested time
531
532        for (int index = 0; index < len; index++) {
533            RowInfo row = mRowInfo.get(index);
534            if (row.mType == TYPE_DAY) {
535                continue;
536            }
537
538            // Found exact match - done
539            if (row.mEventId == id) {
540                if (row.mEventStartTimeMilli == millis) {
541                    return index;
542                }
543
544                // Not an exact match, Save event index if it is the closest to time so far
545                long distance = Math.abs(millis - row.mEventStartTimeMilli);
546                if (distance < idFoundMinDistance) {
547                    idFoundMinDistance = distance;
548                    idFoundMinIndex = index;
549                }
550                idFound = true;
551            }
552            if (!idFound) {
553                // Found an event that contains the requested time
554                if (millis >= row.mEventStartTimeMilli && millis <= row.mEventEndTimeMilli) {
555                    if (row.mAllDay) {
556                        if (allDayEventInTimeIndex == -1) {
557                            allDayEventInTimeIndex = index;
558                            allDayEventDay = row.mDay;
559                        }
560                    } else if (eventInTimeIndex == -1){
561                        eventInTimeIndex = index;
562                    }
563                } else if (eventInTimeIndex == -1){
564                    // Save event index if it is the closest to time so far
565                    long distance = Math.abs(millis - row.mEventStartTimeMilli);
566                    if (distance < minDistance) {
567                        minDistance = distance;
568                        minIndex = index;
569                        minDay = row.mDay;
570                    }
571                }
572            }
573        }
574        // We didn't find an exact match so take the best matching event
575        // Closest event with the same id
576        if (idFound) {
577            return idFoundMinIndex;
578        }
579        // Event which occurs at the searched time
580        if (eventInTimeIndex != -1) {
581            return eventInTimeIndex;
582        // All day event which occurs at the same day of the searched time as long as there is
583        // no regular event at the same day
584        } else if (allDayEventInTimeIndex != -1 && minDay != allDayEventDay) {
585            return allDayEventInTimeIndex;
586        }
587        // Closest event
588        return minIndex;
589    }
590
591
592    /**
593     * Returns a flag indicating if this position is the first day after "yesterday" that has
594     * events in it.
595     *
596     * @return a flag indicating if this is the "first day after yesterday"
597     */
598    public boolean isFirstDayAfterYesterday(int position) {
599        int headerPos = getHeaderPosition(position);
600        RowInfo row = mRowInfo.get(headerPos);
601        if (row != null) {
602            return row.mFirstDayAfterYesterday;
603        }
604        return false;
605    }
606
607    /**
608     * Finds the Julian day containing the event at the given position.
609     *
610     * @param position the list position of an event
611     * @return the Julian day containing that event
612     */
613    public int findJulianDayFromPosition(int position) {
614        if (mRowInfo == null || position < 0) {
615            return 0;
616        }
617
618        int len = mRowInfo.size();
619        if (position >= len) return 0;  // no row info at this position
620
621        for (int index = position; index >= 0; index--) {
622            RowInfo row = mRowInfo.get(index);
623            if (row.mType == TYPE_DAY) {
624                return row.mDay;
625            }
626        }
627        return 0;
628    }
629
630    /**
631     * Marks the current row as the first day that has events after "yesterday".
632     * Used to mark the separation between the past and the present/future
633     *
634     * @param position in the adapter
635     */
636    public void setAsFirstDayAfterYesterday(int position) {
637        if (mRowInfo == null || position < 0 || position > mRowInfo.size()) {
638            return;
639        }
640        RowInfo row = mRowInfo.get(position);
641        row.mFirstDayAfterYesterday = true;
642    }
643
644    /**
645     * Converts a list position to a cursor position.  The list contains
646     * day headers as well as events.  The cursor contains only events.
647     *
648     * @param listPos the list position of an event
649     * @return the corresponding cursor position of that event
650     *         if the position point to day header , it will give the position of the next event
651     *         negated.
652     */
653    public int getCursorPosition(int listPos) {
654        if (mRowInfo != null && listPos >= 0) {
655            RowInfo row = mRowInfo.get(listPos);
656            if (row.mType == TYPE_MEETING) {
657                return row.mPosition;
658            } else {
659                int nextPos = listPos + 1;
660                if (nextPos < mRowInfo.size()) {
661                    nextPos = getCursorPosition(nextPos);
662                    if (nextPos >= 0) {
663                        return -nextPos;
664                    }
665                }
666            }
667        }
668        return Integer.MIN_VALUE;
669    }
670
671    @Override
672    public boolean areAllItemsEnabled() {
673        return false;
674    }
675
676    @Override
677    public boolean isEnabled(int position) {
678        if (mRowInfo != null && position < mRowInfo.size()) {
679            RowInfo row = mRowInfo.get(position);
680            return row.mType == TYPE_MEETING;
681        }
682        return true;
683    }
684}
685