1 /* ***** BEGIN LICENSE BLOCK ***** 2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 3 * 4 * The contents of this file are subject to the Mozilla Public License Version 5 * 1.1 (the "License"); you may not use this file except in compliance with 6 * the License. You may obtain a copy of the License at 7 * http://www.mozilla.org/MPL/ 8 * 9 * Software distributed under the License is distributed on an "AS IS" basis, 10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 11 * for the specific language governing rights and limitations under the 12 * License. 13 * 14 * The Original Code is gContactSync. 15 * 16 * The Initial Developer of the Original Code is 17 * Josh Geenen <gcontactsync@pirules.org>. 18 * Portions created by the Initial Developer are Copyright (C) 2009-2010 19 * the Initial Developer. All Rights Reserved. 20 * 21 * Contributor(s): 22 * 23 * Alternatively, the contents of this file may be used under the terms of 24 * either the GNU General Public License Version 2 or later (the "GPL"), or 25 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 26 * in which case the provisions of the GPL or the LGPL are applicable instead 27 * of those above. If you wish to allow use of your version of this file only 28 * under the terms of either the GPL or the LGPL, and not to allow others to 29 * use your version of this file under the terms of the MPL, indicate your 30 * decision by deleting the provisions above and replace them with the notice 31 * and other provisions required by the GPL or the LGPL. If you do not delete 32 * the provisions above, a recipient may use your version of this file under 33 * the terms of any one of the MPL, the GPL or the LGPL. 34 * 35 * ***** END LICENSE BLOCK ***** */ 36 37 if (!com) var com = {}; // A generic wrapper variable 38 // A wrapper for all GCS functions and variables 39 if (!com.gContactSync) com.gContactSync = {}; 40 41 window.addEventListener("load", 42 /** Initializes the Accounts class when the window has finished loading */ 43 function gCS_AccountsLoadListener(e) { 44 com.gContactSync.Accounts.initDialog(); 45 }, 46 false); 47 48 /** 49 * The JavaScript variables and functions that handle different gContactSync 50 * accounts allowing each synchronized address book to have its own preferences. 51 * @class 52 */ 53 com.gContactSync.Accounts = { 54 mUnsavedChange: false, 55 /** The column index of the address book name 56 * change this if adding a column before the AB name 57 */ 58 mAbNameIndex: 0, 59 /** Element IDs used when enabling/disabling the preferences */ 60 mPrefElemIDs: [ 61 "Username", 62 "Groups", 63 "showAdvanced", 64 "Plugin", 65 "SyncDirection", 66 "disabled" 67 ], 68 /** 69 * Initializes the Accounts dialog by filling the tree of address books, 70 * filling in the usernames, hiding the advanced settings, etc. 71 */ 72 initDialog: function Accounts_initDialog() { 73 try { 74 this.fillAbTree(); 75 this.fillUsernames(); 76 this.showAdvancedSettings(document.getElementById("showAdvanced").checked); 77 this.selectedAbChange(); 78 } 79 catch (e) { 80 com.gContactSync.LOGGER.LOG_WARNING("Error in Accounts.initDialog", e); 81 // TODO remove the alert 82 com.gContactSync.alertError(e); 83 } 84 }, 85 /** 86 * Create a new username/account for the selected plugin. 87 * @returns {boolean} True if an authentication HTTP request was sent. 88 */ 89 newUsername: function Accounts_newUsername() { 90 var prompt = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 91 .getService(Components.interfaces.nsIPromptService) 92 .promptUsernameAndPassword, 93 username = {}, 94 password = {}, 95 // opens a username/password prompt 96 ok = prompt(window, com.gContactSync.StringBundle.getStr("loginTitle"), 97 com.gContactSync.StringBundle.getStr("loginText"), username, password, null, 98 {value: false}); 99 if (!ok) { 100 return false; 101 } 102 if (com.gContactSync.LoginManager.getAuthToken(username.value)) { // the username already exists 103 com.gContactSync.alertWarning(com.gContactSync.StringBundle.getStr("usernameExists")); 104 return false; 105 } 106 // This is a primitive way of validating an e-mail address, but Google takes 107 // care of the rest. It seems to allow getting an auth token w/ only the 108 // username, but returns an error when trying to do anything w/ that token 109 // so this makes sure it is a full e-mail address. 110 if (username.value.indexOf("@") < 1) { 111 com.gContactSync.alertError(com.gContactSync.StringBundle.getStr("invalidEmail")); 112 return this.newUsername(); 113 } 114 // fix the username before authenticating 115 username.value = com.gContactSync.fixUsername(username.value); 116 var body = com.gContactSync.gdata.makeAuthBody(username.value, password.value), 117 httpReq = new com.gContactSync.GHttpRequest("authenticate", null, null, body); 118 // if it succeeds and Google returns the auth token, store it and then start 119 // a new sync 120 httpReq.mOnSuccess = function newUsernameSuccess(httpReq) { 121 com.gContactSync.LoginManager.addAuthToken(username.value, 122 'GoogleLogin' + httpReq.responseText.split("\n")[2]); 123 com.gContactSync.Accounts.selectedAbChange(); 124 com.gContactSync.Accounts.fillUsernames(); 125 }; 126 // if it fails, alert the user and prompt them to try again 127 httpReq.mOnError = function newUsernameError(httpReq) { 128 com.gContactSync.alertError(com.gContactSync.StringBundle.getStr('authErr')); 129 com.gContactSync.LOGGER.LOG_ERROR('Authentication Error - ' + 130 httpReq.status, 131 httpReq.responseText); 132 com.gContactSync.Accounts.newUsername(); 133 }; 134 // if the user is offline, alert them and quit 135 httpReq.mOnOffline = function newUsernameOffline(httpReq) { 136 com.gContactSync.alertWarning(com.gContactSync.StringBundle.getStr('offlineErr')); 137 com.gContactSync.LOGGER.LOG_ERROR(com.gContactSync.StringBundle.getStr('offlineErr')); 138 }; 139 httpReq.send(); 140 return true; 141 }, 142 /** 143 * Returns a new GAddressBook corresponding to the currently-selected address 144 * book in the accounts tree. 145 * @returns {com.gContactSync.GAddressBook} A GAddressBook if one is selected, else false. 146 */ 147 getSelectedAb: function Accounts_getSelectedAb() { 148 var tree = document.getElementById("loginTree"); 149 if (tree.currentIndex < 0) { 150 this.enablePreferences(false); 151 return false; 152 } 153 this.enablePreferences(true); 154 var abName = tree.view.getCellText(tree.currentIndex, 155 tree.columns.getColumnAt(this.mAbNameIndex)), 156 ab = com.gContactSync.GAbManager.getAbByName(abName); 157 if (!ab) { 158 return false; 159 } 160 return new com.gContactSync.GAddressBook(ab); 161 }, 162 /** 163 * Creates and returns a new address book after requesting a name for it. 164 * If an AB of any type already exists this function will do nothing. 165 * @returns {nsIAbDirectory} The new address book. 166 */ 167 newAddressBook: function Accounts_newAddressBook() { 168 var name = com.gContactSync.prompt(com.gContactSync.StringBundle.getStr("newABPrompt"), null, window); 169 if (!name) 170 return false; 171 var ab = com.gContactSync.AbManager.getAbByName(name); 172 this.fillAbTree(); 173 return ab; 174 }, 175 /** 176 * Saves the preferences for the selected address book. 177 * @returns {boolean} True if the preferences were saved 178 */ 179 saveSelectedAccount: function Accounts_saveSelectedAccount() { 180 var usernameElem = document.getElementById("Username"), 181 groupElem = document.getElementById("Groups"), 182 directionElem = document.getElementById("SyncDirection"), 183 pluginElem = document.getElementById("Plugin"), 184 disableElem = document.getElementById("disabled"), 185 updateGElem = document.getElementById("updateGoogleInConflicts"), 186 ab = this.getSelectedAb(), 187 needsReset = false; 188 if (!ab) { 189 return null; 190 } 191 192 if (!usernameElem || !groupElem || !directionElem || !pluginElem || !disableElem) { 193 return false; 194 } 195 var syncGroups = String(groupElem.value === "All"), 196 myContacts = String(groupElem.value !== "All" && groupElem.value !== "false"); 197 // the simple preferences 198 ab.savePref("Username", usernameElem.value); 199 ab.savePref("Plugin", pluginElem.value); 200 ab.savePref("Disabled", disableElem.checked); 201 ab.savePref("updateGoogleInConflicts", updateGElem.checked); 202 // this is for backward compatibility 203 ab.savePref("Primary", "true"); 204 // Group to sync 205 ab.savePref("syncGroups", syncGroups); 206 ab.savePref("myContacts", myContacts); 207 ab.savePref("myContactsName", groupElem.value); 208 // Sync Direction 209 ab.savePref("writeOnly", directionElem.value === "WriteOnly"); 210 ab.savePref("readOnly", directionElem.value === "ReadOnly"); 211 // this is done before the needsReset call in case something happens 212 // reset the unsaved change 213 this.mUnsavedChange = false; 214 this.fillUsernames(); 215 this.selectedAbChange(); 216 this.fillAbTree(); 217 // check if the AB should be reset based on the new values 218 needsReset = this.needsReset(ab, usernameElem.value, syncGroups, myContacts, groupElem.value); 219 if (needsReset) { 220 ab.reset(); 221 com.gContactSync.alert(com.gContactSync.StringBundle.getStr("finishedAcctSave")); 222 } 223 else { 224 com.gContactSync.alert(com.gContactSync.StringBundle.getStr("finishedAcctSaveNoRestart")); 225 } 226 return true; 227 }, 228 /** 229 * Enables or disables the preference elements. 230 * @param aEnable {boolean} Set to true to enable elements or false to disable 231 * them. 232 */ 233 enablePreferences: function Accounts_enablePreferences(aEnable) { 234 var elem, i; 235 for (i = 0; i < this.mPrefElemIDs.length; i++) { 236 elem = document.getElementById(this.mPrefElemIDs[i]); 237 if (!elem) { 238 com.gContactSync.LOGGER.LOG_WARNING(this.mPrefElemIDs[i] + " not found"); 239 continue; 240 } 241 elem.disabled = aEnable ? false : true; 242 } 243 }, 244 /** 245 * Show or hide the advanced settings and then call window.sizeToContent(). 246 * @param aShow {boolean} Set to true to show the advanced settings or false 247 * to hide them. 248 * @returns {boolean} True if the advanced settings were shown or hidden. 249 */ 250 showAdvancedSettings: function Accounts_showAdvanceDsettings(aShow) { 251 var elem = document.getElementById("advancedGroupBox"); 252 if (!elem) return false; 253 elem.setAttribute("collapsed", aShow ? "false" : "true"); 254 window.sizeToContent(); 255 return true; 256 }, 257 /** 258 * Called when the selected address book changes in the accounts tree. 259 * @returns {boolean} true if there is currently an address book selected. 260 */ 261 selectedAbChange: function Accounts_selectedAbChange() { 262 var usernameElem = document.getElementById("Username"), 263 groupElem = document.getElementById("Groups"), 264 directionElem = document.getElementById("SyncDirection"), 265 pluginElem = document.getElementById("Plugin"), 266 disableElem = document.getElementById("disabled"), 267 updateGElem = document.getElementById("updateGoogleInConflicts"), 268 ab = this.getSelectedAb(); 269 this.restoreGroups(); 270 if (!usernameElem || !groupElem || !directionElem || !pluginElem || !disableElem) { 271 return false; 272 } 273 if (!ab) { 274 return ab; 275 } 276 // Username/Account 277 this.fillUsernames(ab.mPrefs.Username); 278 // Group 279 // The myContacts pref (enable sync w/ one group) has priority 280 // If that is checked an the myContactsName is pref sync just that group 281 // Otherwise sync all or no groups based on the syncGroups pref 282 var group = ab.mPrefs.myContacts ? 283 (ab.mPrefs.myContactsName ? ab.mPrefs.myContactsName : "false") : 284 (ab.mPrefs.syncGroups !== "false" ? "All" : "false"); 285 com.gContactSync.selectMenuItem(groupElem, group, true); 286 // Sync Direction 287 var direction = ab.mPrefs.readOnly === "true" ? "ReadOnly" : 288 ab.mPrefs.writeOnly === "true" ? "WriteOnly" : "Complete"; 289 com.gContactSync.selectMenuItem(directionElem, direction, true); 290 // Temporarily disable synchronization with the address book 291 disableElem.checked = ab.mPrefs.Disabled === "true"; 292 // Overwrite remote changes with local changes in a conflict 293 updateGElem.checked = ab.mPrefs.updateGoogleInConflicts === "true"; 294 // Select the correct plugin 295 com.gContactSync.selectMenuItem(pluginElem, ab.mPrefs.Plugin, true); 296 297 return true; 298 }, 299 /** 300 * Fills the 'Username' menulist with all the usernames of the current plugin. 301 * @param aDefault {string} The default account to select. If not present or 302 * evaluating to 'false' then 'None' will be 303 * selected. 304 */ 305 fillUsernames: function Accounts_fillUsernames(aDefault) { 306 var usernameElem = document.getElementById("Username"), 307 tokens = com.gContactSync.LoginManager.getAuthTokens(), 308 item, 309 username, 310 index = -1; 311 if (!usernameElem) { 312 return false; 313 } 314 // Remove all existing logins from the menulist 315 usernameElem.removeAllItems(); 316 317 usernameElem.appendItem(com.gContactSync.StringBundle.getStr("noAccount"), "none"); 318 // Add a menuitem for each account with an auth token 319 for (username in tokens) { 320 item = usernameElem.appendItem(username, username); 321 if (aDefault === username && aDefault !== undefined) { 322 index = usernameElem.menupopup.childNodes.length - 1; 323 } 324 } 325 326 if (index > -1) { 327 usernameElem.selectedIndex = index; 328 } 329 // if the default value isn't in the menu list, add & select it 330 // this can happen when an account is added through one version of the 331 // login manager and the Accounts dialog was opened in another 332 // This isn't retained (for now?) to prevent anyone from setting up a new 333 // synchronized account with it and expecting it to work. 334 else if (aDefault) { 335 com.gContactSync.selectMenuItem(usernameElem, aDefault, true); 336 } 337 // Otherwise select None 338 else { 339 usernameElem.selectedIndex = 0; 340 } 341 342 return true; 343 }, 344 /** 345 * Populates the address book tree with all Personal/Mork Address Books 346 */ 347 fillAbTree: function Accounts_fillAbTree() { 348 var tree = document.getElementById("loginTree"), 349 treechildren = document.getElementById("loginTreeChildren"), 350 newTreeChildren, 351 abs, 352 i; 353 354 if (treechildren) { 355 try { tree.removeChild(treechildren); } catch (e) {} 356 } 357 newTreeChildren = document.createElement("treechildren"); 358 newTreeChildren.setAttribute("id", "loginTreeChildren"); 359 tree.appendChild(newTreeChildren); 360 361 // Get all Personal/Mork DB Address Books (type == 2, 362 // see mailnews/addrbook/src/nsDirPrefs.h) 363 // TODO - there should be a way to change the allowed dir types... 364 abs = com.gContactSync.GAbManager.getAllAddressBooks(2); 365 for (i in abs) { 366 if (abs[i] instanceof com.gContactSync.GAddressBook) { 367 this.addToTree(newTreeChildren, abs[i]); 368 } 369 } 370 return true; 371 }, 372 /** 373 * Adds login information (username and directory name) to the tree. 374 * @param aTreeChildren {object} The <treechildren> XUL element. 375 * @param aAB {GAddressBook} The GAddressBook to add. 376 */ 377 addToTree: function Accounts_addToTree(aTreeChildren, aAB) { 378 if (!aAB || !aAB instanceof com.gContactSync.GAddressBook) { 379 throw "Error - Invalid AB passed to addToTree"; 380 } 381 var treeitem = document.createElement("treeitem"), 382 treerow = document.createElement("treerow"), 383 addressbook = document.createElement("treecell"), 384 synced = document.createElement("treecell"); 385 386 addressbook.setAttribute("label", aAB.getName()); 387 synced.setAttribute("label", aAB.mPrefs.Username ? aAB.mPrefs.Username : com.gContactSync.StringBundle.getStr("noAccount")); 388 389 treerow.appendChild(addressbook); 390 treerow.appendChild(synced); 391 treeitem.appendChild(treerow); 392 aTreeChildren.appendChild(treeitem); 393 394 return true; 395 }, 396 /** 397 * Deletes the selected address book 398 * This function is commented out as the associated button was removed 399 */ 400 /* 401 deleteSelectedAB: function Accounts_deleteSelectedAB() { 402 var ab = this.getSelectedAb(); 403 if (!ab) { 404 com.gContactSync.alertWarning(com.gContactSync.StringBundle.getStr("noABSelected")); 405 return ab; 406 } 407 // Make sure sure the user doesn't try to delete the CAB or PAB 408 var uri = ab.mURI; 409 if (!uri || uri.indexOf("abook.mab") !== -1 || uri.indexOf("history.mab") !== -1) { 410 com.gContactSync.alertError(com.gContactSync.StringBundle.getStr("deletePAB")); 411 return false; 412 } 413 if (!com.gContactSync.confirm(com.gContactSync.StringBundle.getStr("deleteAB"))) 414 return false; 415 // This function also checks that the AB isn't the PAB or CAB 416 ab.deleteAB(); 417 this.fillAbTree(); 418 return true; 419 }, 420 */ 421 /** 422 * Removes the synchronization settings from the selected address book. 423 * @return {boolean} True if the synchronization settings were removed. 424 */ 425 /* 426 removeSyncSettings: function Accounts_removeSelectedLogin() { 427 var ab = this.getSelectedAb(); 428 if (!ab) { 429 com.gContactSync.alertWarning(com.gContactSync.StringBundle.getStr("noABSelected")); 430 return false; 431 } 432 if (!com.gContactSync.confirm(com.gContactSync.StringBundle.getStr("removeSyncSettings"))) { 433 return false; 434 } 435 // remove the saved prefs from the address book 436 ab.savePref("Username", ""); 437 ab.setLastSyncDate(0); 438 ab.savePref("myContactsName", ""); 439 ab.savePref("myContacts", ""); 440 this.fillUsernames(); 441 this.selectedAbChange(); 442 this.fillAbTree(); 443 return true; 444 }, 445 */ 446 /** 447 * Shows an alert dialog that briefly explains the synchronization direction 448 * preference. 449 */ 450 directionPopup: function Accounts_directionPopup() { 451 com.gContactSync.alert(com.gContactSync.StringBundle.getStr("directionPopup")); 452 }, 453 /** 454 * Restores the Groups menulist to contain only the default groups. 455 */ 456 restoreGroups: function Accounts_restoreGroups() { 457 var groupElem = document.getElementById("GroupsPopup"); 458 for (var i = groupElem.childNodes.length - 1; i > -1; i--) { 459 if (groupElem.childNodes[i].getAttribute("class") !== "default") 460 groupElem.removeChild(groupElem.childNodes[i]); 461 } 462 }, 463 /** 464 * Fetch all groups for the selected account and add custom groups to the 465 * menulist. 466 */ 467 getAllGroups: function Accounts_getAllGroups() { 468 var usernameElem = document.getElementById("Username"); 469 this.restoreGroups(); 470 if (usernameElem.value === "none" || !usernameElem.value) 471 return false; 472 var token = com.gContactSync.LoginManager.getAuthTokens()[usernameElem.value]; 473 if (!token) { 474 com.gContactSync.LOGGER.LOG_WARNING("Unable to find the token for username " + usernameElem.value); 475 return false; 476 } 477 com.gContactSync.LOGGER.VERBOSE_LOG("Fetching groups for username: " + usernameElem.value); 478 var httpReq = new com.gContactSync.GHttpRequest("getGroups", token, null, 479 null, usernameElem.value); 480 httpReq.mOnSuccess = function getAllGroupsSuccess(httpReq) { 481 com.gContactSync.LOGGER.VERBOSE_LOG(com.gContactSync.serializeFromText(httpReq.responseText)); 482 com.gContactSync.Accounts.addGroups(httpReq.responseXML, 483 usernameElem.value); 484 }; 485 httpReq.mOnError = function getAllGroupsError(httpReq) { 486 com.gContactSync.LOGGER.LOG_ERROR(httpReq.responseText); 487 }; 488 httpReq.mOnOffline = null; 489 httpReq.send(); 490 return true; 491 }, 492 /** 493 * Adds groups in the given atom feed to the Groups menulist provided the 494 * username hasn't changed since the groups request was sent and the username 495 * isn't blank. 496 */ 497 addGroups: function Accounts_addGroups(aAtom, aUsername) { 498 var usernameElem = document.getElementById("Username"), 499 menulistElem = document.getElementById("Groups"), 500 group, 501 title, 502 i, 503 arr; 504 if (!aAtom) { 505 return false; 506 } 507 if (usernameElem.value === "none" || usernameElem.value !== aUsername) { 508 return false; 509 } 510 arr = aAtom.getElementsByTagNameNS(com.gContactSync.gdata.namespaces.ATOM.url, "entry"); 511 com.gContactSync.LOGGER.VERBOSE_LOG("Adding groups from username: " + aUsername); 512 for (i = 0; i < arr.length; i++) { 513 group = new com.gContactSync.Group(arr[i]); 514 title = group.getTitle(); 515 com.gContactSync.LOGGER.VERBOSE_LOG(" * " + title); 516 // don't add system groups again 517 if (!title || group.isSystemGroup()) { 518 com.gContactSync.LOGGER.VERBOSE_LOG(" - Skipping system group"); 519 continue; 520 } 521 menulistElem.appendItem(title, title); 522 } 523 return true; 524 }, 525 /** 526 * Returns whether the given address book should be reset and prompts the user 527 * before returning true. 528 * Resetting an address book is necessary when ALL of the following 529 * conditions marked with * are met: 530 * * The username was NOT originally blank 531 * * The new username is NOT blank 532 * * The last sync date of the AB is > 0 533 * * The user agrees that the AB should be reset (using a confirm dialog) 534 * * AND at least one of the following is true: 535 * o The username has changed (and wasn't originally blank) 536 * o OR The group to sync has been changed 537 * 538 * @param aAB {string} The GAddressBook being modified. If this 539 * function returns true this AB should be 540 * reset. 541 * @param aUsername {string} The new username for the account with 542 * which aAB will be synchronized. 543 * @param aSyncGroups {string} The new value for the syncGroups pref. 544 * @param aMyContacts {string} The new value for the myContacts pref. 545 * @param aMyContactsName {string} The new value for the myContactsName pref. 546 * 547 * @return {boolean} true if the AB should be reset. See the detailed 548 * description for more details. 549 */ 550 needsReset: function Accounts_needsReset(aAB, aUsername, aSyncGroups, aMyContacts, aMyContactsName) { 551 com.gContactSync.LOGGER.VERBOSE_LOG 552 ( 553 "**Determining if the address book '" + aAB.getName() + 554 "' should be reset:\n" + 555 " * " + aUsername + " <- " + aAB.mPrefs.Username + "\n" + 556 " * " + aSyncGroups + " <- " + aAB.mPrefs.syncGroups + "\n" + 557 " * " + aMyContacts + " <- " + aAB.mPrefs.myContacts + "\n" + 558 " * " + aMyContactsName + " <- " + aAB.mPrefs.myContactsName + "\n" + 559 " * Last sync date: " + aAB.mPrefs.lastSync 560 ); 561 // TODO - remove once this is no longer necessary 562 aAB.getPrefs(); 563 // NOTE: mUnsavedChange is reset to false before this method is called 564 if ((aAB.mPrefs.Username && aAB.mPrefs.Username !== "none") && 565 aUsername !== "none" && 566 parseInt(aAB.mPrefs.lastSync, 10) > 0 && 567 ( 568 aAB.mPrefs.Username !== aUsername || 569 aAB.mPrefs.syncGroups !== aSyncGroups || 570 aAB.mPrefs.myContacts !== aMyContacts || 571 aAB.mPrefs.myContactsName !== aMyContactsName 572 )) { 573 var reset = com.gContactSync.confirm(com.gContactSync.StringBundle.getStr("confirmABReset")); 574 com.gContactSync.LOGGER.VERBOSE_LOG(" * Confirmation result: " + reset + "\n"); 575 return reset; 576 } 577 com.gContactSync.LOGGER.VERBOSE_LOG(" * The AB will NOT be reset\n"); 578 return false; 579 }, 580 /** 581 * This method is called when the user clicks the Accept button 582 * (labeled Close) or when acceptDialog() is called. 583 * If there are unsaved changes it will let the user save changes if 584 * desired. 585 * @returns {boolean} Always returns true (close the dialog). 586 */ 587 close: function Accounts_close() { 588 if (this.mUnsavedChange && 589 com.gContactSync.confirm(com.gContactSync.StringBundle.getStr("unsavedAcctChanges"))) { 590 this.saveSelectedAccount(); 591 } 592 return true; 593 } 594 }; 595