CalendarEventModel.java revision 667af28a8e9729e14831f3db456ff3edb2c4c29a
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17package com.android.calendar; 18 19import android.content.Context; 20import android.content.Intent; 21import android.content.SharedPreferences; 22import android.provider.Calendar.Attendees; 23import android.provider.Calendar.Calendars; 24import android.provider.Calendar.Events; 25import android.provider.Calendar.Reminders; 26import android.text.TextUtils; 27import android.util.Log; 28 29import java.io.Serializable; 30import java.util.ArrayList; 31import java.util.Collections; 32import java.util.LinkedHashMap; 33import java.util.TimeZone; 34 35/** 36 * Stores all the information needed to fill out an entry in the events table. 37 * This is a convenient way for storing information needed by the UI to write to 38 * the events table. Only fields that are important to the UI are included. 39 */ 40public class CalendarEventModel implements Serializable { 41 private static final String TAG = "CalendarEventModel"; 42 43 public static class Attendee implements Serializable { 44 @Override 45 public int hashCode() { 46 return (mEmail == null) ? 0 : mEmail.hashCode(); 47 } 48 49 @Override 50 public boolean equals(Object obj) { 51 if (this == obj) { 52 return true; 53 } 54 if (!(obj instanceof Attendee)) { 55 return false; 56 } 57 Attendee other = (Attendee) obj; 58 if (!TextUtils.equals(mEmail, other.mEmail)) { 59 return false; 60 } 61 return true; 62 } 63 64 public String mName; 65 public String mEmail; 66 public int mStatus; 67 68 public Attendee(String name, String email) { 69 mName = name; 70 mEmail = email; 71 mStatus = Attendees.ATTENDEE_STATUS_NONE; 72 } 73 74 public Attendee(String name, String email, int status) { 75 mName = name; 76 mEmail = email; 77 mStatus = status; 78 } 79 80 public String getDisplayName() { 81 if (TextUtils.isEmpty(mName)) { 82 return mEmail; 83 } else { 84 return mName; 85 } 86 } 87 } 88 89 /** 90 * A single reminder entry. 91 * 92 * Instances of the class are immutable. 93 */ 94 public static class ReminderEntry implements Comparable<ReminderEntry>, Serializable { 95 private final int mMinutes; 96 private final int mMethod; 97 98 /** 99 * Returns a new ReminderEntry, with the specified minutes and method. 100 * 101 * @param minutes Number of minutes before the start of the event that the alert will fire. 102 * @param method Type of alert ({@link Reminders#METHOD_ALERT}, etc). 103 */ 104 public static ReminderEntry valueOf(int minutes, int method) { 105 // TODO: cache common instances 106 return new ReminderEntry(minutes, method); 107 } 108 109 /** 110 * Returns a ReminderEntry, with the specified number of minutes and a default alert method. 111 * 112 * @param minutes Number of minutes before the start of the event that the alert will fire. 113 */ 114 public static ReminderEntry valueOf(int minutes) { 115 return valueOf(minutes, Reminders.METHOD_DEFAULT); 116 } 117 118 /** 119 * Constructs a new ReminderEntry. 120 * 121 * @param minutes Number of minutes before the start of the event that the alert will fire. 122 * @param method Type of alert ({@link Reminders#METHOD_ALERT}, etc). 123 */ 124 private ReminderEntry(int minutes, int method) { 125 // TODO: error-check args 126 mMinutes = minutes; 127 mMethod = method; 128 } 129 130 @Override 131 public int hashCode() { 132 return mMinutes * 10 + mMethod; 133 } 134 135 @Override 136 public boolean equals(Object obj) { 137 if (this == obj) { 138 return true; 139 } 140 if (!(obj instanceof ReminderEntry)) { 141 return false; 142 } 143 144 ReminderEntry re = (ReminderEntry) obj; 145 146 if (re.mMinutes != mMinutes) { 147 return false; 148 } 149 150 // Treat ALERT and DEFAULT as equivalent. This is useful during the "has anything 151 // "changed" test, so that if DEFAULT is present, but we don't change anything, 152 // the internal conversion of DEFAULT to ALERT doesn't force a database update. 153 return re.mMethod == mMethod || 154 (re.mMethod == Reminders.METHOD_DEFAULT && mMethod == Reminders.METHOD_ALERT) || 155 (re.mMethod == Reminders.METHOD_ALERT && mMethod == Reminders.METHOD_DEFAULT); 156 } 157 158 @Override 159 public String toString() { 160 return "ReminderEntry min=" + mMinutes + " meth=" + mMethod; 161 } 162 163 /** 164 * Comparison function for a sort ordered primarily descending by minutes, 165 * secondarily ascending by method type. 166 */ 167 public int compareTo(ReminderEntry re) { 168 if (re.mMinutes != mMinutes) { 169 return re.mMinutes - mMinutes; 170 } 171 if (re.mMethod != mMethod) { 172 return mMethod - re.mMethod; 173 } 174 return 0; 175 } 176 177 /** Returns the minutes. */ 178 public int getMinutes() { 179 return mMinutes; 180 } 181 182 /** Returns the alert method. */ 183 public int getMethod() { 184 return mMethod; 185 } 186 } 187 188 // TODO strip out fields that don't ever get used 189 /** 190 * The uri of the event in the db. This should only be null for new events. 191 */ 192 public String mUri = null; 193 public long mId = -1; 194 public long mCalendarId = -1; 195 public String mCalendarDisplayName = ""; // Make sure this is in sync with the mCalendarId 196 public int mCalendarColor = 0; 197 public int mCalendarMaxReminders; 198 public String mCalendarAllowedReminders; 199 200 public String mSyncId = null; 201 public String mSyncAccount = null; 202 public String mSyncAccountType = null; 203 204 // PROVIDER_NOTES owner account comes from the calendars table 205 public String mOwnerAccount = null; 206 public String mTitle = null; 207 public String mLocation = null; 208 public String mDescription = null; 209 public String mRrule = null; 210 public String mOrganizer = null; 211 public String mOrganizerDisplayName = null; 212 /** 213 * Read-Only - Derived from other fields 214 */ 215 public boolean mIsOrganizer = true; 216 public boolean mIsFirstEventInSeries = true; 217 218 // This should be set the same as mStart when created and is used for making changes to 219 // recurring events. It should not be updated after it is initially set. 220 public long mOriginalStart = -1; 221 public long mStart = -1; 222 223 // This should be set the same as mEnd when created and is used for making changes to 224 // recurring events. It should not be updated after it is initially set. 225 public long mOriginalEnd = -1; 226 public long mEnd = -1; 227 public String mDuration = null; 228 public String mTimezone = null; 229 public String mTimezone2 = null; 230 public boolean mAllDay = false; 231 public boolean mHasAlarm = false; 232 public boolean mTransparency = false; 233 234 // PROVIDER_NOTES How does an event not have attendee data? The owner is added 235 // as an attendee by default. 236 public boolean mHasAttendeeData = true; 237 public int mSelfAttendeeStatus = -1; 238 public int mOwnerAttendeeId = -1; 239 public String mOriginalEvent = null; 240 public Long mOriginalTime = null; 241 public Boolean mOriginalAllDay = null; 242 public boolean mGuestsCanModify = false; 243 public boolean mGuestsCanInviteOthers = false; 244 public boolean mGuestsCanSeeGuests = false; 245 246 public boolean mOrganizerCanRespond = false; 247 public int mCalendarAccessLevel = Calendars.CONTRIBUTOR_ACCESS; 248 249 // The model can't be updated with a calendar cursor until it has been 250 // updated with an event cursor. 251 public boolean mModelUpdatedWithEventCursor; 252 253 public int mVisibility = 0; 254 public ArrayList<ReminderEntry> mReminders; 255 public ArrayList<ReminderEntry> mDefaultReminders; 256 257 // PROVIDER_NOTES Using EditEventHelper the owner should not be included in this 258 // list and will instead be added by saveEvent. Is this what we want? 259 public LinkedHashMap<String, Attendee> mAttendeesList; 260 261 public CalendarEventModel() { 262 mReminders = new ArrayList<ReminderEntry>(); 263 mDefaultReminders = new ArrayList<ReminderEntry>(); 264 mAttendeesList = new LinkedHashMap<String, Attendee>(); 265 mTimezone = TimeZone.getDefault().getID(); 266 } 267 268 public CalendarEventModel(Context context) { 269 this(); 270 271 mTimezone = Utils.getTimeZone(context, null); 272 SharedPreferences prefs = GeneralPreferences.getSharedPreferences(context); 273 274 String defaultReminder = prefs.getString( 275 GeneralPreferences.KEY_DEFAULT_REMINDER, GeneralPreferences.NO_REMINDER_STRING); 276 int defaultReminderMins = Integer.parseInt(defaultReminder); 277 if (defaultReminderMins != GeneralPreferences.NO_REMINDER) { 278 // Assume all calendars allow at least one reminder. 279 mHasAlarm = true; 280 mReminders.add(ReminderEntry.valueOf(defaultReminderMins)); 281 mDefaultReminders.add(ReminderEntry.valueOf(defaultReminderMins)); 282 } 283 } 284 285 public CalendarEventModel(Context context, Intent intent) { 286 this(context); 287 288 String title = intent.getStringExtra(Events.TITLE); 289 if (title != null) { 290 mTitle = title; 291 } 292 293 String location = intent.getStringExtra(Events.EVENT_LOCATION); 294 if (location != null) { 295 mLocation = location; 296 } 297 298 String description = intent.getStringExtra(Events.DESCRIPTION); 299 if (description != null) { 300 mDescription = description; 301 } 302 303 int transparency = intent.getIntExtra(Events.AVAILABILITY, -1); 304 if (transparency != -1) { 305 mTransparency = transparency != 0; 306 } 307 308 int visibility = intent.getIntExtra(Events.ACCESS_LEVEL, -1); 309 if (visibility != -1) { 310 mVisibility = visibility; 311 } 312 313 String rrule = intent.getStringExtra(Events.RRULE); 314 if (!TextUtils.isEmpty(rrule)) { 315 mRrule = rrule; 316 } 317 } 318 319 public boolean isValid() { 320 if (mCalendarId == -1) { 321 return false; 322 } 323 if (TextUtils.isEmpty(mOwnerAccount)) { 324 return false; 325 } 326 return true; 327 } 328 329 private boolean isEmpty() { 330 if (mTitle != null && mTitle.length() > 0) { 331 return false; 332 } 333 334 if (mLocation != null && mLocation.length() > 0) { 335 return false; 336 } 337 338 if (mDescription != null && mDescription.length() > 0) { 339 return false; 340 } 341 342 return true; 343 } 344 345 public void clear() { 346 mUri = null; 347 mId = -1; 348 mCalendarId = -1; 349 350 mSyncId = null; 351 mSyncAccount = null; 352 mSyncAccountType = null; 353 mOwnerAccount = null; 354 355 mTitle = null; 356 mLocation = null; 357 mDescription = null; 358 mRrule = null; 359 mOrganizer = null; 360 mOrganizerDisplayName = null; 361 mIsOrganizer = true; 362 mIsFirstEventInSeries = true; 363 364 mOriginalStart = -1; 365 mStart = -1; 366 mOriginalEnd = -1; 367 mEnd = -1; 368 mDuration = null; 369 mTimezone = null; 370 mTimezone2 = null; 371 mAllDay = false; 372 mHasAlarm = false; 373 374 mHasAttendeeData = true; 375 mSelfAttendeeStatus = -1; 376 mOwnerAttendeeId = -1; 377 mOriginalEvent = null; 378 mOriginalTime = null; 379 mOriginalAllDay = null; 380 381 mGuestsCanModify = false; 382 mGuestsCanInviteOthers = false; 383 mGuestsCanSeeGuests = false; 384 mVisibility = 0; 385 mOrganizerCanRespond = false; 386 mCalendarAccessLevel = Calendars.CONTRIBUTOR_ACCESS; 387 mModelUpdatedWithEventCursor = false; 388 389 mReminders = new ArrayList<ReminderEntry>(); 390 mAttendeesList.clear(); 391 } 392 393 public void addAttendee(Attendee attendee) { 394 mAttendeesList.put(attendee.mEmail, attendee); 395 } 396 397 public void removeAttendee(Attendee attendee) { 398 mAttendeesList.remove(attendee.mEmail); 399 } 400 401 public String getAttendeesString() { 402 StringBuilder b = new StringBuilder(); 403 for (Attendee attendee : mAttendeesList.values()) { 404 String name = attendee.mName; 405 String email = attendee.mEmail; 406 String status = Integer.toString(attendee.mStatus); 407 b.append("name:").append(name); 408 b.append(" email:").append(email); 409 b.append(" status:").append(status); 410 } 411 return b.toString(); 412 } 413 414 @Override 415 public int hashCode() { 416 final int prime = 31; 417 int result = 1; 418 result = prime * result + (mAllDay ? 1231 : 1237); 419 result = prime * result + ((mAttendeesList == null) ? 0 : getAttendeesString().hashCode()); 420 result = prime * result + (int) (mCalendarId ^ (mCalendarId >>> 32)); 421 result = prime * result + ((mDescription == null) ? 0 : mDescription.hashCode()); 422 result = prime * result + ((mDuration == null) ? 0 : mDuration.hashCode()); 423 result = prime * result + (int) (mEnd ^ (mEnd >>> 32)); 424 result = prime * result + (mGuestsCanInviteOthers ? 1231 : 1237); 425 result = prime * result + (mGuestsCanModify ? 1231 : 1237); 426 result = prime * result + (mGuestsCanSeeGuests ? 1231 : 1237); 427 result = prime * result + (mOrganizerCanRespond ? 1231 : 1237); 428 result = prime * result + (mModelUpdatedWithEventCursor ? 1231 : 1237); 429 result = prime * result + mCalendarAccessLevel; 430 result = prime * result + (mHasAlarm ? 1231 : 1237); 431 result = prime * result + (mHasAttendeeData ? 1231 : 1237); 432 result = prime * result + (int) (mId ^ (mId >>> 32)); 433 result = prime * result + (mIsFirstEventInSeries ? 1231 : 1237); 434 result = prime * result + (mIsOrganizer ? 1231 : 1237); 435 result = prime * result + ((mLocation == null) ? 0 : mLocation.hashCode()); 436 result = prime * result + ((mOrganizer == null) ? 0 : mOrganizer.hashCode()); 437 result = prime * result + ((mOriginalAllDay == null) ? 0 : mOriginalAllDay.hashCode()); 438 result = prime * result + (int) (mOriginalEnd ^ (mOriginalEnd >>> 32)); 439 result = prime * result + ((mOriginalEvent == null) ? 0 : mOriginalEvent.hashCode()); 440 result = prime * result + (int) (mOriginalStart ^ (mOriginalStart >>> 32)); 441 result = prime * result + ((mOriginalTime == null) ? 0 : mOriginalTime.hashCode()); 442 result = prime * result + ((mOwnerAccount == null) ? 0 : mOwnerAccount.hashCode()); 443 result = prime * result + ((mReminders == null) ? 0 : mReminders.hashCode()); 444 result = prime * result + ((mRrule == null) ? 0 : mRrule.hashCode()); 445 result = prime * result + mSelfAttendeeStatus; 446 result = prime * result + mOwnerAttendeeId; 447 result = prime * result + (int) (mStart ^ (mStart >>> 32)); 448 result = prime * result + ((mSyncAccount == null) ? 0 : mSyncAccount.hashCode()); 449 result = prime * result + ((mSyncAccountType == null) ? 0 : mSyncAccountType.hashCode()); 450 result = prime * result + ((mSyncId == null) ? 0 : mSyncId.hashCode()); 451 result = prime * result + ((mTimezone == null) ? 0 : mTimezone.hashCode()); 452 result = prime * result + ((mTimezone2 == null) ? 0 : mTimezone2.hashCode()); 453 result = prime * result + ((mTitle == null) ? 0 : mTitle.hashCode()); 454 result = prime * result + (mTransparency ? 1231 : 1237); 455 result = prime * result + ((mUri == null) ? 0 : mUri.hashCode()); 456 result = prime * result + mVisibility; 457 return result; 458 } 459 460 // Autogenerated equals method 461 @Override 462 public boolean equals(Object obj) { 463 if (this == obj) { 464 return true; 465 } 466 if (obj == null) { 467 return false; 468 } 469 if (!(obj instanceof CalendarEventModel)) { 470 return false; 471 } 472 473 CalendarEventModel other = (CalendarEventModel) obj; 474 if (!checkOriginalModelFields(other)) { 475 return false; 476 } 477 478 if (mEnd != other.mEnd) { 479 return false; 480 } 481 if (mIsFirstEventInSeries != other.mIsFirstEventInSeries) { 482 return false; 483 } 484 if (mOriginalEnd != other.mOriginalEnd) { 485 return false; 486 } 487 488 if (mOriginalStart != other.mOriginalStart) { 489 return false; 490 } 491 if (mStart != other.mStart) { 492 return false; 493 } 494 495 if (mOriginalEvent == null) { 496 if (other.mOriginalEvent != null) { 497 return false; 498 } 499 } else if (!mOriginalEvent.equals(other.mOriginalEvent)) { 500 return false; 501 } 502 503 if (mRrule == null) { 504 if (other.mRrule != null) { 505 return false; 506 } 507 } else if (!mRrule.equals(other.mRrule)) { 508 return false; 509 } 510 return true; 511 } 512 513 /** 514 * Whether the event has been modified based on its original model. 515 * 516 * @param originalModel 517 * @return true if the model is unchanged, false otherwise 518 */ 519 public boolean isUnchanged(CalendarEventModel originalModel) { 520 if (this == originalModel) { 521 return true; 522 } 523 if (originalModel == null) { 524 return false; 525 } 526 527 if (!checkOriginalModelFields(originalModel)) { 528 return false; 529 } 530 if (mEnd != mOriginalEnd) { 531 return false; 532 } 533 if (mStart != mOriginalStart) { 534 return false; 535 } 536 537 if (mRrule == null) { 538 if (originalModel.mRrule != null) { 539 if (mOriginalEvent == null || !mOriginalEvent.equals(originalModel.mSyncId)) { 540 return false; 541 } 542 } 543 } else if (!mRrule.equals(originalModel.mRrule)) { 544 return false; 545 } 546 547 return true; 548 } 549 550 /** 551 * Checks against an original model for changes to an event. This covers all 552 * the fields that should remain consistent between an original event model 553 * and the new one if nothing in the event was modified. This is also the 554 * portion that overlaps with equality between two event models. 555 * 556 * @param originalModel 557 * @return true if these fields are unchanged, false otherwise 558 */ 559 protected boolean checkOriginalModelFields(CalendarEventModel originalModel) { 560 if (mAllDay != originalModel.mAllDay) { 561 return false; 562 } 563 if (mAttendeesList == null) { 564 if (originalModel.mAttendeesList != null) { 565 return false; 566 } 567 } else if (!mAttendeesList.equals(originalModel.mAttendeesList)) { 568 return false; 569 } 570 571 if (mCalendarId != originalModel.mCalendarId) { 572 return false; 573 } 574 575 if (mDescription == null) { 576 if (originalModel.mDescription != null) { 577 return false; 578 } 579 } else if (!mDescription.equals(originalModel.mDescription)) { 580 return false; 581 } 582 583 if (mDuration == null) { 584 if (originalModel.mDuration != null) { 585 return false; 586 } 587 } else if (!mDuration.equals(originalModel.mDuration)) { 588 return false; 589 } 590 591 if (mGuestsCanInviteOthers != originalModel.mGuestsCanInviteOthers) { 592 return false; 593 } 594 if (mGuestsCanModify != originalModel.mGuestsCanModify) { 595 return false; 596 } 597 if (mGuestsCanSeeGuests != originalModel.mGuestsCanSeeGuests) { 598 return false; 599 } 600 if (mOrganizerCanRespond != originalModel.mOrganizerCanRespond) { 601 return false; 602 } 603 if (mCalendarAccessLevel != originalModel.mCalendarAccessLevel) { 604 return false; 605 } 606 if (mModelUpdatedWithEventCursor != originalModel.mModelUpdatedWithEventCursor) { 607 return false; 608 } 609 if (mHasAlarm != originalModel.mHasAlarm) { 610 return false; 611 } 612 if (mHasAttendeeData != originalModel.mHasAttendeeData) { 613 return false; 614 } 615 if (mId != originalModel.mId) { 616 return false; 617 } 618 if (mIsOrganizer != originalModel.mIsOrganizer) { 619 return false; 620 } 621 622 if (mLocation == null) { 623 if (originalModel.mLocation != null) { 624 return false; 625 } 626 } else if (!mLocation.equals(originalModel.mLocation)) { 627 return false; 628 } 629 630 if (mOrganizer == null) { 631 if (originalModel.mOrganizer != null) { 632 return false; 633 } 634 } else if (!mOrganizer.equals(originalModel.mOrganizer)) { 635 return false; 636 } 637 638 if (mOriginalAllDay == null) { 639 if (originalModel.mOriginalAllDay != null) { 640 return false; 641 } 642 } else if (!mOriginalAllDay.equals(originalModel.mOriginalAllDay)) { 643 return false; 644 } 645 646 if (mOriginalTime == null) { 647 if (originalModel.mOriginalTime != null) { 648 return false; 649 } 650 } else if (!mOriginalTime.equals(originalModel.mOriginalTime)) { 651 return false; 652 } 653 654 if (mOwnerAccount == null) { 655 if (originalModel.mOwnerAccount != null) { 656 return false; 657 } 658 } else if (!mOwnerAccount.equals(originalModel.mOwnerAccount)) { 659 return false; 660 } 661 662 if (mReminders == null) { 663 if (originalModel.mReminders != null) { 664 return false; 665 } 666 } else if (!mReminders.equals(originalModel.mReminders)) { 667 return false; 668 } 669 670 if (mSelfAttendeeStatus != originalModel.mSelfAttendeeStatus) { 671 return false; 672 } 673 if (mOwnerAttendeeId != originalModel.mOwnerAttendeeId) { 674 return false; 675 } 676 if (mSyncAccount == null) { 677 if (originalModel.mSyncAccount != null) { 678 return false; 679 } 680 } else if (!mSyncAccount.equals(originalModel.mSyncAccount)) { 681 return false; 682 } 683 684 if (mSyncAccountType == null) { 685 if (originalModel.mSyncAccountType != null) { 686 return false; 687 } 688 } else if (!mSyncAccountType.equals(originalModel.mSyncAccountType)) { 689 return false; 690 } 691 692 if (mSyncId == null) { 693 if (originalModel.mSyncId != null) { 694 return false; 695 } 696 } else if (!mSyncId.equals(originalModel.mSyncId)) { 697 return false; 698 } 699 700 if (mTimezone == null) { 701 if (originalModel.mTimezone != null) { 702 return false; 703 } 704 } else if (!mTimezone.equals(originalModel.mTimezone)) { 705 return false; 706 } 707 708 if (mTimezone2 == null) { 709 if (originalModel.mTimezone2 != null) { 710 return false; 711 } 712 } else if (!mTimezone2.equals(originalModel.mTimezone2)) { 713 return false; 714 } 715 716 if (mTitle == null) { 717 if (originalModel.mTitle != null) { 718 return false; 719 } 720 } else if (!mTitle.equals(originalModel.mTitle)) { 721 return false; 722 } 723 724 if (mTransparency != originalModel.mTransparency) { 725 return false; 726 } 727 728 if (mUri == null) { 729 if (originalModel.mUri != null) { 730 return false; 731 } 732 } else if (!mUri.equals(originalModel.mUri)) { 733 return false; 734 } 735 736 if (mVisibility != originalModel.mVisibility) { 737 return false; 738 } 739 return true; 740 } 741 742 /** 743 * Sort and uniquify mReminderMinutes. 744 * 745 * @return true (for convenience of caller) 746 */ 747 public boolean normalizeReminders() { 748 if (mReminders.size() <= 1) { 749 return true; 750 } 751 752 // sort 753 Collections.sort(mReminders); 754 755 // remove duplicates 756 ReminderEntry prev = mReminders.get(mReminders.size()-1); 757 for (int i = mReminders.size()-2; i >= 0; --i) { 758 ReminderEntry cur = mReminders.get(i); 759 if (prev.equals(cur)) { 760 // match, remove later entry 761 mReminders.remove(i+1); 762 } 763 prev = cur; 764 } 765 766 return true; 767 } 768} 769