1 /* 2 * Copyright (C) 2011 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 17 package com.android.sdkuilib.internal.repository.sdkman2; 18 19 import com.android.sdklib.SdkConstants; 20 import com.android.sdklib.internal.repository.IDescription; 21 import com.android.sdklib.internal.repository.ITask; 22 import com.android.sdklib.internal.repository.ITaskMonitor; 23 import com.android.sdklib.internal.repository.archives.Archive; 24 import com.android.sdklib.internal.repository.archives.ArchiveInstaller; 25 import com.android.sdklib.internal.repository.packages.Package; 26 import com.android.sdklib.internal.repository.sources.SdkSource; 27 import com.android.sdkuilib.internal.repository.UpdaterData; 28 import com.android.sdkuilib.internal.repository.UpdaterPage; 29 import com.android.sdkuilib.internal.repository.icons.ImageFactory; 30 import com.android.sdkuilib.internal.repository.sdkman2.PackageLoader.ISourceLoadedCallback; 31 import com.android.sdkuilib.internal.repository.sdkman2.PkgItem.PkgState; 32 import com.android.sdkuilib.repository.ISdkChangeListener; 33 import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext; 34 import com.android.sdkuilib.ui.GridDataBuilder; 35 import com.android.sdkuilib.ui.GridLayoutBuilder; 36 37 import org.eclipse.jface.dialogs.MessageDialog; 38 import org.eclipse.jface.viewers.CheckStateChangedEvent; 39 import org.eclipse.jface.viewers.CheckboxTreeViewer; 40 import org.eclipse.jface.viewers.ColumnLabelProvider; 41 import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; 42 import org.eclipse.jface.viewers.DoubleClickEvent; 43 import org.eclipse.jface.viewers.ICheckStateListener; 44 import org.eclipse.jface.viewers.IDoubleClickListener; 45 import org.eclipse.jface.viewers.ISelection; 46 import org.eclipse.jface.viewers.ITableFontProvider; 47 import org.eclipse.jface.viewers.ITreeContentProvider; 48 import org.eclipse.jface.viewers.ITreeSelection; 49 import org.eclipse.jface.viewers.TreeViewerColumn; 50 import org.eclipse.jface.viewers.Viewer; 51 import org.eclipse.jface.viewers.ViewerFilter; 52 import org.eclipse.jface.window.ToolTip; 53 import org.eclipse.swt.SWT; 54 import org.eclipse.swt.events.DisposeEvent; 55 import org.eclipse.swt.events.DisposeListener; 56 import org.eclipse.swt.events.SelectionAdapter; 57 import org.eclipse.swt.events.SelectionEvent; 58 import org.eclipse.swt.graphics.Font; 59 import org.eclipse.swt.graphics.FontData; 60 import org.eclipse.swt.graphics.Image; 61 import org.eclipse.swt.graphics.Point; 62 import org.eclipse.swt.widgets.Button; 63 import org.eclipse.swt.widgets.Composite; 64 import org.eclipse.swt.widgets.Control; 65 import org.eclipse.swt.widgets.Event; 66 import org.eclipse.swt.widgets.Group; 67 import org.eclipse.swt.widgets.Label; 68 import org.eclipse.swt.widgets.Link; 69 import org.eclipse.swt.widgets.MenuItem; 70 import org.eclipse.swt.widgets.Text; 71 import org.eclipse.swt.widgets.Tree; 72 import org.eclipse.swt.widgets.TreeColumn; 73 74 import java.io.File; 75 import java.net.MalformedURLException; 76 import java.net.URL; 77 import java.util.ArrayList; 78 import java.util.HashMap; 79 import java.util.List; 80 import java.util.Map; 81 import java.util.Map.Entry; 82 83 /** 84 * Page that displays both locally installed packages as well as all known 85 * remote available packages. This gives an overview of what is installed 86 * vs what is available and allows the user to update or install packages. 87 */ 88 public class PackagesPage extends UpdaterPage implements ISdkChangeListener { 89 90 static final String ICON_CAT_OTHER = "pkgcat_other_16.png"; //$NON-NLS-1$ 91 static final String ICON_CAT_PLATFORM = "pkgcat_16.png"; //$NON-NLS-1$ 92 static final String ICON_SORT_BY_SOURCE = "source_icon16.png"; //$NON-NLS-1$ 93 static final String ICON_SORT_BY_API = "platform_pkg_16.png"; //$NON-NLS-1$ 94 static final String ICON_PKG_NEW = "pkg_new_16.png"; //$NON-NLS-1$ 95 static final String ICON_PKG_INCOMPAT = "pkg_incompat_16.png"; //$NON-NLS-1$ 96 static final String ICON_PKG_UPDATE = "pkg_update_16.png"; //$NON-NLS-1$ 97 static final String ICON_PKG_INSTALLED = "pkg_installed_16.png"; //$NON-NLS-1$ 98 99 enum MenuAction { 100 RELOAD (SWT.NONE, "Reload"), 101 SHOW_ADDON_SITES (SWT.NONE, "Manage Add-on Sites..."), 102 TOGGLE_SHOW_ARCHIVES (SWT.CHECK, "Show Archives Details"), 103 TOGGLE_SHOW_INSTALLED_PKG (SWT.CHECK, "Show Installed Packages"), 104 TOGGLE_SHOW_OBSOLETE_PKG (SWT.CHECK, "Show Obsolete Packages"), 105 TOGGLE_SHOW_UPDATE_NEW_PKG (SWT.CHECK, "Show Updates/New Packages"), 106 SORT_API_LEVEL (SWT.RADIO, "Sort by API Level"), 107 SORT_SOURCE (SWT.RADIO, "Sort by Repository") 108 ; 109 110 private final int mMenuStyle; 111 private final String mMenuTitle; 112 113 MenuAction(int menuStyle, String menuTitle) { 114 mMenuStyle = menuStyle; 115 mMenuTitle = menuTitle; 116 } 117 118 public int getMenuStyle() { 119 return mMenuStyle; 120 } 121 122 public String getMenuTitle() { 123 return mMenuTitle; 124 } 125 }; 126 127 private final Map<MenuAction, MenuItem> mMenuActions = new HashMap<MenuAction, MenuItem>(); 128 129 private final SdkInvocationContext mContext; 130 private final UpdaterData mUpdaterData; 131 private final PackagesDiffLogic mDiffLogic; 132 private boolean mDisplayArchives = false; 133 private boolean mOperationPending; 134 135 private Text mTextSdkOsPath; 136 private Button mCheckSortSource; 137 private Button mCheckSortApi; 138 private Button mCheckFilterObsolete; 139 private Button mCheckFilterInstalled; 140 private Button mCheckFilterNew; 141 private Composite mGroupOptions; 142 private Composite mGroupSdk; 143 private Group mGroupPackages; 144 private Button mButtonDelete; 145 private Button mButtonInstall; 146 private Tree mTree; 147 private CheckboxTreeViewer mTreeViewer; 148 private TreeViewerColumn mColumnName; 149 private TreeViewerColumn mColumnApi; 150 private TreeViewerColumn mColumnRevision; 151 private TreeViewerColumn mColumnStatus; 152 private Font mTreeFontItalic; 153 private TreeColumn mTreeColumnName; 154 155 public PackagesPage( 156 Composite parent, 157 int swtStyle, 158 UpdaterData updaterData, 159 SdkInvocationContext context) { 160 super(parent, swtStyle); 161 mUpdaterData = updaterData; 162 mContext = context; 163 164 mDiffLogic = new PackagesDiffLogic(updaterData); 165 166 createContents(this); 167 postCreate(); //$hide$ 168 } 169 170 public void performFirstLoad() { 171 // Initialize the package list the first time the page is shown. 172 loadPackages(true /*isFirstLoad*/); 173 } 174 175 @SuppressWarnings("unused") 176 private void createContents(Composite parent) { 177 GridLayoutBuilder.create(parent).noMargins().columns(2); 178 179 mGroupSdk = new Composite(parent, SWT.NONE); 180 GridDataBuilder.create(mGroupSdk).hFill().vCenter().hGrab().hSpan(2); 181 GridLayoutBuilder.create(mGroupSdk).columns(2); 182 183 Label label1 = new Label(mGroupSdk, SWT.NONE); 184 label1.setText("SDK Path:"); 185 186 mTextSdkOsPath = new Text(mGroupSdk, SWT.NONE); 187 GridDataBuilder.create(mTextSdkOsPath).hFill().vCenter().hGrab(); 188 mTextSdkOsPath.setEnabled(false); 189 190 mGroupPackages = new Group(parent, SWT.NONE); 191 GridDataBuilder.create(mGroupPackages).fill().grab().hSpan(2); 192 mGroupPackages.setText("Packages"); 193 GridLayoutBuilder.create(mGroupPackages).columns(1); 194 195 mTreeViewer = new CheckboxTreeViewer(mGroupPackages, SWT.BORDER); 196 mTreeViewer.addFilter(new ViewerFilter() { 197 @Override 198 public boolean select(Viewer viewer, Object parentElement, Object element) { 199 return filterViewerItem(element); 200 } 201 }); 202 203 mTreeViewer.addCheckStateListener(new ICheckStateListener() { 204 @Override 205 public void checkStateChanged(CheckStateChangedEvent event) { 206 onTreeCheckStateChanged(event); //$hide$ 207 } 208 }); 209 210 mTreeViewer.addDoubleClickListener(new IDoubleClickListener() { 211 @Override 212 public void doubleClick(DoubleClickEvent event) { 213 onTreeDoubleClick(event); //$hide$ 214 } 215 }); 216 217 mTree = mTreeViewer.getTree(); 218 mTree.setLinesVisible(true); 219 mTree.setHeaderVisible(true); 220 GridDataBuilder.create(mTree).fill().grab(); 221 222 // column name icon is set when loading depending on the current filter type 223 // (e.g. API level or source) 224 mColumnName = new TreeViewerColumn(mTreeViewer, SWT.NONE); 225 mTreeColumnName = mColumnName.getColumn(); 226 mTreeColumnName.setText("Name"); 227 mTreeColumnName.setWidth(340); 228 229 mColumnApi = new TreeViewerColumn(mTreeViewer, SWT.NONE); 230 TreeColumn treeColumn2 = mColumnApi.getColumn(); 231 treeColumn2.setText("API"); 232 treeColumn2.setAlignment(SWT.CENTER); 233 treeColumn2.setWidth(50); 234 235 mColumnRevision = new TreeViewerColumn(mTreeViewer, SWT.NONE); 236 TreeColumn treeColumn3 = mColumnRevision.getColumn(); 237 treeColumn3.setText("Rev."); 238 treeColumn3.setToolTipText("Revision currently installed"); 239 treeColumn3.setAlignment(SWT.CENTER); 240 treeColumn3.setWidth(50); 241 242 243 mColumnStatus = new TreeViewerColumn(mTreeViewer, SWT.NONE); 244 TreeColumn treeColumn4 = mColumnStatus.getColumn(); 245 treeColumn4.setText("Status"); 246 treeColumn4.setAlignment(SWT.LEAD); 247 treeColumn4.setWidth(190); 248 249 mGroupOptions = new Composite(mGroupPackages, SWT.NONE); 250 GridDataBuilder.create(mGroupOptions).hFill().vCenter().hGrab(); 251 GridLayoutBuilder.create(mGroupOptions).columns(6).noMargins(); 252 253 // Options line 1, 6 columns 254 255 Label label3 = new Label(mGroupOptions, SWT.NONE); 256 label3.setText("Show:"); 257 258 mCheckFilterNew = new Button(mGroupOptions, SWT.CHECK); 259 mCheckFilterNew.setText("Updates/New"); 260 mCheckFilterNew.setToolTipText("Show Updates and New"); 261 mCheckFilterNew.addSelectionListener(new SelectionAdapter() { 262 @Override 263 public void widgetSelected(SelectionEvent e) { 264 refreshViewerInput(); 265 } 266 }); 267 mCheckFilterNew.setSelection(true); 268 269 mCheckFilterInstalled = new Button(mGroupOptions, SWT.CHECK); 270 mCheckFilterInstalled.setToolTipText("Show Installed"); 271 mCheckFilterInstalled.addSelectionListener(new SelectionAdapter() { 272 @Override 273 public void widgetSelected(SelectionEvent e) { 274 refreshViewerInput(); 275 } 276 }); 277 mCheckFilterInstalled.setSelection(true); 278 mCheckFilterInstalled.setText("Installed"); 279 280 mCheckFilterObsolete = new Button(mGroupOptions, SWT.CHECK); 281 mCheckFilterObsolete.setText("Obsolete"); 282 mCheckFilterObsolete.setToolTipText("Also show obsolete packages"); 283 mCheckFilterObsolete.addSelectionListener(new SelectionAdapter() { 284 @Override 285 public void widgetSelected(SelectionEvent e) { 286 refreshViewerInput(); 287 } 288 }); 289 mCheckFilterObsolete.setSelection(false); 290 291 Link linkSelectNew = new Link(mGroupOptions, SWT.NONE); 292 // Note for i18n: we need to identify which link is used, and this is done by using the 293 // text itself so for translation purposes we want to keep the <a> link strings separate. 294 final String strLinkNew = "New"; 295 final String strLinkUpdates = "Updates"; 296 linkSelectNew.setText( 297 String.format("Select <a>%1$s</a> or <a>%2$s</a>", strLinkNew, strLinkUpdates)); 298 linkSelectNew.setToolTipText("Selects all items that are either new or updates."); 299 GridDataBuilder.create(linkSelectNew).hFill().hGrab(); 300 linkSelectNew.addSelectionListener(new SelectionAdapter() { 301 @Override 302 public void widgetSelected(SelectionEvent e) { 303 super.widgetSelected(e); 304 boolean selectNew = e.text == null || e.text.equals(strLinkNew); 305 onSelectNewUpdates(selectNew, !selectNew, false/*selectTop*/); 306 } 307 }); 308 309 mButtonInstall = new Button(mGroupOptions, SWT.NONE); 310 mButtonInstall.setText(""); //$NON-NLS-1$ placeholder, filled in updateButtonsState() 311 mButtonInstall.setToolTipText("Install one or more packages"); 312 GridDataBuilder.create(mButtonInstall).hFill().vCenter().hGrab(); 313 mButtonInstall.addSelectionListener(new SelectionAdapter() { 314 @Override 315 public void widgetSelected(SelectionEvent e) { 316 onButtonInstall(); //$hide$ 317 } 318 }); 319 320 // Options line 2, 6 columns 321 322 Label label2 = new Label(mGroupOptions, SWT.NONE); 323 label2.setText("Sort by:"); 324 325 mCheckSortApi = new Button(mGroupOptions, SWT.RADIO); 326 mCheckSortApi.setToolTipText("Sort by API level"); 327 mCheckSortApi.addSelectionListener(new SelectionAdapter() { 328 @Override 329 public void widgetSelected(SelectionEvent e) { 330 if (mCheckSortApi.getSelection()) { 331 refreshViewerInput(); 332 copySelection(true /*toApi*/); 333 syncViewerSelection(); 334 } 335 } 336 }); 337 mCheckSortApi.setText("API level"); 338 mCheckSortApi.setSelection(true); 339 340 mCheckSortSource = new Button(mGroupOptions, SWT.RADIO); 341 mCheckSortSource.setText("Repository"); 342 mCheckSortSource.setToolTipText("Sort by Repository"); 343 mCheckSortSource.addSelectionListener(new SelectionAdapter() { 344 @Override 345 public void widgetSelected(SelectionEvent e) { 346 if (mCheckSortSource.getSelection()) { 347 refreshViewerInput(); 348 copySelection(false /*toApi*/); 349 syncViewerSelection(); 350 } 351 } 352 }); 353 354 new Label(mGroupOptions, SWT.NONE); 355 356 Link linkDeselect = new Link(mGroupOptions, SWT.NONE); 357 linkDeselect.setText("<a>Deselect All</a>"); 358 linkDeselect.setToolTipText("Deselects all the currently selected items"); 359 GridDataBuilder.create(linkDeselect).hFill().hGrab(); 360 linkDeselect.addSelectionListener(new SelectionAdapter() { 361 @Override 362 public void widgetSelected(SelectionEvent e) { 363 super.widgetSelected(e); 364 onDeselectAll(); 365 } 366 }); 367 368 mButtonDelete = new Button(mGroupOptions, SWT.NONE); 369 mButtonDelete.setText(""); //$NON-NLS-1$ placeholder, filled in updateButtonsState() 370 mButtonDelete.setToolTipText("Delete one ore more installed packages"); 371 GridDataBuilder.create(mButtonDelete).hFill().vCenter().hGrab(); 372 mButtonDelete.addSelectionListener(new SelectionAdapter() { 373 @Override 374 public void widgetSelected(SelectionEvent e) { 375 onButtonDelete(); //$hide$ 376 } 377 }); 378 } 379 380 private Image getImage(String filename) { 381 if (mUpdaterData != null) { 382 ImageFactory imgFactory = mUpdaterData.getImageFactory(); 383 if (imgFactory != null) { 384 return imgFactory.getImageByName(filename); 385 } 386 } 387 return null; 388 } 389 390 391 // -- Start of internal part ---------- 392 // Hide everything down-below from SWT designer 393 //$hide>>$ 394 395 396 // --- menu interactions --- 397 398 public void registerMenuAction(final MenuAction action, MenuItem item) { 399 item.addSelectionListener(new SelectionAdapter() { 400 @Override 401 public void widgetSelected(SelectionEvent e) { 402 Button button = null; 403 404 switch (action) { 405 case RELOAD: 406 fullReload(); 407 break; 408 case SHOW_ADDON_SITES: 409 AddonSitesDialog d = new AddonSitesDialog(getShell(), mUpdaterData); 410 if (d.open()) { 411 loadPackages(); 412 } 413 break; 414 case TOGGLE_SHOW_ARCHIVES: 415 mDisplayArchives = !mDisplayArchives; 416 // Force the viewer to be refreshed 417 ((PkgContentProvider) mTreeViewer.getContentProvider()).setDisplayArchives( 418 mDisplayArchives); 419 mTreeViewer.setInput(null); 420 refreshViewerInput(); 421 syncViewerSelection(); 422 updateButtonsState(); 423 break; 424 case TOGGLE_SHOW_INSTALLED_PKG: 425 button = mCheckFilterInstalled; 426 break; 427 case TOGGLE_SHOW_OBSOLETE_PKG: 428 button = mCheckFilterObsolete; 429 break; 430 case TOGGLE_SHOW_UPDATE_NEW_PKG: 431 button = mCheckFilterNew; 432 break; 433 case SORT_API_LEVEL: 434 button = mCheckSortApi; 435 break; 436 case SORT_SOURCE: 437 button = mCheckSortSource; 438 break; 439 } 440 441 if (button != null && !button.isDisposed()) { 442 // Toggle this button (radio or checkbox) 443 444 boolean value = button.getSelection(); 445 446 // SWT doesn't automatically switch radio buttons when using the 447 // Widget#setSelection method, so we'll do it here manually. 448 if (!value && (button.getStyle() & SWT.RADIO) != 0) { 449 // we'll be selecting this radio button, so deselect all ther other ones 450 // in the parent group. 451 for (Control child : button.getParent().getChildren()) { 452 if (child instanceof Button && 453 child != button && 454 (child.getStyle() & SWT.RADIO) != 0) { 455 ((Button) child).setSelection(value); 456 } 457 } 458 } 459 460 button.setSelection(!value); 461 462 // SWT doesn't actually invoke the listeners when using Widget#setSelection 463 // so let's run the actual action. 464 button.notifyListeners(SWT.Selection, new Event()); 465 } 466 467 updateMenuCheckmarks(); 468 } 469 }); 470 471 mMenuActions.put(action, item); 472 } 473 474 // --- internal methods --- 475 476 private void updateMenuCheckmarks() { 477 478 for (Entry<MenuAction, MenuItem> entry : mMenuActions.entrySet()) { 479 MenuAction action = entry.getKey(); 480 MenuItem item = entry.getValue(); 481 482 if (action.getMenuStyle() == SWT.NONE) { 483 continue; 484 } 485 486 boolean value = false; 487 Button button = null; 488 489 switch (action) { 490 case TOGGLE_SHOW_ARCHIVES: 491 value = mDisplayArchives; 492 break; 493 case TOGGLE_SHOW_INSTALLED_PKG: 494 button = mCheckFilterInstalled; 495 break; 496 case TOGGLE_SHOW_OBSOLETE_PKG: 497 button = mCheckFilterObsolete; 498 break; 499 case TOGGLE_SHOW_UPDATE_NEW_PKG: 500 button = mCheckFilterNew; 501 break; 502 case SORT_API_LEVEL: 503 button = mCheckSortApi; 504 break; 505 case SORT_SOURCE: 506 button = mCheckSortSource; 507 break; 508 } 509 510 if (button != null && !button.isDisposed()) { 511 value = button.getSelection(); 512 } 513 514 if (!item.isDisposed()) { 515 item.setSelection(value); 516 } 517 } 518 } 519 520 private void postCreate() { 521 if (mUpdaterData != null) { 522 mTextSdkOsPath.setText(mUpdaterData.getOsSdkRoot()); 523 } 524 525 mTreeViewer.setContentProvider(new PkgContentProvider(mTreeViewer)); 526 ((PkgContentProvider) mTreeViewer.getContentProvider()).setDisplayArchives( 527 mDisplayArchives); 528 ColumnViewerToolTipSupport.enableFor(mTreeViewer, ToolTip.NO_RECREATE); 529 530 mColumnApi.setLabelProvider( 531 new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mColumnApi))); 532 mColumnName.setLabelProvider( 533 new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mColumnName))); 534 mColumnStatus.setLabelProvider( 535 new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mColumnStatus))); 536 mColumnRevision.setLabelProvider( 537 new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mColumnRevision))); 538 539 FontData fontData = mTree.getFont().getFontData()[0]; 540 fontData.setStyle(SWT.ITALIC); 541 mTreeFontItalic = new Font(mTree.getDisplay(), fontData); 542 543 mTree.addDisposeListener(new DisposeListener() { 544 @Override 545 public void widgetDisposed(DisposeEvent e) { 546 mTreeFontItalic.dispose(); 547 mTreeFontItalic = null; 548 } 549 }); 550 } 551 552 /** 553 * Performs a full reload by removing all cached packages data, including the platforms 554 * and addons from the sdkmanager instance. This will perform a full local parsing 555 * as well as a full reload of the remote data (by fetching all sources again.) 556 */ 557 private void fullReload() { 558 // Clear all source information, forcing them to be refreshed. 559 mUpdaterData.getSources().clearAllPackages(); 560 // Clear and reload all local data too. 561 localReload(); 562 } 563 564 /** 565 * Performs a full reload of all the local package information, including the platforms 566 * and addons from the sdkmanager instance. This will perform a full local parsing. 567 * <p/> 568 * This method does NOT force a new fetch of the remote sources. 569 * 570 * @see #fullReload() 571 */ 572 private void localReload() { 573 // Clear all source caches, otherwise loading will use the cached data 574 mUpdaterData.getLocalSdkParser().clearPackages(); 575 mUpdaterData.getSdkManager().reloadSdk(mUpdaterData.getSdkLog()); 576 loadPackages(); 577 } 578 579 private void loadPackages() { 580 loadPackages(false /*isFirstLoad*/); 581 } 582 583 private void loadPackages(final boolean isFirstLoad) { 584 if (mUpdaterData == null) { 585 return; 586 } 587 588 // LoadPackage is synchronous but does not block the UI. 589 // Consequently it's entirely possible for the user 590 // to request the app to close whilst the packages are loading. Any 591 // action done after loadPackages must check the UI hasn't been 592 // disposed yet. Otherwise hilarity ensues. 593 594 final boolean displaySortByApi = isSortByApi(); 595 596 if (!mTreeColumnName.isDisposed()) { 597 mTreeColumnName.setImage( 598 getImage(displaySortByApi ? ICON_SORT_BY_API : ICON_SORT_BY_SOURCE)); 599 } 600 601 mDiffLogic.updateStart(); 602 mDiffLogic.getPackageLoader().loadPackages( 603 mUpdaterData.getDownloadCache(), // TODO do a first pass with Cache=SERVE_CACHE 604 new ISourceLoadedCallback() { 605 @Override 606 public boolean onUpdateSource(SdkSource source, Package[] newPackages) { 607 // This runs in a thread and must not access UI directly. 608 final boolean changed = mDiffLogic.updateSourcePackages( 609 displaySortByApi, source, newPackages); 610 611 if (!mGroupPackages.isDisposed()) { 612 mGroupPackages.getDisplay().syncExec(new Runnable() { 613 @Override 614 public void run() { 615 if (changed || 616 mTreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) { 617 refreshViewerInput(); 618 } 619 } 620 }); 621 } 622 623 // Return true to tell the loader to continue with the next source. 624 // Return false to stop the loader if any UI has been disposed, which can 625 // happen if the user is trying to close the window during the load operation. 626 return !mGroupPackages.isDisposed(); 627 } 628 629 @Override 630 public void onLoadCompleted() { 631 // This runs in a thread and must not access UI directly. 632 final boolean changed = mDiffLogic.updateEnd(displaySortByApi); 633 634 if (!mGroupPackages.isDisposed()) { 635 mGroupPackages.getDisplay().syncExec(new Runnable() { 636 @Override 637 public void run() { 638 if (changed || 639 mTreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) { 640 refreshViewerInput(); 641 } 642 643 if (mDiffLogic.isFirstLoadComplete() && !mGroupPackages.isDisposed()) { 644 // At the end of the first load, if nothing is selected then 645 // automatically select all new and update packages. 646 Object[] checked = mTreeViewer.getCheckedElements(); 647 if (checked == null || checked.length == 0) { 648 onSelectNewUpdates( 649 false, //selectNew 650 true, //selectUpdates, 651 true); //selectTop 652 } 653 } 654 } 655 }); 656 } 657 } 658 }); 659 } 660 661 private void refreshViewerInput() { 662 // Dynamically update the table while we load after each source. 663 // Since the official Android source gets loaded first, it makes the 664 // window look non-empty a lot sooner. 665 if (!mGroupPackages.isDisposed()) { 666 667 List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi()); 668 if (mTreeViewer.getInput() != cats) { 669 // set initial input 670 mTreeViewer.setInput(cats); 671 } else { 672 // refresh existing, which preserves the expanded state, the selection 673 // and the checked state. 674 mTreeViewer.refresh(); 675 } 676 677 // set the initial expanded state 678 expandInitial(mTreeViewer.getInput()); 679 680 updateButtonsState(); 681 updateMenuCheckmarks(); 682 } 683 } 684 685 private boolean isSortByApi() { 686 return mCheckSortApi != null && !mCheckSortApi.isDisposed() && mCheckSortApi.getSelection(); 687 } 688 689 /** 690 * Decide whether to keep an item in the current tree based on user-chosen filter options. 691 */ 692 private boolean filterViewerItem(Object treeElement) { 693 if (treeElement instanceof PkgCategory) { 694 PkgCategory cat = (PkgCategory) treeElement; 695 696 if (!cat.getItems().isEmpty()) { 697 // A category is hidden if all of its content is hidden. 698 // However empty categories are always visible. 699 for (PkgItem item : cat.getItems()) { 700 if (filterViewerItem(item)) { 701 // We found at least one element that is visible. 702 return true; 703 } 704 } 705 return false; 706 } 707 } 708 709 if (treeElement instanceof PkgItem) { 710 PkgItem item = (PkgItem) treeElement; 711 712 if (!mCheckFilterObsolete.getSelection()) { 713 if (item.isObsolete()) { 714 return false; 715 } 716 } 717 718 if (!mCheckFilterInstalled.getSelection()) { 719 if (item.getState() == PkgState.INSTALLED) { 720 return false; 721 } 722 } 723 724 if (!mCheckFilterNew.getSelection()) { 725 if (item.getState() == PkgState.NEW || item.hasUpdatePkg()) { 726 return false; 727 } 728 } 729 } 730 731 return true; 732 } 733 734 /** 735 * Performs the initial expansion of the tree. This expands categories that contain 736 * at least one installed item and collapses the ones with nothing installed. 737 * 738 * TODO: change this to only change the expanded state on categories that have not 739 * been touched by the user yet. Once we do that, call this every time a new source 740 * is added or the list is reloaded. 741 */ 742 private void expandInitial(Object elem) { 743 if (elem == null) { 744 return; 745 } 746 if (mTreeViewer != null && !mTreeViewer.getTree().isDisposed()) { 747 mTreeViewer.setExpandedState(elem, true); 748 for (Object pkg : 749 ((ITreeContentProvider) mTreeViewer.getContentProvider()).getChildren(elem)) { 750 if (pkg instanceof PkgCategory) { 751 PkgCategory cat = (PkgCategory) pkg; 752 for (PkgItem item : cat.getItems()) { 753 if (item.getState() == PkgState.INSTALLED) { 754 expandInitial(pkg); 755 break; 756 } 757 } 758 } 759 } 760 } 761 } 762 763 /** 764 * Handle checking and unchecking of the tree items. 765 * 766 * When unchecking, all sub-tree items checkboxes are cleared too. 767 * When checking a source, all of its packages are checked too. 768 * When checking a package, only its compatible archives are checked. 769 */ 770 private void onTreeCheckStateChanged(CheckStateChangedEvent event) { 771 boolean checked = event.getChecked(); 772 Object elem = event.getElement(); 773 774 assert event.getSource() == mTreeViewer; 775 776 // When selecting, we want to only select compatible archives and expand the super nodes. 777 checkAndExpandItem(elem, checked, true/*fixChildren*/, true/*fixParent*/); 778 updateButtonsState(); 779 } 780 781 private void onTreeDoubleClick(DoubleClickEvent event) { 782 assert event.getSource() == mTreeViewer; 783 ISelection sel = event.getSelection(); 784 if (sel.isEmpty() || !(sel instanceof ITreeSelection)) { 785 return; 786 } 787 ITreeSelection tsel = (ITreeSelection) sel; 788 Object elem = tsel.getFirstElement(); 789 if (elem == null) { 790 return; 791 } 792 793 ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider(); 794 Object[] children = provider.getElements(elem); 795 if (children == null) { 796 return; 797 } 798 799 if (children.length > 0) { 800 // If the element has children, expand/collapse it. 801 if (mTreeViewer.getExpandedState(elem)) { 802 mTreeViewer.collapseToLevel(elem, 1); 803 } else { 804 mTreeViewer.expandToLevel(elem, 1); 805 } 806 } else { 807 // If the element is a terminal one, select/deselect it. 808 checkAndExpandItem( 809 elem, 810 !mTreeViewer.getChecked(elem), 811 false /*fixChildren*/, 812 true /*fixParent*/); 813 updateButtonsState(); 814 } 815 } 816 817 private void checkAndExpandItem( 818 Object elem, 819 boolean checked, 820 boolean fixChildren, 821 boolean fixParent) { 822 ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider(); 823 824 // fix the item itself 825 if (checked != mTreeViewer.getChecked(elem)) { 826 mTreeViewer.setChecked(elem, checked); 827 } 828 if (elem instanceof PkgItem) { 829 // update the PkgItem to reflect the selection 830 ((PkgItem) elem).setChecked(checked); 831 } 832 833 if (!checked) { 834 if (fixChildren) { 835 // when de-selecting, we deselect all children too 836 mTreeViewer.setSubtreeChecked(elem, checked); 837 for (Object child : provider.getChildren(elem)) { 838 checkAndExpandItem(child, checked, fixChildren, false/*fixParent*/); 839 } 840 } 841 842 // fix the parent when deselecting 843 if (fixParent) { 844 Object parent = provider.getParent(elem); 845 if (parent != null && mTreeViewer.getChecked(parent)) { 846 mTreeViewer.setChecked(parent, false); 847 } 848 } 849 return; 850 } 851 852 // When selecting, we also select sub-items (for a category) 853 if (fixChildren) { 854 if (elem instanceof PkgCategory || elem instanceof PkgItem) { 855 Object[] children = provider.getChildren(elem); 856 for (Object child : children) { 857 checkAndExpandItem(child, true, fixChildren, false/*fixParent*/); 858 } 859 // only fix the parent once the last sub-item is set 860 if (elem instanceof PkgCategory) { 861 if (children.length > 0) { 862 checkAndExpandItem( 863 children[0], true, false/*fixChildren*/, true/*fixParent*/); 864 } else { 865 mTreeViewer.setChecked(elem, false); 866 } 867 } 868 } else if (elem instanceof Package) { 869 // in details mode, we auto-select compatible packages 870 selectCompatibleArchives(elem, provider); 871 } 872 } 873 874 if (fixParent && checked && elem instanceof PkgItem) { 875 Object parent = provider.getParent(elem); 876 if (!mTreeViewer.getChecked(parent)) { 877 Object[] children = provider.getChildren(parent); 878 boolean allChecked = children.length > 0; 879 for (Object e : children) { 880 if (!mTreeViewer.getChecked(e)) { 881 allChecked = false; 882 break; 883 } 884 } 885 if (allChecked) { 886 mTreeViewer.setChecked(parent, true); 887 } 888 } 889 } 890 } 891 892 private void selectCompatibleArchives(Object pkg, ITreeContentProvider provider) { 893 for (Object archive : provider.getChildren(pkg)) { 894 if (archive instanceof Archive) { 895 mTreeViewer.setChecked(archive, ((Archive) archive).isCompatible()); 896 } 897 } 898 } 899 900 /** 901 * Checks all PkgItems that are either new or have updates or select top platform 902 * for initial run. 903 */ 904 private void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) { 905 // This does not update the tree itself, syncViewerSelection does it below. 906 mDiffLogic.checkNewUpdateItems( 907 selectNew, 908 selectUpdates, 909 selectTop, 910 SdkConstants.CURRENT_PLATFORM); 911 syncViewerSelection(); 912 updateButtonsState(); 913 } 914 915 /** 916 * Deselect all checked PkgItems. 917 */ 918 private void onDeselectAll() { 919 // This does not update the tree itself, syncViewerSelection does it below. 920 mDiffLogic.uncheckAllItems(); 921 syncViewerSelection(); 922 updateButtonsState(); 923 } 924 925 /** 926 * When switching between the tree-by-api and the tree-by-source, copy the selection 927 * (aka the checked items) from one list to the other. 928 * This does not update the tree itself. 929 */ 930 private void copySelection(boolean fromSourceToApi) { 931 List<PkgItem> fromItems = mDiffLogic.getAllPkgItems(!fromSourceToApi, fromSourceToApi); 932 List<PkgItem> toItems = mDiffLogic.getAllPkgItems(fromSourceToApi, !fromSourceToApi); 933 934 // deselect all targets 935 for (PkgItem item : toItems) { 936 item.setChecked(false); 937 } 938 939 // mark new one from the source 940 for (PkgItem source : fromItems) { 941 if (source.isChecked()) { 942 // There should typically be a corresponding item in the target side 943 for (PkgItem target : toItems) { 944 if (target.isSameMainPackageAs(source.getMainPackage())) { 945 target.setChecked(true); 946 break; 947 } 948 } 949 } 950 } 951 } 952 953 /** 954 * Synchronize the 'checked' state of PkgItems in the tree with their internal isChecked state. 955 */ 956 private void syncViewerSelection() { 957 ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider(); 958 959 Object input = mTreeViewer.getInput(); 960 if (input == null) { 961 return; 962 } 963 for (Object cat : provider.getElements(input)) { 964 Object[] children = provider.getElements(cat); 965 boolean allChecked = children.length > 0; 966 for (Object child : children) { 967 if (child instanceof PkgItem) { 968 PkgItem item = (PkgItem) child; 969 boolean checked = item.isChecked(); 970 allChecked &= checked; 971 972 if (checked != mTreeViewer.getChecked(item)) { 973 if (checked) { 974 if (!mTreeViewer.getExpandedState(cat)) { 975 mTreeViewer.setExpandedState(cat, true); 976 } 977 } 978 checkAndExpandItem(item, checked, true/*fixChildren*/, false/*fixParent*/); 979 } 980 } 981 } 982 983 if (allChecked != mTreeViewer.getChecked(cat)) { 984 mTreeViewer.setChecked(cat, allChecked); 985 } 986 } 987 } 988 989 /** 990 * Indicate an install/delete operation is pending. 991 * This disables the install/delete buttons. 992 * Use {@link #endOperationPending()} to revert, typically in a {@code try..finally} block. 993 */ 994 private void beginOperationPending() { 995 mOperationPending = true; 996 updateButtonsState(); 997 } 998 999 private void endOperationPending() { 1000 mOperationPending = false; 1001 updateButtonsState(); 1002 } 1003 1004 /** 1005 * Updates the Install and Delete Package buttons. 1006 */ 1007 private void updateButtonsState() { 1008 if (!mButtonInstall.isDisposed()) { 1009 int numPackages = getArchivesForInstall(null /*archives*/); 1010 1011 mButtonInstall.setEnabled((numPackages > 0) && !mOperationPending); 1012 mButtonInstall.setText( 1013 numPackages == 0 ? "Install packages..." : // disabled button case 1014 numPackages == 1 ? "Install 1 package..." : 1015 String.format("Install %d packages...", numPackages)); 1016 } 1017 1018 if (!mButtonDelete.isDisposed()) { 1019 // We can only delete local archives 1020 int numPackages = getArchivesToDelete(null /*outMsg*/, null /*outArchives*/); 1021 1022 mButtonDelete.setEnabled((numPackages > 0) && !mOperationPending); 1023 mButtonDelete.setText( 1024 numPackages == 0 ? "Delete packages..." : // disabled button case 1025 numPackages == 1 ? "Delete 1 package..." : 1026 String.format("Delete %d packages...", numPackages)); 1027 } 1028 } 1029 1030 /** 1031 * Called when the Install Package button is selected. 1032 * Collects the packages to be installed and shows the installation window. 1033 */ 1034 private void onButtonInstall() { 1035 ArrayList<Archive> archives = new ArrayList<Archive>(); 1036 getArchivesForInstall(archives); 1037 1038 if (mUpdaterData != null) { 1039 boolean needsRefresh = false; 1040 try { 1041 beginOperationPending(); 1042 1043 List<Archive> installed = mUpdaterData.updateOrInstallAll_WithGUI( 1044 archives, 1045 mCheckFilterObsolete.getSelection() /* includeObsoletes */, 1046 mContext == SdkInvocationContext.IDE ? 1047 UpdaterData.TOOLS_MSG_UPDATED_FROM_ADT : 1048 UpdaterData.TOOLS_MSG_UPDATED_FROM_SDKMAN); 1049 needsRefresh = installed != null && !installed.isEmpty(); 1050 } finally { 1051 endOperationPending(); 1052 1053 if (needsRefresh) { 1054 // The local package list has changed, make sure to refresh it 1055 localReload(); 1056 } 1057 } 1058 } 1059 } 1060 1061 /** 1062 * Selects the archives that can be installed. 1063 * This can be used with a null {@code outArchives} just to count the number of 1064 * installable archives. 1065 * 1066 * @param outArchives An archive list where to add the archives that can be installed. 1067 * This can be null. 1068 * @return The number of archives that can be installed. 1069 */ 1070 private int getArchivesForInstall(List<Archive> outArchives) { 1071 if (mTreeViewer == null || 1072 mTreeViewer.getTree() == null || 1073 mTreeViewer.getTree().isDisposed()) { 1074 return 0; 1075 } 1076 Object[] checked = mTreeViewer.getCheckedElements(); 1077 if (checked == null) { 1078 return 0; 1079 } 1080 1081 int count = 0; 1082 1083 // Give us a way to force install of incompatible archives. 1084 boolean checkIsCompatible = 1085 System.getenv(ArchiveInstaller.ENV_VAR_IGNORE_COMPAT) == null; 1086 1087 if (mDisplayArchives) { 1088 // In detail mode, we display archives so we can install only the 1089 // archives that are actually selected. 1090 1091 for (Object c : checked) { 1092 if (c instanceof Archive) { 1093 Archive a = (Archive) c; 1094 if (a != null) { 1095 if (checkIsCompatible && !a.isCompatible()) { 1096 continue; 1097 } 1098 count++; 1099 if (outArchives != null) { 1100 outArchives.add((Archive) c); 1101 } 1102 } 1103 } 1104 } 1105 } else { 1106 // In non-detail mode, we install all the compatible archives 1107 // found in the selected pkg items. We also automatically 1108 // select update packages rather than the root package if any. 1109 1110 for (Object c : checked) { 1111 Package p = null; 1112 if (c instanceof Package) { 1113 // This is an update package 1114 p = (Package) c; 1115 } else if (c instanceof PkgItem) { 1116 p = ((PkgItem) c).getMainPackage(); 1117 1118 PkgItem pi = (PkgItem) c; 1119 if (pi.getState() == PkgState.INSTALLED) { 1120 // We don't allow installing items that are already installed 1121 // unless they have a pending update. 1122 p = pi.getUpdatePkg(); 1123 1124 } else if (pi.getState() == PkgState.NEW) { 1125 p = pi.getMainPackage(); 1126 } 1127 } 1128 if (p != null) { 1129 for (Archive a : p.getArchives()) { 1130 if (a != null) { 1131 if (checkIsCompatible && !a.isCompatible()) { 1132 continue; 1133 } 1134 count++; 1135 if (outArchives != null) { 1136 outArchives.add(a); 1137 } 1138 } 1139 } 1140 } 1141 } 1142 } 1143 1144 return count; 1145 } 1146 1147 /** 1148 * Called when the Delete Package button is selected. 1149 * Collects the packages to be deleted, prompt the user for confirmation 1150 * and actually performs the deletion. 1151 */ 1152 private void onButtonDelete() { 1153 final String title = "Delete SDK Package"; 1154 StringBuilder msg = new StringBuilder("Are you sure you want to delete:"); 1155 1156 // A list of archives to delete 1157 final ArrayList<Archive> archives = new ArrayList<Archive>(); 1158 1159 getArchivesToDelete(msg, archives); 1160 1161 if (!archives.isEmpty()) { 1162 msg.append("\n").append("This cannot be undone."); //$NON-NLS-1$ 1163 if (MessageDialog.openQuestion(getShell(), title, msg.toString())) { 1164 try { 1165 beginOperationPending(); 1166 1167 mUpdaterData.getTaskFactory().start("Delete Package", new ITask() { 1168 @Override 1169 public void run(ITaskMonitor monitor) { 1170 monitor.setProgressMax(archives.size() + 1); 1171 for (Archive a : archives) { 1172 monitor.setDescription("Deleting '%1$s' (%2$s)", 1173 a.getParentPackage().getShortDescription(), 1174 a.getLocalOsPath()); 1175 1176 // Delete the actual package 1177 a.deleteLocal(); 1178 1179 monitor.incProgress(1); 1180 if (monitor.isCancelRequested()) { 1181 break; 1182 } 1183 } 1184 1185 monitor.incProgress(1); 1186 monitor.setDescription("Done"); 1187 } 1188 }); 1189 } finally { 1190 endOperationPending(); 1191 1192 // The local package list has changed, make sure to refresh it 1193 localReload(); 1194 } 1195 } 1196 } 1197 } 1198 1199 /** 1200 * Selects the archives that can be deleted and collect their names. 1201 * This can be used with a null {@code outArchives} and a null {@code outMsg} 1202 * just to count the number of archives to be deleted. 1203 * 1204 * @param outMsg A StringBuilder where the names of the packages to be deleted is 1205 * accumulated. This is used to confirm deletion with the user. 1206 * @param outArchives An archive list where to add the archives that can be installed. 1207 * This can be null. 1208 * @return The number of archives that can be deleted. 1209 */ 1210 private int getArchivesToDelete(StringBuilder outMsg, List<Archive> outArchives) { 1211 if (mTreeViewer == null || 1212 mTreeViewer.getTree() == null || 1213 mTreeViewer.getTree().isDisposed()) { 1214 return 0; 1215 } 1216 Object[] checked = mTreeViewer.getCheckedElements(); 1217 if (checked == null) { 1218 // This should not happen since the button should be disabled 1219 return 0; 1220 } 1221 1222 int count = 0; 1223 1224 if (mDisplayArchives) { 1225 // In detail mode, select archives that can be deleted 1226 1227 for (Object c : checked) { 1228 if (c instanceof Archive) { 1229 Archive a = (Archive) c; 1230 if (a != null && a.isLocal()) { 1231 count++; 1232 if (outMsg != null) { 1233 String osPath = a.getLocalOsPath(); 1234 File dir = new File(osPath); 1235 Package p = a.getParentPackage(); 1236 if (p != null && dir.isDirectory()) { 1237 outMsg.append("\n - ") //$NON-NLS-1$ 1238 .append(p.getShortDescription()); 1239 } 1240 } 1241 if (outArchives != null) { 1242 outArchives.add(a); 1243 } 1244 } 1245 } 1246 } 1247 } else { 1248 // In non-detail mode, select archives of selected packages that can be deleted. 1249 1250 for (Object c : checked) { 1251 if (c instanceof PkgItem) { 1252 PkgItem pi = (PkgItem) c; 1253 PkgState state = pi.getState(); 1254 if (state == PkgState.INSTALLED) { 1255 Package p = pi.getMainPackage(); 1256 1257 for (Archive a : p.getArchives()) { 1258 if (a != null && a.isLocal()) { 1259 count++; 1260 if (outMsg != null) { 1261 String osPath = a.getLocalOsPath(); 1262 File dir = new File(osPath); 1263 if (dir.isDirectory()) { 1264 outMsg.append("\n - ") //$NON-NLS-1$ 1265 .append(p.getShortDescription()); 1266 } 1267 } 1268 if (outArchives != null) { 1269 outArchives.add(a); 1270 } 1271 } 1272 } 1273 } 1274 } 1275 } 1276 } 1277 1278 return count; 1279 } 1280 1281 // ---------------------- 1282 1283 public class PkgCellLabelProvider extends ColumnLabelProvider implements ITableFontProvider { 1284 1285 private final TreeViewerColumn mColumn; 1286 1287 public PkgCellLabelProvider(TreeViewerColumn column) { 1288 super(); 1289 mColumn = column; 1290 } 1291 1292 @Override 1293 public String getText(Object element) { 1294 1295 if (mColumn == mColumnName) { 1296 if (element instanceof PkgCategory) { 1297 return ((PkgCategory) element).getLabel(); 1298 } else if (element instanceof PkgItem) { 1299 return getPkgItemName((PkgItem) element); 1300 } else if (element instanceof IDescription) { 1301 return ((IDescription) element).getShortDescription(); 1302 } 1303 1304 } else if (mColumn == mColumnApi) { 1305 int api = -1; 1306 if (element instanceof PkgItem) { 1307 api = ((PkgItem) element).getApi(); 1308 } 1309 if (api >= 1) { 1310 return Integer.toString(api); 1311 } 1312 1313 } else if (mColumn == mColumnRevision) { 1314 if (element instanceof PkgItem) { 1315 PkgItem pkg = (PkgItem) element; 1316 return Integer.toString(pkg.getRevision()); 1317 } 1318 1319 } else if (mColumn == mColumnStatus) { 1320 if (element instanceof PkgItem) { 1321 PkgItem pkg = (PkgItem) element; 1322 1323 switch(pkg.getState()) { 1324 case INSTALLED: 1325 Package update = pkg.getUpdatePkg(); 1326 if (update != null) { 1327 return String.format( 1328 "Update available: rev. %1$s", 1329 update.getRevision()); 1330 } 1331 return "Installed"; 1332 1333 case NEW: 1334 Package p = pkg.getMainPackage(); 1335 if (p != null && p.hasCompatibleArchive()) { 1336 return "Not installed"; 1337 } else { 1338 return String.format("Not compatible with %1$s", 1339 SdkConstants.currentPlatformName()); 1340 } 1341 } 1342 return pkg.getState().toString(); 1343 1344 } else if (element instanceof Package) { 1345 // This is an update package. 1346 return "New revision " + Integer.toString(((Package) element).getRevision()); 1347 } 1348 } 1349 1350 return ""; //$NON-NLS-1$ 1351 } 1352 1353 private String getPkgItemName(PkgItem item) { 1354 String name = item.getName().trim(); 1355 1356 if (isSortByApi()) { 1357 // When sorting by API, the package name might contains the API number 1358 // or the platform name at the end. If we find it, cut it out since it's 1359 // redundant. 1360 1361 PkgCategoryApi cat = (PkgCategoryApi) findCategoryForItem(item); 1362 String apiLabel = cat.getApiLabel(); 1363 String platLabel = cat.getPlatformName(); 1364 1365 if (platLabel != null && name.endsWith(platLabel)) { 1366 return name.substring(0, name.length() - platLabel.length()); 1367 1368 } else if (apiLabel != null && name.endsWith(apiLabel)) { 1369 return name.substring(0, name.length() - apiLabel.length()); 1370 1371 } else if (platLabel != null && item.isObsolete() && name.indexOf(platLabel) > 0) { 1372 // For obsolete items, the format is "<base name> <platform name> (Obsolete)" 1373 // so in this case only accept removing a platform name that is not at 1374 // the end. 1375 name = name.replace(platLabel, ""); //$NON-NLS-1$ 1376 } 1377 } 1378 1379 // Collapse potential duplicated spacing 1380 name = name.replaceAll(" +", " "); //$NON-NLS-1$ //$NON-NLS-2$ 1381 1382 return name; 1383 } 1384 1385 private PkgCategory findCategoryForItem(PkgItem item) { 1386 List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi()); 1387 for (PkgCategory cat : cats) { 1388 for (PkgItem i : cat.getItems()) { 1389 if (i == item) { 1390 return cat; 1391 } 1392 } 1393 } 1394 1395 return null; 1396 } 1397 1398 @Override 1399 public Image getImage(Object element) { 1400 ImageFactory imgFactory = mUpdaterData.getImageFactory(); 1401 1402 if (imgFactory != null) { 1403 if (mColumn == mColumnName) { 1404 if (element instanceof PkgCategory) { 1405 return imgFactory.getImageForObject(((PkgCategory) element).getIconRef()); 1406 } else if (element instanceof PkgItem) { 1407 return imgFactory.getImageForObject(((PkgItem) element).getMainPackage()); 1408 } 1409 return imgFactory.getImageForObject(element); 1410 1411 } else if (mColumn == mColumnStatus && element instanceof PkgItem) { 1412 PkgItem pi = (PkgItem) element; 1413 switch(pi.getState()) { 1414 case INSTALLED: 1415 if (pi.hasUpdatePkg()) { 1416 return imgFactory.getImageByName(ICON_PKG_UPDATE); 1417 } else { 1418 return imgFactory.getImageByName(ICON_PKG_INSTALLED); 1419 } 1420 case NEW: 1421 Package p = pi.getMainPackage(); 1422 if (p != null && p.hasCompatibleArchive()) { 1423 return imgFactory.getImageByName(ICON_PKG_NEW); 1424 } else { 1425 return imgFactory.getImageByName(ICON_PKG_INCOMPAT); 1426 } 1427 } 1428 } 1429 } 1430 return super.getImage(element); 1431 } 1432 1433 // -- ITableFontProvider 1434 1435 @Override 1436 public Font getFont(Object element, int columnIndex) { 1437 if (element instanceof PkgItem) { 1438 if (((PkgItem) element).getState() == PkgState.NEW) { 1439 return mTreeFontItalic; 1440 } 1441 } else if (element instanceof Package) { 1442 // update package 1443 return mTreeFontItalic; 1444 } 1445 return super.getFont(element); 1446 } 1447 1448 // -- Tooltip support 1449 1450 @Override 1451 public String getToolTipText(Object element) { 1452 PkgItem pi = element instanceof PkgItem ? (PkgItem) element : null; 1453 if (pi != null) { 1454 element = pi.getMainPackage(); 1455 } 1456 if (element instanceof IDescription) { 1457 String s = getTooltipDescription((IDescription) element); 1458 1459 if (pi != null && pi.hasUpdatePkg()) { 1460 s += "\n-----------------" + //$NON-NLS-1$ 1461 "\nUpdate Available:\n" + //$NON-NLS-1$ 1462 getTooltipDescription(pi.getUpdatePkg()); 1463 } 1464 1465 return s; 1466 } 1467 return super.getToolTipText(element); 1468 } 1469 1470 private String getTooltipDescription(IDescription element) { 1471 String s = element.getLongDescription(); 1472 if (element instanceof Package) { 1473 Package p = (Package) element; 1474 1475 if (!p.isLocal()) { 1476 // For non-installed item, try to find a download size 1477 for (Archive a : p.getArchives()) { 1478 if (!a.isLocal() && a.isCompatible()) { 1479 s += '\n' + a.getSizeDescription(); 1480 break; 1481 } 1482 } 1483 } 1484 1485 // Display info about where this package comes/came from 1486 SdkSource src = p.getParentSource(); 1487 if (src != null) { 1488 try { 1489 URL url = new URL(src.getUrl()); 1490 String host = url.getHost(); 1491 if (p.isLocal()) { 1492 s += String.format("\nInstalled from %1$s", host); 1493 } else { 1494 s += String.format("\nProvided by %1$s", host); 1495 } 1496 } catch (MalformedURLException ignore) { 1497 } 1498 } 1499 } 1500 return s; 1501 } 1502 1503 @Override 1504 public Point getToolTipShift(Object object) { 1505 return new Point(15, 5); 1506 } 1507 1508 @Override 1509 public int getToolTipDisplayDelayTime(Object object) { 1510 return 500; 1511 } 1512 } 1513 1514 // --- Implementation of ISdkChangeListener --- 1515 1516 @Override 1517 public void onSdkLoaded() { 1518 onSdkReload(); 1519 } 1520 1521 @Override 1522 public void onSdkReload() { 1523 // The sdkmanager finished reloading its data. We must not call localReload() from here 1524 // since we don't want to alter the sdkmanager's data that just finished loading. 1525 loadPackages(); 1526 } 1527 1528 @Override 1529 public void preInstallHook() { 1530 // nothing to be done for now. 1531 } 1532 1533 @Override 1534 public void postInstallHook() { 1535 // nothing to be done for now. 1536 } 1537 1538 1539 // --- End of hiding from SWT Designer --- 1540 //$hide<<$ 1541 } 1542