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.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