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