OSDN Git Service

Revamp of the SDK Manager UI.
authorRaphael Moll <ralf@android.com>
Sat, 26 Feb 2011 03:37:29 +0000 (19:37 -0800)
committerRaphael Moll <ralf@android.com>
Wed, 16 Mar 2011 21:33:50 +0000 (14:33 -0700)
This replaces the Installed Packages and
Available Packages page by a single one that
combines both installed and available updates.

This is still experimental and is actually
not enabled unless the env var EXPERIMENTAL is
set.

Change-Id: I5ec5776da69d2668ce746c07df022bf5adc6fbf7

17 files changed:
sdkmanager/app/src/com/android/sdkmanager/Main.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/AddonPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/BrokenPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/DocPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ExtraPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Package.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/PlatformToolPackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/SamplePackage.java
sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ToolPackage.java
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IPageListener.java [new file with mode: 0755]
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/LocalPackagesPage.java
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackagesPage.java [new file with mode: 0755]
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/RemotePackagesPage.java
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterWindowImpl.java
sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/icons/ImageFactory.java
sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/UpdaterDataTest.java

index 4b0581f..3e50a90 100644 (file)
@@ -20,23 +20,23 @@ import com.android.io.FileWrapper;
 import com.android.prefs.AndroidLocation;
 import com.android.prefs.AndroidLocation.AndroidLocationException;
 import com.android.sdklib.IAndroidTarget;
-import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
 import com.android.sdklib.ISdkLog;
 import com.android.sdklib.SdkConstants;
 import com.android.sdklib.SdkManager;
+import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
 import com.android.sdklib.internal.avd.AvdManager;
-import com.android.sdklib.internal.avd.AvdManager.AvdInfo;
 import com.android.sdklib.internal.avd.HardwareProperties;
+import com.android.sdklib.internal.avd.AvdManager.AvdInfo;
 import com.android.sdklib.internal.avd.HardwareProperties.HardwareProperty;
 import com.android.sdklib.internal.project.ProjectCreator;
-import com.android.sdklib.internal.project.ProjectCreator.OutputLevel;
 import com.android.sdklib.internal.project.ProjectProperties;
+import com.android.sdklib.internal.project.ProjectCreator.OutputLevel;
 import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
 import com.android.sdklib.repository.SdkRepoConstants;
 import com.android.sdklib.xml.AndroidXPathFactory;
 import com.android.sdkmanager.internal.repository.AboutPage;
 import com.android.sdkmanager.internal.repository.SettingsPage;
-import com.android.sdkuilib.internal.repository.LocalPackagesPage;
+import com.android.sdkuilib.internal.repository.PackagesPage;
 import com.android.sdkuilib.internal.repository.UpdateNoWindow;
 import com.android.sdkuilib.internal.widgets.MessageBoxLog;
 import com.android.sdkuilib.repository.UpdaterWindow;
@@ -306,7 +306,7 @@ public class Main {
             window.registerPage("Settings", SettingsPage.class);
             window.registerPage("About", AboutPage.class);
             if (autoUpdate) {
-                window.setInitialPage(LocalPackagesPage.class);
+                window.setInitialPage(PackagesPage.class);
                 window.setRequestAutoUpdate(true);
             }
             window.open();
index bed9174..526bfcb 100755 (executable)
@@ -247,7 +247,22 @@ public class AddonPackage extends Package
         return mLibs;\r
     }\r
 \r
-    /** Returns a short description for an {@link IDescription}. */\r
+    /**\r
+     * Returns a description of this package that is suitable for a list display.\r
+     * <p/>\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public String getListDescription() {\r
+        return String.format("%1$s by %2$s%3$s",\r
+                getName(),\r
+                getVendor(),\r
+                isObsolete() ? " (Obsolete)" : "");\r
+    }\r
+\r
+    /**\r
+     * Returns a short description for an {@link IDescription}.\r
+     */\r
     @Override\r
     public String getShortDescription() {\r
         return String.format("%1$s by %2$s, Android API %3$s, revision %4$s%5$s",\r
index 1629045..ca6f463 100755 (executable)
@@ -99,7 +99,19 @@ public class BrokenPackage extends Package
         return mExactApiLevel;\r
     }\r
 \r
-    /** Returns a short description for an {@link IDescription}. */\r
+    /**\r
+     * Returns a description of this package that is suitable for a list display.\r
+     * <p/>\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public String getListDescription() {\r
+        return mShortDescription;\r
+    }\r
+\r
+    /**\r
+     * Returns a short description for an {@link IDescription}.\r
+     */\r
     @Override\r
     public String getShortDescription() {\r
         return mShortDescription;\r
index 8a4c19d..5171454 100755 (executable)
@@ -117,13 +117,35 @@ public class DocPackage extends Package implements IPackageVersion {
         mVersion.saveProperties(props);\r
     }\r
 \r
-    /** Returns the version, for platform, add-on and doc packages.\r
-     *  Can be 0 if this is a local package of unknown api-level. */\r
+    /**\r
+     * Returns the version, for platform, add-on and doc packages.\r
+     * Can be 0 if this is a local package of unknown api-level.\r
+     */\r
     public AndroidVersion getVersion() {\r
         return mVersion;\r
     }\r
 \r
-    /** Returns a short description for an {@link IDescription}. */\r
+    /**\r
+     * Returns a description of this package that is suitable for a list display.\r
+     * <p/>\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public String getListDescription() {\r
+        if (mVersion.isPreview()) {\r
+            return String.format("Documentation for Android '%1$s' Preview SDK%2$s",\r
+                    mVersion.getCodename(),\r
+                    isObsolete() ? " (Obsolete)" : "");\r
+        } else {\r
+            return String.format("Documentation for Android SDK%2$s",\r
+                    mVersion.getApiLevel(),\r
+                    isObsolete() ? " (Obsolete)" : "");\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Returns a short description for an {@link IDescription}.\r
+     */\r
     @Override\r
     public String getShortDescription() {\r
         if (mVersion.isPreview()) {\r
index 3472544..bdf2805 100755 (executable)
@@ -253,9 +253,7 @@ public class ExtraPackage extends MinToolsPackage
         return ""; //$NON-NLS-1$\r
     }\r
 \r
-    /** Returns a short description for an {@link IDescription}. */\r
-    @Override\r
-    public String getShortDescription() {\r
+    private String getPrettyName() {\r
         String name = mPath;\r
 \r
         // In the past, we used to save the extras in a folder vendor-path,\r
@@ -299,8 +297,31 @@ public class ExtraPackage extends MinToolsPackage
         name = name.replaceAll(" Usb ", " USB ");   //$NON-NLS-1$\r
         name = name.replaceAll(" Api ", " API ");   //$NON-NLS-1$\r
 \r
+        return name;\r
+    }\r
+\r
+    /**\r
+     * Returns a description of this package that is suitable for a list display.\r
+     * <p/>\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public String getListDescription() {\r
+        String s = String.format("%1$s package%2$s",\r
+                getPrettyName(),\r
+                isObsolete() ? " (Obsolete)" : "");  //$NON-NLS-2$\r
+\r
+        return s;\r
+    }\r
+\r
+    /**\r
+     * Returns a short description for an {@link IDescription}.\r
+     */\r
+    @Override\r
+    public String getShortDescription() {\r
+\r
         String s = String.format("%1$s package, revision %2$d%3$s",\r
-                name,\r
+                getPrettyName(),\r
                 getRevision(),\r
                 isObsolete() ? " (Obsolete)" : "");  //$NON-NLS-2$\r
 \r
index 58be3c9..c597ad8 100755 (executable)
@@ -372,6 +372,17 @@ public abstract class Package implements IDescription, Comparable<Package> {
     }\r
 \r
     /**\r
+     * Returns a description of this package that is suitable for a list display.\r
+     * Should not be empty. Must never be null.\r
+     * <p/>\r
+     * Note that this is the "base" name for the package\r
+     * with no specific revision nor API mentionned.\r
+     * In contrast, {@link #getShortDescription()} should be used if you want more details\r
+     * such as the package revision number or the API, if applicable.\r
+     */\r
+    public abstract String getListDescription();\r
+\r
+    /**\r
      * Returns a short description for an {@link IDescription}.\r
      * Can be empty but not null.\r
      */\r
index c303e2f..622a922 100755 (executable)
@@ -121,7 +121,31 @@ public class PlatformPackage extends MinToolsPackage implements IPackageVersion
         return mVersion;\r
     }\r
 \r
-    /** Returns a short description for an {@link IDescription}. */\r
+    /**\r
+     * Returns a description of this package that is suitable for a list display.\r
+     * <p/>\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public String getListDescription() {\r
+        String s;\r
+\r
+        if (mVersion.isPreview()) {\r
+            s = String.format("SDK Platform Android %1$s Preview%2$s",\r
+                    getVersionName(),\r
+                    isObsolete() ? " (Obsolete)" : "");  //$NON-NLS-2$\r
+        } else {\r
+            s = String.format("SDK Platform Android %1$s%2$s",\r
+                getVersionName(),\r
+                isObsolete() ? " (Obsolete)" : "");      //$NON-NLS-2$\r
+        }\r
+\r
+        return s;\r
+    }\r
+\r
+    /**\r
+     * Returns a short description for an {@link IDescription}.\r
+     */\r
     @Override\r
     public String getShortDescription() {\r
         String s;\r
@@ -130,13 +154,13 @@ public class PlatformPackage extends MinToolsPackage implements IPackageVersion
             s = String.format("SDK Platform Android %1$s Preview, revision %2$s%3$s",\r
                     getVersionName(),\r
                     getRevision(),\r
-                    isObsolete() ? " (Obsolete)" : "");\r
+                    isObsolete() ? " (Obsolete)" : "");  //$NON-NLS-2$\r
         } else {\r
             s = String.format("SDK Platform Android %1$s, API %2$d, revision %3$s%4$s",\r
                 getVersionName(),\r
                 mVersion.getApiLevel(),\r
                 getRevision(),\r
-                isObsolete() ? " (Obsolete)" : "");\r
+                isObsolete() ? " (Obsolete)" : "");      //$NON-NLS-2$\r
         }\r
 \r
         return s;\r
index 860d703..9a2de62 100755 (executable)
@@ -143,7 +143,20 @@ public class PlatformToolPackage extends Package {
                 archiveOsPath);
     }
 
-    /** Returns a short description for an {@link IDescription}. */
+    /**
+     * Returns a description of this package that is suitable for a list display.
+     * <p/>
+     * {@inheritDoc}
+     */
+    @Override
+    public String getListDescription() {
+        return String.format("Android SDK Platform-tools%1$s",
+                isObsolete() ? " (Obsolete)" : "");
+    }
+
+    /**
+     * Returns a short description for an {@link IDescription}.
+     */
     @Override
     public String getShortDescription() {
         return String.format("Android SDK Platform-tools, revision %1$d%2$s",
index 035677b..b436b91 100755 (executable)
@@ -175,7 +175,23 @@ public class SamplePackage extends MinToolsPackage
         return mVersion;\r
     }\r
 \r
-    /** Returns a short description for an {@link IDescription}. */\r
+    /**\r
+     * Returns a description of this package that is suitable for a list display.\r
+     * <p/>\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public String getListDescription() {\r
+        String s = String.format("Samples for SDK API %1$s%2$s%3$s",\r
+                mVersion.getApiString(),\r
+                mVersion.isPreview() ? " Preview" : "",\r
+                isObsolete() ? " (Obsolete)" : "");\r
+        return s;\r
+    }\r
+\r
+    /**\r
+     * Returns a short description for an {@link IDescription}.\r
+     */\r
     @Override\r
     public String getShortDescription() {\r
         String s = String.format("Samples for SDK API %1$s%2$s, revision %3$d%4$s",\r
index 6e53dd7..827c721 100755 (executable)
@@ -151,7 +151,20 @@ public class ToolPackage extends Package implements IMinPlatformToolsDependency
         return mMinPlatformToolsRevision;\r
     }\r
 \r
-    /** Returns a short description for an {@link IDescription}. */\r
+    /**\r
+     * Returns a description of this package that is suitable for a list display.\r
+     * <p/>\r
+     * {@inheritDoc}\r
+     */\r
+    @Override\r
+    public String getListDescription() {\r
+        return String.format("Android SDK Tools%1$s",\r
+                isObsolete() ? " (Obsolete)" : "");\r
+    }\r
+\r
+    /**\r
+     * Returns a short description for an {@link IDescription}.\r
+     */\r
     @Override\r
     public String getShortDescription() {\r
         return String.format("Android SDK Tools, revision %1$d%2$s",\r
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IPageListener.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/IPageListener.java
new file mode 100755 (executable)
index 0000000..fc59404
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+
+
+/**
+ * Interface for lifecycle events of pages.
+ */
+interface IPageListener {
+
+    /**
+     * The page was just selected and brought to front.
+     */
+    public void onPageSelected();
+}
index d579094..12bb93f 100755 (executable)
@@ -42,6 +42,9 @@ import org.eclipse.swt.widgets.TableColumn;
 \r
 import java.io.File;\r
 \r
+/**\r
+ * Page that display all locally installed packages from the current SDK.\r
+ */\r
 public class LocalPackagesPage extends Composite implements ISdkChangeListener {\r
 \r
     private final UpdaterData mUpdaterData;\r
diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackagesPage.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/PackagesPage.java
new file mode 100755 (executable)
index 0000000..51d20f7
--- /dev/null
@@ -0,0 +1,809 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+import com.android.sdklib.internal.repository.IPackageVersion;
+import com.android.sdklib.internal.repository.ITask;
+import com.android.sdklib.internal.repository.ITaskMonitor;
+import com.android.sdklib.internal.repository.Package;
+import com.android.sdklib.internal.repository.PlatformPackage;
+import com.android.sdklib.internal.repository.SdkSource;
+import com.android.sdklib.internal.repository.Package.UpdateInfo;
+import com.android.sdkuilib.internal.repository.icons.ImageFactory;
+import com.android.sdkuilib.repository.ISdkChangeListener;
+
+import org.eclipse.jface.viewers.CheckboxTreeViewer;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ITableColorProvider;
+import org.eclipse.jface.viewers.ITableFontProvider;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.LabelProvider;
+import org.eclipse.jface.viewers.TreeColumnViewerLabelProvider;
+import org.eclipse.jface.viewers.TreeViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Page that displays both locally installed packages as well as all known
+ * remote available packages. This gives an overview of what is installed
+ * vs what is available and allows the user to update or install packages.
+ */
+public class PackagesPage extends Composite
+        implements ISdkChangeListener, IPageListener {
+
+    private final List<PkgItem> mPackages = new ArrayList<PkgItem>();
+    private final List<PkgCategory> mCategories = new ArrayList<PkgCategory>();
+    private final UpdaterData mUpdaterData;
+
+    private Text mTextSdkOsPath;
+
+    private Button mCheckSortSource;
+    private Button mCheckSortApi;
+    private Button mCheckFilterObsolete;
+    private Button mCheckFilterInstalled;
+    private Button mCheckFilterNew;
+    private Composite mGroupOptions;
+    private Composite mGroupSdk;
+    private Group mGroupPackages;
+    private Composite mGroupButtons;
+    private Button mButtonDelete;
+    private Button mButtonInstall;
+    private Tree mTree;
+    private CheckboxTreeViewer mTreeViewer;
+    private TreeViewerColumn mColumnName;
+    private TreeViewerColumn mColumnApi;
+    private TreeViewerColumn mColumnRevision;
+    private TreeViewerColumn mColumnStatus;
+    private Color mColorUpdate;
+    private Color mColorNew;
+    private Font mTreeFontItalic;
+
+    public PackagesPage(Composite parent, UpdaterData updaterData) {
+        super(parent, SWT.BORDER);
+
+        mUpdaterData = updaterData;
+        createContents(this);
+        postCreate();  //$hide$
+    }
+
+    public void onPageSelected() {
+        if (mPackages.isEmpty()) {
+            // Initialize the package list the first time the page is shown.
+            loadPackages();
+        }
+    }
+
+    protected void createContents(Composite parent) {
+        parent.setLayout(new GridLayout(1, false));
+
+        mGroupSdk = new Composite(parent, SWT.NONE);
+        mGroupSdk.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+        mGroupSdk.setLayout(new GridLayout(2, false));
+
+        Label label1 = new Label(mGroupSdk, SWT.NONE);
+        label1.setText("SDK Path:");
+
+        mTextSdkOsPath = new Text(mGroupSdk, SWT.NONE);
+        mTextSdkOsPath.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+        mTextSdkOsPath.setEnabled(false);
+
+        mGroupPackages = new Group(parent, SWT.NONE);
+        mGroupPackages.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+        mGroupPackages.setText("Packages");
+        mGroupPackages.setLayout(new GridLayout(1, false));
+
+        mTreeViewer = new CheckboxTreeViewer(mGroupPackages, SWT.BORDER);
+        mTree = mTreeViewer.getTree();
+        mTree.setHeaderVisible(true);
+        mTree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+
+        mColumnName = new TreeViewerColumn(mTreeViewer, SWT.NONE);
+        TreeColumn treeColumn1 = mColumnName.getColumn();
+        treeColumn1.setImage(getImage("platform_pkg_16.png"));  //$NON-NLS-1$
+        treeColumn1.setWidth(290);
+        treeColumn1.setText("Name");
+
+        mColumnApi = new TreeViewerColumn(mTreeViewer, SWT.NONE);
+        TreeColumn treeColumn2 = mColumnApi.getColumn();
+        treeColumn2.setWidth(50);
+        treeColumn2.setText("API");
+
+        mColumnRevision = new TreeViewerColumn(mTreeViewer, SWT.NONE);
+        TreeColumn treeColumn3 = mColumnRevision.getColumn();
+        treeColumn3.setWidth(50);
+        treeColumn3.setText("Rev.");
+        treeColumn3.setToolTipText("Revision currently installed");
+
+
+        mColumnStatus = new TreeViewerColumn(mTreeViewer, SWT.NONE);
+        TreeColumn treeColumn4 = mColumnStatus.getColumn();
+        treeColumn4.setWidth(88);
+        treeColumn4.setText("Status");
+
+        mGroupOptions = new Composite(mGroupPackages, SWT.NONE);
+        mGroupOptions.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+        mGroupOptions.setLayout(new GridLayout(8, false));
+
+        Label label2 = new Label(mGroupOptions, SWT.NONE);
+        label2.setText("Sort by");
+
+        mCheckSortSource = new Button(mGroupOptions, SWT.RADIO);
+        mCheckSortSource.setToolTipText("Sort by Source");
+        mCheckSortSource.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                sortPackages();
+            }
+        });
+        mCheckSortSource.setImage(getImage("source_icon16.png"));  //$NON-NLS-1$
+        mCheckSortSource.setText("Source");
+
+        mCheckSortApi = new Button(mGroupOptions, SWT.RADIO);
+        mCheckSortApi.setToolTipText("Sort by API level");
+        mCheckSortApi.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                sortPackages();
+            }
+        });
+        mCheckSortApi.setImage(getImage("platform_pkg_16.png"));  //$NON-NLS-1$
+        mCheckSortApi.setText("API level");
+        mCheckSortApi.setSelection(true);
+
+                Label expandPlaceholder = new Label(mGroupOptions, SWT.NONE);
+                expandPlaceholder.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+
+                Label label3 = new Label(mGroupOptions, SWT.NONE);
+                label3.setText("Show:");
+
+                        mCheckFilterNew = new Button(mGroupOptions, SWT.CHECK);
+                        mCheckFilterNew.setToolTipText("Show Updates and New");
+                        mCheckFilterNew.addSelectionListener(new SelectionAdapter() {
+                            @Override
+                            public void widgetSelected(SelectionEvent e) {
+                                sortPackages();
+                            }
+                        });
+                        mCheckFilterNew.setImage(getImage("reject_icon16.png"));
+                        mCheckFilterNew.setSelection(true);
+                        mCheckFilterNew.setText("Updates/New");
+
+                        mCheckFilterInstalled = new Button(mGroupOptions, SWT.CHECK);
+                        mCheckFilterInstalled.setToolTipText("Show Installed");
+                        mCheckFilterInstalled.addSelectionListener(new SelectionAdapter() {
+                            @Override
+                            public void widgetSelected(SelectionEvent e) {
+                                sortPackages();
+                            }
+                        });
+                        mCheckFilterInstalled.setImage(getImage("accept_icon16.png"));  //$NON-NLS-1$
+                        mCheckFilterInstalled.setSelection(true);
+                        mCheckFilterInstalled.setText("Installed");
+
+                mCheckFilterObsolete = new Button(mGroupOptions, SWT.CHECK);
+                mCheckFilterObsolete.setToolTipText("Show Obsolete");
+                mCheckFilterObsolete.addSelectionListener(new SelectionAdapter() {
+                    @Override
+                    public void widgetSelected(SelectionEvent e) {
+                        sortPackages();
+                    }
+                });
+                mCheckFilterObsolete.setImage(getImage("nopkg_icon16.png"));  //$NON-NLS-1$
+                mCheckFilterObsolete.setSelection(false);
+                mCheckFilterObsolete.setText("Obsolete");
+
+        mGroupButtons = new Composite(parent, SWT.NONE);
+        mGroupButtons.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true,
+                false, 1, 1));
+        mGroupButtons.setLayout(new GridLayout(3, false));
+
+        mButtonDelete = new Button(mGroupButtons, SWT.NONE);
+        mButtonDelete.setText("Delete");
+
+        Label label4 = new Label(mGroupButtons, SWT.NONE);
+        label4.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
+
+        mButtonInstall = new Button(mGroupButtons, SWT.NONE);
+        mButtonInstall.setText("Install");
+    }
+
+    private Image getImage(String filename) {
+        if (mUpdaterData != null) {
+            ImageFactory imgFact = mUpdaterData.getImageFactory();
+            if (imgFact != null) {
+                imgFact.getImageByName(filename);
+            }
+        }
+        return null;
+    }
+
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+    private void postCreate() {
+        if (mUpdaterData != null) {
+            mTextSdkOsPath.setText(mUpdaterData.getOsSdkRoot());
+        }
+
+        mTreeViewer.setContentProvider(new PkgContentProvider());
+        //--mTreeViewer.setLabelProvider(new PkgLabelProvider());
+
+
+        mColumnApi.setLabelProvider(new TreeColumnViewerLabelProvider(new PkgCellLabelProvider(mColumnApi)));
+        mColumnName.setLabelProvider(new TreeColumnViewerLabelProvider(new PkgCellLabelProvider(mColumnName)));
+        mColumnStatus.setLabelProvider(new TreeColumnViewerLabelProvider(new PkgCellLabelProvider(mColumnStatus)));
+        mColumnRevision.setLabelProvider(new TreeColumnViewerLabelProvider(new PkgCellLabelProvider(mColumnRevision)));
+
+        FontData fontData = mTree.getFont().getFontData()[0];
+        fontData.setStyle(SWT.ITALIC);
+        mTreeFontItalic = new Font(mTree.getDisplay(), fontData);
+
+        mColorUpdate = new Color(mTree.getDisplay(), 0xff, 0xff, 0xcc);
+        mColorNew = new Color(mTree.getDisplay(), 0xff, 0xee, 0xcc);
+
+        mTree.addDisposeListener(new DisposeListener() {
+            public void widgetDisposed(DisposeEvent e) {
+                mTreeFontItalic.dispose();
+                mColorUpdate.dispose();
+                mColorNew.dispose();
+                mTreeFontItalic = null;
+                mColorUpdate = null;
+                mColorNew = null;
+            }
+        });
+    }
+
+    private void loadPackages() {
+        if (mUpdaterData == null) {
+            return;
+        }
+
+        mPackages.clear();
+
+        // get local packages
+        for (Package pkg : mUpdaterData.getInstalledPackages()) {
+            PkgItem pi = new PkgItem(pkg, PkgState.INSTALLED);
+            mPackages.add(pi);
+        }
+
+        // get remote packages
+        final boolean forceHttp = mUpdaterData.getSettingsController().getForceHttp();
+        mUpdaterData.loadRemoteAddonsList();
+        mUpdaterData.getTaskFactory().start("Loading Source", new ITask() {
+            public void run(ITaskMonitor monitor) {
+                for (SdkSource source : mUpdaterData.getSources().getAllSources()) {
+                    Package[] pkgs = source.getPackages();
+                    if (pkgs == null) {
+                        source.load(monitor, forceHttp);
+                        pkgs = source.getPackages();
+                    }
+                    if (pkgs == null) {
+                        continue;
+                    }
+
+                    nextPkg: for(Package pkg : pkgs) {
+                        boolean isUpdate = false;
+                        for (PkgItem pi: mPackages) {
+                            if (pi.isSameAs(pkg)) {
+                                continue nextPkg;
+                            }
+                            if (pi.isUpdatedBy(pkg)) {
+                                isUpdate = true;
+                                break;
+                            }
+                        }
+
+                        if (!isUpdate) {
+                            PkgItem pi = new PkgItem(pkg, PkgState.NEW_AVAILABLE);
+                            mPackages.add(pi);
+                        }
+                    }
+                }
+            }
+        });
+
+        sortPackages();
+    }
+
+    private void sortPackages() {
+        if (mCheckSortApi != null && !mCheckSortApi.isDisposed() && mCheckSortApi.getSelection()) {
+            sortByAPI();
+        } else {
+            sortBySource();
+        }
+    }
+
+    private void sortByAPI() {
+        mCategories.clear();
+
+        Set<Integer> apiSet = new HashSet<Integer>();
+        for (PkgItem item : mPackages) {
+            if (keepItem(item)) {
+                apiSet.add(item.getApi());
+            }
+        }
+
+        Integer[] apis = apiSet.toArray(new Integer[apiSet.size()]);
+        Arrays.sort(apis, new Comparator<Integer>() {
+            public int compare(Integer o1, Integer o2) {
+                return o2.compareTo(o1);
+            }
+        });
+
+        for (Integer api : apis) {
+            String name = api == -1 ? "Other" : "API " + api.toString();
+            Object iconRef = null;
+
+            List<PkgItem> items = new ArrayList<PkgItem>();
+            for (PkgItem item : mPackages) {
+                if (item.getApi() == api) {
+                    items.add(item);
+
+                    if (api != -1) {
+                        Package p = item.getPackage();
+                        if (p instanceof PlatformPackage) {
+                            String vn = ((PlatformPackage) p).getVersionName();
+                            name = String.format("%1$s (Android %2$s)", name, vn);
+                            iconRef = p;
+                        }
+                    }
+                }
+            }
+
+            PkgCategory cat = new PkgCategory(
+                    name,
+                    iconRef,
+                    items.toArray(new PkgItem[items.size()]));
+            mCategories.add(cat);
+        }
+
+        mTreeViewer.setInput(mCategories);
+
+        // expand all items by default
+        expandItem(mCategories);
+    }
+
+    private void sortBySource() {
+        mCategories.clear();
+
+        Set<SdkSource> sourceSet = new HashSet<SdkSource>();
+        for (PkgItem item : mPackages) {
+            if (keepItem(item)) {
+                sourceSet.add(item.getSource());
+            }
+        }
+
+        SdkSource[] sources = sourceSet.toArray(new SdkSource[sourceSet.size()]);
+        Arrays.sort(sources, new Comparator<SdkSource>() {
+            public int compare(SdkSource o1, SdkSource o2) {
+                if (o1 == o2) {
+                    return 0;
+                } else if (o1 == null && o2 != null) {
+                    return -1;
+                } else if (o1 != null && o2 == null) {
+                    return 1;
+                }
+                assert o1 != null;
+                return o1.toString().compareTo(o2.toString());
+            }
+        });
+
+        for (SdkSource source : sources) {
+            Object key = source != null ? source : "Installed Packages";
+
+            List<PkgItem> items = new ArrayList<PkgItem>();
+            for (PkgItem item : mPackages) {
+                if (item.getSource() == source) {
+                    items.add(item);
+                }
+            }
+
+            PkgCategory cat = new PkgCategory(
+                    key,
+                    key,
+                    items.toArray(new PkgItem[items.size()]));
+            mCategories.add(cat);
+        }
+
+        mTreeViewer.setInput(mCategories);
+
+        // expand all items by default
+        expandItem(mCategories);
+    }
+
+    private boolean keepItem(PkgItem item) {
+        if (!mCheckFilterObsolete.getSelection()) {
+            if (item.isObsolete()) {
+                return false;
+            }
+        }
+
+        if (!mCheckFilterInstalled.getSelection()) {
+            if (item.getState() == PkgState.INSTALLED) {
+                return false;
+            }
+        }
+
+        if (!mCheckFilterNew.getSelection()) {
+            if (item.getState() == PkgState.NEW_AVAILABLE ||
+                    item.getState() == PkgState.UPDATE_AVAILABLE) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private void expandItem(Object elem) {
+        //if (elem instanceof SdkSource || elem instanceof SdkSourceCategory) {
+            mTreeViewer.setExpandedState(elem, true);
+            for (Object pkg :
+                    ((ITreeContentProvider) mTreeViewer.getContentProvider()).getChildren(elem)) {
+                mTreeViewer.setChecked(pkg, true);
+                expandItem(pkg);
+            }
+        //}
+    }
+
+    // ----------------------
+
+    public class PkgCellLabelProvider extends ColumnLabelProvider
+        implements ITableFontProvider, ITableColorProvider {
+
+        private final TreeViewerColumn mColumn;
+
+        public PkgCellLabelProvider(TreeViewerColumn column) {
+            super();
+            mColumn = column;
+        }
+
+        @Override
+        public String getText(Object element) {
+            if (element instanceof PkgCategory) {
+                if (mColumn == mColumnName) {
+                    return ((PkgCategory) element).getLabel();
+                }
+            } else if (element instanceof PkgItem) {
+                PkgItem pkg = (PkgItem) element;
+
+                if (mColumn == mColumnName) {
+                    return pkg.getName();
+
+                } else if (mColumn == mColumnApi) {
+                    int api = pkg.getApi();
+                    if (api < 1) {
+                        return "";  //$NON-NLS-1$
+                    } else {
+                        return Integer.toString(api);
+                    }
+                } else if (mColumn == mColumnRevision) {
+                    if (pkg.getState() == PkgState.INSTALLED ||
+                            pkg.getState() == PkgState.UPDATE_AVAILABLE) {
+                        return Integer.toString(pkg.getRevision());
+                    }
+
+                } else if (mColumn == mColumnStatus) {
+                    switch(pkg.getState()) {
+                    case INSTALLED:
+                        return "Installed";
+                    case UPDATE_AVAILABLE:
+                        return "Rev. " + Integer.toString(pkg.getUpdateRev());
+                    case NEW_AVAILABLE:
+                        return "New rev. " + Integer.toString(pkg.getRevision());
+                    case LOCKED_NO_INSTALL:
+                        return "Locked";
+                    }
+                    return pkg.getState().toString();
+                }
+            } else if (element instanceof Package) {
+                return ((Package) element).getShortDescription();
+            }
+
+            return "";  //$NON-NLS-1$
+        }
+
+        @Override
+        public Image getImage(Object element) {
+            ImageFactory imgFactory = mUpdaterData.getImageFactory();
+
+            if (imgFactory != null) {
+                if (mColumn == mColumnName) {
+                    if (element instanceof PkgCategory) {
+                        return imgFactory.getImageForObject(((PkgCategory) element).getIconRef());
+                    }
+                    return imgFactory.getImageForObject(element);
+
+                } else if (mColumn == mColumnStatus && element instanceof PkgItem) {
+                    switch(((PkgItem) element).getState()) {
+                    case INSTALLED:
+                        return imgFactory.getImageByName("accept_icon16.png");
+                    case UPDATE_AVAILABLE:
+                    case NEW_AVAILABLE:
+                        return imgFactory.getImageByName("reject_icon16.png");
+                    case LOCKED_NO_INSTALL:
+                        return imgFactory.getImageByName("broken_pkg_16.png");
+                    }
+                }
+            }
+            return super.getImage(element);
+        }
+
+        // -- ITableFontProvider
+
+        public Font getFont(Object element, int columnIndex) {
+            if (element instanceof PkgItem) {
+                if (((PkgItem) element).getState() != PkgState.INSTALLED) {
+                    return mTreeFontItalic;
+                }
+            }
+            return super.getFont(element);
+        }
+
+        // -- ITableColorProvider
+
+        public Color getBackground(Object element, int columnIndex) {
+            if (element instanceof PkgItem) {
+                if (((PkgItem) element).getState() == PkgState.NEW_AVAILABLE) {
+                    return mColorNew;
+                } else if (((PkgItem) element).getState() == PkgState.UPDATE_AVAILABLE) {
+                        return mColorUpdate;
+                }
+            }
+            return null;
+        }
+
+        public Color getForeground(Object element, int columnIndex) {
+            // Not used
+            return null;
+        }
+    }
+
+    public class PkgLabelProvider extends LabelProvider {
+
+        @Override
+        public String getText(Object element) {
+            return super.getText(element);
+        }
+
+        @Override
+        public Image getImage(Object element) {
+            ImageFactory imgFactory = mUpdaterData.getImageFactory();
+
+            if (imgFactory != null) {
+                return imgFactory.getImageForObject(element);
+            }
+            return super.getImage(element);
+        }
+
+    }
+
+    public static class PkgContentProvider implements ITreeContentProvider {
+
+        public Object[] getChildren(Object parentElement) {
+
+            if (parentElement instanceof ArrayList<?>) {
+                return ((ArrayList<?>) parentElement).toArray();
+
+            } else if (parentElement instanceof PkgCategory) {
+                return ((PkgCategory) parentElement).getItems();
+
+            } else if (parentElement instanceof PkgItem) {
+                List<Package> pkgs = ((PkgItem) parentElement).getUpdatePkgs();
+                if (pkgs != null) {
+                    return pkgs.toArray();
+                }
+            }
+
+            return new Object[0];
+        }
+
+        public Object getParent(Object element) {
+            // TODO Auto-generated method stub
+            return null;
+        }
+
+        public boolean hasChildren(Object parentElement) {
+            if (parentElement instanceof ArrayList<?>) {
+                return true;
+
+            } else if (parentElement instanceof PkgCategory) {
+                return true;
+
+            } else if (parentElement instanceof PkgItem) {
+                List<Package> pkgs = ((PkgItem) parentElement).getUpdatePkgs();
+                if (pkgs != null) {
+                    return !pkgs.isEmpty();
+                }
+            }
+
+            return false;
+        }
+
+        public Object[] getElements(Object inputElement) {
+            return getChildren(inputElement);
+        }
+
+        public void dispose() {
+            // TODO Auto-generated method stub
+
+        }
+
+        public void inputChanged(Viewer arg0, Object arg1, Object arg2) {
+            // TODO Auto-generated method stub
+
+        }
+
+    }
+
+    public static class PkgCategory {
+        private final Object mKey;
+        private final PkgItem[] mItems;
+        private final Object mIconRef;
+
+        public PkgCategory(Object key, Object iconRef, PkgItem[] items) {
+            mKey = key;
+            mIconRef = iconRef;
+            mItems = items;
+        }
+
+        public String getLabel() {
+            return mKey.toString();
+        }
+
+        public Object getIconRef() {
+            return mIconRef;
+        }
+
+        public PkgItem[] getItems() {
+            return mItems;
+        }
+    }
+
+    public enum PkgState {
+        INSTALLED, UPDATE_AVAILABLE, NEW_AVAILABLE, LOCKED_NO_INSTALL
+    }
+
+    public static class PkgItem {
+        private final Package mPkg;
+        private PkgState mState;
+        private int mUpdateRev;
+        private List<Package> mUpdatePkgs;
+
+        public PkgItem(Package pkg, PkgState state) {
+            mPkg = pkg;
+            mState = state;
+        }
+
+        public boolean isObsolete() {
+            return mPkg.isObsolete();
+        }
+
+        public boolean isSameAs(Package pkg) {
+            return mPkg.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE;
+        }
+
+        /**
+         * Check whether the 'pkg' argument updates this package.
+         * If it does, record it as a sub-package.
+         * Returns true if it was recorded as an update, false otherwise.
+         */
+        public boolean isUpdatedBy(Package pkg) {
+            if (mPkg.canBeUpdatedBy(pkg) == UpdateInfo.UPDATE) {
+                if (mUpdatePkgs == null) {
+                    mUpdatePkgs = new ArrayList<Package>();
+                }
+                mUpdatePkgs.add(pkg);
+                mState = PkgState.UPDATE_AVAILABLE;
+                return true;
+            }
+
+            return false;
+        }
+
+        public String getName() {
+            return mPkg.getListDescription();
+        }
+
+        public int getRevision() {
+            return mPkg.getRevision();
+        }
+
+        public String getDescription() {
+            return mPkg.getDescription();
+        }
+
+        public Package getPackage() {
+            return mPkg;
+        }
+
+        public PkgState getState() {
+            return mState;
+        }
+
+        public SdkSource getSource() {
+            if (mState == PkgState.NEW_AVAILABLE) {
+                return mPkg.getParentSource();
+            } else {
+                return null;
+            }
+        }
+
+        public int getApi() {
+            return mPkg instanceof IPackageVersion ?
+                    ((IPackageVersion) mPkg).getVersion().getApiLevel() :
+                        -1;
+        }
+
+        public int getUpdateRev() {
+            return mUpdateRev;
+        }
+
+        public List<Package> getUpdatePkgs() {
+            return mUpdatePkgs;
+        }
+    }
+
+
+
+    // --- Implementation of ISdkChangeListener ---
+
+    public void onSdkLoaded() {
+        onSdkReload();
+    }
+
+    public void onSdkReload() {
+        loadPackages();
+    }
+
+    public void preInstallHook() {
+        // nothing to be done for now.
+    }
+
+    public void postInstallHook() {
+        // nothing to be done for now.
+    }
+
+    // --- End of hiding from SWT Designer ---
+    //$hide<<$
+
+}
index 6fbf060..0a70162 100755 (executable)
-/*\r
- * Copyright (C) 2009 The Android Open Source Project\r
- *\r
- * Licensed under the Apache License, Version 2.0 (the "License");\r
- * you may not use this file except in compliance with the License.\r
- * You may obtain a copy of the License at\r
- *\r
- *      http://www.apache.org/licenses/LICENSE-2.0\r
- *\r
- * Unless required by applicable law or agreed to in writing, software\r
- * distributed under the License is distributed on an "AS IS" BASIS,\r
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
- * See the License for the specific language governing permissions and\r
- * limitations under the License.\r
- */\r
-\r
-package com.android.sdkuilib.internal.repository;\r
-\r
-\r
-import com.android.sdklib.internal.repository.Archive;\r
-import com.android.sdklib.internal.repository.IDescription;\r
-import com.android.sdklib.internal.repository.Package;\r
-import com.android.sdklib.internal.repository.SdkAddonSource;\r
-import com.android.sdklib.internal.repository.SdkSource;\r
-import com.android.sdklib.internal.repository.SdkSourceCategory;\r
-import com.android.sdkuilib.repository.ISdkChangeListener;\r
-\r
-import org.eclipse.jface.dialogs.IInputValidator;\r
-import org.eclipse.jface.dialogs.InputDialog;\r
-import org.eclipse.jface.dialogs.MessageDialog;\r
-import org.eclipse.jface.viewers.CheckStateChangedEvent;\r
-import org.eclipse.jface.viewers.CheckboxTreeViewer;\r
-import org.eclipse.jface.viewers.ICheckStateListener;\r
-import org.eclipse.jface.viewers.ISelection;\r
-import org.eclipse.jface.viewers.ITreeContentProvider;\r
-import org.eclipse.jface.viewers.ITreeSelection;\r
-import org.eclipse.jface.window.Window;\r
-import org.eclipse.swt.SWT;\r
-import org.eclipse.swt.events.ControlAdapter;\r
-import org.eclipse.swt.events.ControlEvent;\r
-import org.eclipse.swt.events.SelectionAdapter;\r
-import org.eclipse.swt.events.SelectionEvent;\r
-import org.eclipse.swt.graphics.Rectangle;\r
-import org.eclipse.swt.layout.GridData;\r
-import org.eclipse.swt.layout.GridLayout;\r
-import org.eclipse.swt.widgets.Button;\r
-import org.eclipse.swt.widgets.Composite;\r
-import org.eclipse.swt.widgets.Group;\r
-import org.eclipse.swt.widgets.Label;\r
-import org.eclipse.swt.widgets.Tree;\r
-import org.eclipse.swt.widgets.TreeColumn;\r
-\r
-import java.util.ArrayList;\r
-\r
-\r
-public class RemotePackagesPage extends Composite implements ISdkChangeListener {\r
-\r
-    private final UpdaterData mUpdaterData;\r
-\r
-    private CheckboxTreeViewer mTreeViewerSources;\r
-    private Tree mTreeSources;\r
-    private TreeColumn mColumnSource;\r
-    private Button mUpdateOnlyCheckBox;\r
-    private Group mDescriptionContainer;\r
-    private Button mAddSiteButton;\r
-    private Button mDeleteSiteButton;\r
-    private Button mRefreshButton;\r
-    private Button mInstallSelectedButton;\r
-    private Label mDescriptionLabel;\r
-    private Label mSdkLocLabel;\r
-\r
-\r
-\r
-    /**\r
-     * Create the composite.\r
-     * @param parent The parent of the composite.\r
-     * @param updaterData An instance of {@link UpdaterData}.\r
-     */\r
-    RemotePackagesPage(Composite parent, UpdaterData updaterData) {\r
-        super(parent, SWT.BORDER);\r
-\r
-        mUpdaterData = updaterData;\r
-\r
-        createContents(this);\r
-        postCreate();  //$hide$\r
-    }\r
-\r
-    private void createContents(Composite parent) {\r
-        parent.setLayout(new GridLayout(5, false));\r
-\r
-        mSdkLocLabel = new Label(parent, SWT.NONE);\r
-        mSdkLocLabel.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false, 5, 1));\r
-        mSdkLocLabel.setText("SDK Location: " +\r
-                (mUpdaterData.getOsSdkRoot() != null ? mUpdaterData.getOsSdkRoot()\r
-                                                     : "<unknown>"));\r
-\r
-        mTreeViewerSources = new CheckboxTreeViewer(parent, SWT.BORDER);\r
-        mTreeViewerSources.addCheckStateListener(new ICheckStateListener() {\r
-            public void checkStateChanged(CheckStateChangedEvent event) {\r
-                onTreeCheckStateChanged(event); //$hide$\r
-            }\r
-        });\r
-        mTreeSources = mTreeViewerSources.getTree();\r
-        mTreeSources.addSelectionListener(new SelectionAdapter() {\r
-            @Override\r
-            public void widgetSelected(SelectionEvent e) {\r
-                onTreeSelected(); //$hide$\r
-            }\r
-        });\r
-        mTreeSources.setHeaderVisible(true);\r
-        mTreeSources.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 5, 1));\r
-\r
-        mColumnSource = new TreeColumn(mTreeSources, SWT.NONE);\r
-        mColumnSource.setWidth(289);\r
-        mColumnSource.setText("Packages available for download");\r
-\r
-        mDescriptionContainer = new Group(parent, SWT.NONE);\r
-        mDescriptionContainer.setLayout(new GridLayout(1, false));\r
-        mDescriptionContainer.setText("Description");\r
-        mDescriptionContainer.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, 5, 1));\r
-\r
-        mDescriptionLabel = new Label(mDescriptionContainer, SWT.NONE);\r
-        mDescriptionLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));\r
-        mDescriptionLabel.setText("Line1\nLine2\nLine3");  //$NON-NLS-1$\r
-\r
-        mAddSiteButton = new Button(parent, SWT.NONE);\r
-        mAddSiteButton.setText("Add Add-on Site...");\r
-        mAddSiteButton.setToolTipText("Allows you to enter a new add-on site. " +\r
-                "Such site can only contribute add-ons and extra packages.");\r
-        mAddSiteButton.addSelectionListener(new SelectionAdapter() {\r
-            @Override\r
-            public void widgetSelected(SelectionEvent e) {\r
-                onAddSiteSelected(); //$hide$\r
-            }\r
-        });\r
-\r
-        mDeleteSiteButton = new Button(parent, SWT.NONE);\r
-        mDeleteSiteButton.setText("Delete Add-on Site...");\r
-        mDeleteSiteButton.setToolTipText("Allows you to remove an add-on site. " +\r
-                "Built-in default sites cannot be removed.");\r
-        mDeleteSiteButton.addSelectionListener(new SelectionAdapter() {\r
-            @Override\r
-            public void widgetSelected(SelectionEvent e) {\r
-                onRemoveSiteSelected(); //$hide$\r
-            }\r
-        });\r
-\r
-        mUpdateOnlyCheckBox = new Button(parent, SWT.CHECK);\r
-        mUpdateOnlyCheckBox.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 1, 1));\r
-        mUpdateOnlyCheckBox.setText("Display updates only");\r
-        mUpdateOnlyCheckBox.setToolTipText("When selected, only compatible non-obsolete update packages are shown in the list above.");\r
-        mUpdateOnlyCheckBox.setSelection(mUpdaterData.getSettingsController().getShowUpdateOnly());\r
-        mUpdateOnlyCheckBox.addSelectionListener(new SelectionAdapter() {\r
-            @Override\r
-            public void widgetSelected(SelectionEvent arg0) {\r
-                onShowUpdateOnly(); //$hide$\r
-            }\r
-        });\r
-\r
-        mRefreshButton = new Button(parent, SWT.NONE);\r
-        mRefreshButton.setText("Refresh");\r
-        mRefreshButton.setToolTipText("Refreshes the list of packages from open sites.");\r
-        mRefreshButton.addSelectionListener(new SelectionAdapter() {\r
-            @Override\r
-            public void widgetSelected(SelectionEvent e) {\r
-                onRefreshSelected(); //$hide$\r
-            }\r
-        });\r
-\r
-        mInstallSelectedButton = new Button(parent, SWT.NONE);\r
-        mInstallSelectedButton.setText("Install Selected");\r
-        mInstallSelectedButton.setToolTipText("Allows you to review all selected packages " +\r
-                "and install them.");\r
-        mInstallSelectedButton.addSelectionListener(new SelectionAdapter() {\r
-            @Override\r
-            public void widgetSelected(SelectionEvent e) {\r
-                onInstallSelectedArchives();  //$hide$\r
-            }\r
-        });\r
-    }\r
-\r
-    @Override\r
-    public void dispose() {\r
-        mUpdaterData.removeListener(this);\r
-        super.dispose();\r
-    }\r
-\r
-    @Override\r
-    protected void checkSubclass() {\r
-        // Disable the check that prevents subclassing of SWT components\r
-    }\r
-\r
-    // -- Start of internal part ----------\r
-    // Hide everything down-below from SWT designer\r
-    //$hide>>$\r
-\r
-    /**\r
-     * Called by the constructor right after {@link #createContents(Composite)}.\r
-     */\r
-    private void postCreate() {\r
-        mUpdaterData.addListeners(this);\r
-        adjustColumnsWidth();\r
-        updateButtonsState();\r
-    }\r
-\r
-    /**\r
-     * Adds a listener to adjust the columns width when the parent is resized.\r
-     * <p/>\r
-     * If we need something more fancy, we might want to use this:\r
-     * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co\r
-     */\r
-    private void adjustColumnsWidth() {\r
-        // Add a listener to resize the column to the full width of the table\r
-        ControlAdapter resizer = new ControlAdapter() {\r
-            @Override\r
-            public void controlResized(ControlEvent e) {\r
-                Rectangle r = mTreeSources.getClientArea();\r
-                mColumnSource.setWidth(r.width);\r
-            }\r
-        };\r
-\r
-        mTreeSources.addControlListener(resizer);\r
-        resizer.controlResized(null);\r
-    }\r
-\r
-    /**\r
-     * Called when an item in the package table viewer is selected.\r
-     * If the items is an {@link IDescription} (as it should), this will display its long\r
-     * description in the description area. Otherwise when the item is not of the expected\r
-     * type or there is no selection, it empties the description area.\r
-     */\r
-    private void onTreeSelected() {\r
-        updateButtonsState();\r
-\r
-        ISelection sel = mTreeViewerSources.getSelection();\r
-        if (sel instanceof ITreeSelection) {\r
-            Object elem = ((ITreeSelection) sel).getFirstElement();\r
-            if (elem instanceof IDescription) {\r
-                mDescriptionLabel.setText(((IDescription) elem).getLongDescription());\r
-                mDescriptionContainer.layout(true);\r
-                return;\r
-            }\r
-        }\r
-        mDescriptionLabel.setText("");  //$NON-NLS1-$\r
-    }\r
-\r
-    /**\r
-     * Handle checking and unchecking of the tree items.\r
-     *\r
-     * When unchecking, all sub-tree items checkboxes are cleared too.\r
-     * When checking a source, all of its packages are checked too.\r
-     * When checking a package, only its compatible archives are checked.\r
-     */\r
-    private void onTreeCheckStateChanged(CheckStateChangedEvent event) {\r
-        updateButtonsState();\r
-\r
-        boolean b = event.getChecked();\r
-        Object elem = event.getElement(); // Will be Archive or Package or RepoSource\r
-\r
-        assert event.getSource() == mTreeViewerSources;\r
-\r
-        // when deselecting, we just deselect all children too\r
-        if (b == false) {\r
-            mTreeViewerSources.setSubtreeChecked(elem, b);\r
-            return;\r
-        }\r
-\r
-        ITreeContentProvider provider =\r
-            (ITreeContentProvider) mTreeViewerSources.getContentProvider();\r
-\r
-        // When selecting, we want to only select compatible archives\r
-        // and expand the super nodes.\r
-        expandItem(elem, provider);\r
-    }\r
-\r
-    private void expandItem(Object elem, ITreeContentProvider provider) {\r
-        if (elem instanceof SdkSource || elem instanceof SdkSourceCategory) {\r
-            mTreeViewerSources.setExpandedState(elem, true);\r
-            for (Object pkg : provider.getChildren(elem)) {\r
-                mTreeViewerSources.setChecked(pkg, true);\r
-                expandItem(pkg, provider);\r
-            }\r
-        } else if (elem instanceof Package) {\r
-            selectCompatibleArchives(elem, provider);\r
-        }\r
-    }\r
-\r
-    private void selectCompatibleArchives(Object pkg, ITreeContentProvider provider) {\r
-        for (Object archive : provider.getChildren(pkg)) {\r
-            if (archive instanceof Archive) {\r
-                mTreeViewerSources.setChecked(archive, ((Archive) archive).isCompatible());\r
-            }\r
-        }\r
-    }\r
-\r
-    private void onShowUpdateOnly() {\r
-        SettingsController controller = mUpdaterData.getSettingsController();\r
-        controller.setShowUpdateOnly(mUpdateOnlyCheckBox.getSelection());\r
-        controller.saveSettings();\r
-\r
-        // Get the list of selected archives\r
-        ArrayList<Archive> archives = new ArrayList<Archive>();\r
-        for (Object element : mTreeViewerSources.getCheckedElements()) {\r
-            if (element instanceof Archive) {\r
-                archives.add((Archive) element);\r
-            }\r
-            // Deselect them all\r
-            mTreeViewerSources.setChecked(element, false);\r
-        }\r
-\r
-        mTreeViewerSources.refresh();\r
-\r
-        // Now reselect those that still exist in the tree but only if they\r
-        // are compatible archives\r
-        for (Archive a : archives) {\r
-            if (a.isCompatible() && mTreeViewerSources.setChecked(a, true)) {\r
-                // If we managed to select the archive, also select the parent package.\r
-                // Technically we should only select the parent package if *all* the\r
-                // compatible archives children are selected. In practice we'll rarely\r
-                // have more than one compatible archive per package.\r
-                mTreeViewerSources.setChecked(a.getParentPackage(), true);\r
-            }\r
-        }\r
-\r
-        updateButtonsState();\r
-    }\r
-\r
-    private void onInstallSelectedArchives() {\r
-        ArrayList<Archive> archives = new ArrayList<Archive>();\r
-        for (Object element : mTreeViewerSources.getCheckedElements()) {\r
-            if (element instanceof Archive) {\r
-                archives.add((Archive) element);\r
-            }\r
-        }\r
-\r
-        if (mUpdaterData != null) {\r
-            mUpdaterData.updateOrInstallAll_WithGUI(\r
-                    archives,\r
-                    mUpdateOnlyCheckBox.getSelection() /* includeObsoletes */);\r
-        }\r
-    }\r
-\r
-    private void onAddSiteSelected() {\r
-\r
-        final SdkSource[] knowSources = mUpdaterData.getSources().getAllSources();\r
-        String title = "Add Add-on Site URL";\r
-\r
-        String msg =\r
-        "This dialog lets you add the URL of a new add-on site.\n" +\r
-        "\n" +\r
-        "An add-on site can only provide new add-ons or \"user\" packages.\n" +\r
-        "Add-on sites cannot provide standard Android platforms, docs or samples packages.\n" +\r
-        "Inserting a URL here will not allow you to clone an official Android repository.\n" +\r
-        "\n" +\r
-        "Please enter the URL of the repository.xml for the new add-on site:";\r
-\r
-        InputDialog dlg = new InputDialog(getShell(), title, msg, null, new IInputValidator() {\r
-            public String isValid(String newText) {\r
-\r
-                newText = newText == null ? null : newText.trim();\r
-\r
-                if (newText == null || newText.length() == 0) {\r
-                    return "Error: URL field is empty. Please enter a URL.";\r
-                }\r
-\r
-                // A URL should have one of the following prefixes\r
-                if (!newText.startsWith("file://") &&                 //$NON-NLS-1$\r
-                        !newText.startsWith("ftp://") &&              //$NON-NLS-1$\r
-                        !newText.startsWith("http://") &&             //$NON-NLS-1$\r
-                        !newText.startsWith("https://")) {            //$NON-NLS-1$\r
-                    return "Error: The URL must start by one of file://, ftp://, http:// or https://";\r
-                }\r
-\r
-                // Reject URLs that are already in the source list.\r
-                // URLs are generally case-insensitive (except for file:// where it all depends\r
-                // on the current OS so we'll ignore this case.)\r
-                for (SdkSource s : knowSources) {\r
-                    if (newText.equalsIgnoreCase(s.getUrl())) {\r
-                        return "Error : This site is already listed.";\r
-                    }\r
-                }\r
-\r
-                return null;\r
-            }\r
-        });\r
-\r
-        if (dlg.open() == Window.OK) {\r
-            String url = dlg.getValue().trim();\r
-            mUpdaterData.getSources().add(\r
-                    SdkSourceCategory.USER_ADDONS,\r
-                    new SdkAddonSource(url, null/*uiName*/));\r
-            onRefreshSelected();\r
-        }\r
-    }\r
-\r
-    private void onRemoveSiteSelected() {\r
-        boolean changed = false;\r
-\r
-        ISelection sel = mTreeViewerSources.getSelection();\r
-        if (mUpdaterData != null && sel instanceof ITreeSelection) {\r
-            for (Object c : ((ITreeSelection) sel).toList()) {\r
-                if (c instanceof SdkSource) {\r
-                    SdkSource source = (SdkSource) c;\r
-\r
-                    if (mUpdaterData.getSources().hasSourceUrl(\r
-                            SdkSourceCategory.USER_ADDONS, source)) {\r
-                        String title = "Delete Add-on Site?";\r
-\r
-                        String msg = String.format("Are you sure you want to delete the add-on site '%1$s'?",\r
-                                source.getUrl());\r
-\r
-                        if (MessageDialog.openQuestion(getShell(), title, msg)) {\r
-                            mUpdaterData.getSources().remove(source);\r
-                            changed = true;\r
-                        }\r
-                    }\r
-                }\r
-            }\r
-        }\r
-\r
-        if (changed) {\r
-            onRefreshSelected();\r
-        }\r
-    }\r
-\r
-    private void onRefreshSelected() {\r
-        if (mUpdaterData != null) {\r
-            mUpdaterData.refreshSources(false /*forceFetching*/);\r
-        }\r
-        mTreeViewerSources.refresh();\r
-        updateButtonsState();\r
-    }\r
-\r
-    private void updateButtonsState() {\r
-        // We install archives, so there should be at least one checked archive.\r
-        // Having sites or packages checked does not count.\r
-        boolean hasCheckedArchive = false;\r
-        Object[] checked = mTreeViewerSources.getCheckedElements();\r
-        if (checked != null) {\r
-            for (Object c : checked) {\r
-                if (c instanceof Archive) {\r
-                    hasCheckedArchive = true;\r
-                    break;\r
-                }\r
-            }\r
-        }\r
-\r
-        // Is there a selected site Source?\r
-        boolean hasSelectedUserSource = false;\r
-        ISelection sel = mTreeViewerSources.getSelection();\r
-        if (sel instanceof ITreeSelection) {\r
-            for (Object c : ((ITreeSelection) sel).toList()) {\r
-                if (c instanceof SdkSource &&\r
-                        mUpdaterData.getSources().hasSourceUrl(\r
-                                SdkSourceCategory.USER_ADDONS, (SdkSource) c)) {\r
-                    hasSelectedUserSource = true;\r
-                    break;\r
-                }\r
-            }\r
-        }\r
-\r
-        mAddSiteButton.setEnabled(true);\r
-        mDeleteSiteButton.setEnabled(hasSelectedUserSource);\r
-        mRefreshButton.setEnabled(true);\r
-        mInstallSelectedButton.setEnabled(hasCheckedArchive);\r
-\r
-        // set value on the show only update checkbox\r
-        mUpdateOnlyCheckBox.setSelection(mUpdaterData.getSettingsController().getShowUpdateOnly());\r
-    }\r
-\r
-    // --- Implementation of ISdkChangeListener ---\r
-\r
-    public void onSdkLoaded() {\r
-        onSdkReload();\r
-    }\r
-\r
-    public void onSdkReload() {\r
-        RepoSourcesAdapter sources = mUpdaterData.getSourcesAdapter();\r
-        mTreeViewerSources.setContentProvider(sources.getContentProvider());\r
-        mTreeViewerSources.setLabelProvider(  sources.getLabelProvider());\r
-        mTreeViewerSources.setInput(sources);\r
-        onTreeSelected();\r
-    }\r
-\r
-    public void preInstallHook() {\r
-        // nothing to be done for now.\r
-    }\r
-\r
-    public void postInstallHook() {\r
-        // nothing to be done for now.\r
-    }\r
-\r
-    // End of hiding from SWT Designer\r
-    //$hide<<$\r
-}\r
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.sdkuilib.internal.repository;
+
+
+import com.android.sdklib.internal.repository.Archive;
+import com.android.sdklib.internal.repository.IDescription;
+import com.android.sdklib.internal.repository.Package;
+import com.android.sdklib.internal.repository.SdkAddonSource;
+import com.android.sdklib.internal.repository.SdkSource;
+import com.android.sdklib.internal.repository.SdkSourceCategory;
+import com.android.sdkuilib.repository.ISdkChangeListener;
+
+import org.eclipse.jface.dialogs.IInputValidator;
+import org.eclipse.jface.dialogs.InputDialog;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.CheckStateChangedEvent;
+import org.eclipse.jface.viewers.CheckboxTreeViewer;
+import org.eclipse.jface.viewers.ICheckStateListener;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+
+import java.util.ArrayList;
+
+/**
+ * Page that displays remote repository & add-ons sources and let the user
+ * select packages for installation.
+ */
+public class RemotePackagesPage extends Composite implements ISdkChangeListener {
+
+    private final UpdaterData mUpdaterData;
+
+    private CheckboxTreeViewer mTreeViewerSources;
+    private Tree mTreeSources;
+    private TreeColumn mColumnSource;
+    private Button mUpdateOnlyCheckBox;
+    private Group mDescriptionContainer;
+    private Button mAddSiteButton;
+    private Button mDeleteSiteButton;
+    private Button mRefreshButton;
+    private Button mInstallSelectedButton;
+    private Label mDescriptionLabel;
+    private Label mSdkLocLabel;
+
+
+
+    /**
+     * Create the composite.
+     * @param parent The parent of the composite.
+     * @param updaterData An instance of {@link UpdaterData}.
+     */
+    RemotePackagesPage(Composite parent, UpdaterData updaterData) {
+        super(parent, SWT.BORDER);
+
+        mUpdaterData = updaterData;
+
+        createContents(this);
+        postCreate();  //$hide$
+    }
+
+    private void createContents(Composite parent) {
+        parent.setLayout(new GridLayout(5, false));
+
+        mSdkLocLabel = new Label(parent, SWT.NONE);
+        mSdkLocLabel.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false, 5, 1));
+        mSdkLocLabel.setText("SDK Location: " +
+                (mUpdaterData.getOsSdkRoot() != null ? mUpdaterData.getOsSdkRoot()
+                                                     : "<unknown>"));
+
+        mTreeViewerSources = new CheckboxTreeViewer(parent, SWT.BORDER);
+        mTreeViewerSources.addCheckStateListener(new ICheckStateListener() {
+            public void checkStateChanged(CheckStateChangedEvent event) {
+                onTreeCheckStateChanged(event); //$hide$
+            }
+        });
+        mTreeSources = mTreeViewerSources.getTree();
+        mTreeSources.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onTreeSelected(); //$hide$
+            }
+        });
+        mTreeSources.setHeaderVisible(true);
+        mTreeSources.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 5, 1));
+
+        mColumnSource = new TreeColumn(mTreeSources, SWT.NONE);
+        mColumnSource.setWidth(289);
+        mColumnSource.setText("Packages available for download");
+
+        mDescriptionContainer = new Group(parent, SWT.NONE);
+        mDescriptionContainer.setLayout(new GridLayout(1, false));
+        mDescriptionContainer.setText("Description");
+        mDescriptionContainer.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, 5, 1));
+
+        mDescriptionLabel = new Label(mDescriptionContainer, SWT.NONE);
+        mDescriptionLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
+        mDescriptionLabel.setText("Line1\nLine2\nLine3");  //$NON-NLS-1$
+
+        mAddSiteButton = new Button(parent, SWT.NONE);
+        mAddSiteButton.setText("Add Add-on Site...");
+        mAddSiteButton.setToolTipText("Allows you to enter a new add-on site. " +
+                "Such site can only contribute add-ons and extra packages.");
+        mAddSiteButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onAddSiteSelected(); //$hide$
+            }
+        });
+
+        mDeleteSiteButton = new Button(parent, SWT.NONE);
+        mDeleteSiteButton.setText("Delete Add-on Site...");
+        mDeleteSiteButton.setToolTipText("Allows you to remove an add-on site. " +
+                "Built-in default sites cannot be removed.");
+        mDeleteSiteButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onRemoveSiteSelected(); //$hide$
+            }
+        });
+
+        mUpdateOnlyCheckBox = new Button(parent, SWT.CHECK);
+        mUpdateOnlyCheckBox.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 1, 1));
+        mUpdateOnlyCheckBox.setText("Display updates only");
+        mUpdateOnlyCheckBox.setToolTipText("When selected, only compatible non-obsolete update packages are shown in the list above.");
+        mUpdateOnlyCheckBox.setSelection(mUpdaterData.getSettingsController().getShowUpdateOnly());
+        mUpdateOnlyCheckBox.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent arg0) {
+                onShowUpdateOnly(); //$hide$
+            }
+        });
+
+        mRefreshButton = new Button(parent, SWT.NONE);
+        mRefreshButton.setText("Refresh");
+        mRefreshButton.setToolTipText("Refreshes the list of packages from open sites.");
+        mRefreshButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onRefreshSelected(); //$hide$
+            }
+        });
+
+        mInstallSelectedButton = new Button(parent, SWT.NONE);
+        mInstallSelectedButton.setText("Install Selected");
+        mInstallSelectedButton.setToolTipText("Allows you to review all selected packages " +
+                "and install them.");
+        mInstallSelectedButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                onInstallSelectedArchives();  //$hide$
+            }
+        });
+    }
+
+    @Override
+    public void dispose() {
+        mUpdaterData.removeListener(this);
+        super.dispose();
+    }
+
+    @Override
+    protected void checkSubclass() {
+        // Disable the check that prevents subclassing of SWT components
+    }
+
+    // -- Start of internal part ----------
+    // Hide everything down-below from SWT designer
+    //$hide>>$
+
+    /**
+     * Called by the constructor right after {@link #createContents(Composite)}.
+     */
+    private void postCreate() {
+        mUpdaterData.addListeners(this);
+        adjustColumnsWidth();
+        updateButtonsState();
+    }
+
+    /**
+     * Adds a listener to adjust the columns width when the parent is resized.
+     * <p/>
+     * If we need something more fancy, we might want to use this:
+     * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co
+     */
+    private void adjustColumnsWidth() {
+        // Add a listener to resize the column to the full width of the table
+        ControlAdapter resizer = new ControlAdapter() {
+            @Override
+            public void controlResized(ControlEvent e) {
+                Rectangle r = mTreeSources.getClientArea();
+                mColumnSource.setWidth(r.width);
+            }
+        };
+
+        mTreeSources.addControlListener(resizer);
+        resizer.controlResized(null);
+    }
+
+    /**
+     * Called when an item in the package table viewer is selected.
+     * If the items is an {@link IDescription} (as it should), this will display its long
+     * description in the description area. Otherwise when the item is not of the expected
+     * type or there is no selection, it empties the description area.
+     */
+    private void onTreeSelected() {
+        updateButtonsState();
+
+        ISelection sel = mTreeViewerSources.getSelection();
+        if (sel instanceof ITreeSelection) {
+            Object elem = ((ITreeSelection) sel).getFirstElement();
+            if (elem instanceof IDescription) {
+                mDescriptionLabel.setText(((IDescription) elem).getLongDescription());
+                mDescriptionContainer.layout(true);
+                return;
+            }
+        }
+        mDescriptionLabel.setText("");  //$NON-NLS1-$
+    }
+
+    /**
+     * Handle checking and unchecking of the tree items.
+     *
+     * When unchecking, all sub-tree items checkboxes are cleared too.
+     * When checking a source, all of its packages are checked too.
+     * When checking a package, only its compatible archives are checked.
+     */
+    private void onTreeCheckStateChanged(CheckStateChangedEvent event) {
+        updateButtonsState();
+
+        boolean b = event.getChecked();
+        Object elem = event.getElement(); // Will be Archive or Package or RepoSource
+
+        assert event.getSource() == mTreeViewerSources;
+
+        // when deselecting, we just deselect all children too
+        if (b == false) {
+            mTreeViewerSources.setSubtreeChecked(elem, b);
+            return;
+        }
+
+        ITreeContentProvider provider =
+            (ITreeContentProvider) mTreeViewerSources.getContentProvider();
+
+        // When selecting, we want to only select compatible archives
+        // and expand the super nodes.
+        expandItem(elem, provider);
+    }
+
+    private void expandItem(Object elem, ITreeContentProvider provider) {
+        if (elem instanceof SdkSource || elem instanceof SdkSourceCategory) {
+            mTreeViewerSources.setExpandedState(elem, true);
+            for (Object pkg : provider.getChildren(elem)) {
+                mTreeViewerSources.setChecked(pkg, true);
+                expandItem(pkg, provider);
+            }
+        } else if (elem instanceof Package) {
+            selectCompatibleArchives(elem, provider);
+        }
+    }
+
+    private void selectCompatibleArchives(Object pkg, ITreeContentProvider provider) {
+        for (Object archive : provider.getChildren(pkg)) {
+            if (archive instanceof Archive) {
+                mTreeViewerSources.setChecked(archive, ((Archive) archive).isCompatible());
+            }
+        }
+    }
+
+    private void onShowUpdateOnly() {
+        SettingsController controller = mUpdaterData.getSettingsController();
+        controller.setShowUpdateOnly(mUpdateOnlyCheckBox.getSelection());
+        controller.saveSettings();
+
+        // Get the list of selected archives
+        ArrayList<Archive> archives = new ArrayList<Archive>();
+        for (Object element : mTreeViewerSources.getCheckedElements()) {
+            if (element instanceof Archive) {
+                archives.add((Archive) element);
+            }
+            // Deselect them all
+            mTreeViewerSources.setChecked(element, false);
+        }
+
+        mTreeViewerSources.refresh();
+
+        // Now reselect those that still exist in the tree but only if they
+        // are compatible archives
+        for (Archive a : archives) {
+            if (a.isCompatible() && mTreeViewerSources.setChecked(a, true)) {
+                // If we managed to select the archive, also select the parent package.
+                // Technically we should only select the parent package if *all* the
+                // compatible archives children are selected. In practice we'll rarely
+                // have more than one compatible archive per package.
+                mTreeViewerSources.setChecked(a.getParentPackage(), true);
+            }
+        }
+
+        updateButtonsState();
+    }
+
+    private void onInstallSelectedArchives() {
+        ArrayList<Archive> archives = new ArrayList<Archive>();
+        for (Object element : mTreeViewerSources.getCheckedElements()) {
+            if (element instanceof Archive) {
+                archives.add((Archive) element);
+            }
+        }
+
+        if (mUpdaterData != null) {
+            mUpdaterData.updateOrInstallAll_WithGUI(
+                    archives,
+                    mUpdateOnlyCheckBox.getSelection() /* includeObsoletes */);
+        }
+    }
+
+    private void onAddSiteSelected() {
+        final SdkSource[] knowSources = mUpdaterData.getSources().getAllSources();
+        String title = "Add Add-on Site URL";
+
+        String msg =
+        "This dialog lets you add the URL of a new add-on site.\n" +
+        "\n" +
+        "An add-on site can only provide new add-ons or \"user\" packages.\n" +
+        "Add-on sites cannot provide standard Android platforms, docs or samples packages.\n" +
+        "Inserting a URL here will not allow you to clone an official Android repository.\n" +
+        "\n" +
+        "Please enter the URL of the repository.xml for the new add-on site:";
+
+        InputDialog dlg = new InputDialog(getShell(), title, msg, null, new IInputValidator() {
+            public String isValid(String newText) {
+
+                newText = newText == null ? null : newText.trim();
+
+                if (newText == null || newText.length() == 0) {
+                    return "Error: URL field is empty. Please enter a URL.";
+                }
+
+                // A URL should have one of the following prefixes
+                if (!newText.startsWith("file://") &&                 //$NON-NLS-1$
+                        !newText.startsWith("ftp://") &&              //$NON-NLS-1$
+                        !newText.startsWith("http://") &&             //$NON-NLS-1$
+                        !newText.startsWith("https://")) {            //$NON-NLS-1$
+                    return "Error: The URL must start by one of file://, ftp://, http:// or https://";
+                }
+
+                // Reject URLs that are already in the source list.
+                // URLs are generally case-insensitive (except for file:// where it all depends
+                // on the current OS so we'll ignore this case.)
+                for (SdkSource s : knowSources) {
+                    if (newText.equalsIgnoreCase(s.getUrl())) {
+                        return "Error : This site is already listed.";
+                    }
+                }
+
+                return null;
+            }
+        });
+
+        if (dlg.open() == Window.OK) {
+            String url = dlg.getValue().trim();
+            mUpdaterData.getSources().add(
+                    SdkSourceCategory.USER_ADDONS,
+                    new SdkAddonSource(url, null/*uiName*/));
+            onRefreshSelected();
+        }
+    }
+
+    private void onRemoveSiteSelected() {
+        boolean changed = false;
+
+        ISelection sel = mTreeViewerSources.getSelection();
+        if (mUpdaterData != null && sel instanceof ITreeSelection) {
+            for (Object c : ((ITreeSelection) sel).toList()) {
+                if (c instanceof SdkSource) {
+                    SdkSource source = (SdkSource) c;
+
+                    if (mUpdaterData.getSources().hasSourceUrl(
+                            SdkSourceCategory.USER_ADDONS, source)) {
+                        String title = "Delete Add-on Site?";
+
+                        String msg = String.format("Are you sure you want to delete the add-on site '%1$s'?",
+                                source.getUrl());
+
+                        if (MessageDialog.openQuestion(getShell(), title, msg)) {
+                            mUpdaterData.getSources().remove(source);
+                            changed = true;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (changed) {
+            onRefreshSelected();
+        }
+    }
+
+    private void onRefreshSelected() {
+        if (mUpdaterData != null) {
+            mUpdaterData.refreshSources(false /*forceFetching*/);
+        }
+        mTreeViewerSources.refresh();
+        updateButtonsState();
+    }
+
+    private void updateButtonsState() {
+        // We install archives, so there should be at least one checked archive.
+        // Having sites or packages checked does not count.
+        boolean hasCheckedArchive = false;
+        Object[] checked = mTreeViewerSources.getCheckedElements();
+        if (checked != null) {
+            for (Object c : checked) {
+                if (c instanceof Archive) {
+                    hasCheckedArchive = true;
+                    break;
+                }
+            }
+        }
+
+        // Is there a selected site Source?
+        boolean hasSelectedUserSource = false;
+        ISelection sel = mTreeViewerSources.getSelection();
+        if (sel instanceof ITreeSelection) {
+            for (Object c : ((ITreeSelection) sel).toList()) {
+                if (c instanceof SdkSource &&
+                        mUpdaterData.getSources().hasSourceUrl(
+                                SdkSourceCategory.USER_ADDONS, (SdkSource) c)) {
+                    hasSelectedUserSource = true;
+                    break;
+                }
+            }
+        }
+
+        mAddSiteButton.setEnabled(true);
+        mDeleteSiteButton.setEnabled(hasSelectedUserSource);
+        mRefreshButton.setEnabled(true);
+        mInstallSelectedButton.setEnabled(hasCheckedArchive);
+
+        // set value on the show only update checkbox
+        mUpdateOnlyCheckBox.setSelection(mUpdaterData.getSettingsController().getShowUpdateOnly());
+    }
+
+    // --- Implementation of ISdkChangeListener ---
+
+    public void onSdkLoaded() {
+        onSdkReload();
+    }
+
+    public void onSdkReload() {
+        RepoSourcesAdapter sources = mUpdaterData.getSourcesAdapter();
+        mTreeViewerSources.setContentProvider(sources.getContentProvider());
+        mTreeViewerSources.setLabelProvider(  sources.getLabelProvider());
+        mTreeViewerSources.setInput(sources);
+        onTreeSelected();
+    }
+
+    public void preInstallHook() {
+        // nothing to be done for now.
+    }
+
+    public void postInstallHook() {
+        // nothing to be done for now.
+    }
+
+    // End of hiding from SWT Designer
+    //$hide<<$
+}
index 19d3916..9ebef70 100755 (executable)
@@ -69,10 +69,11 @@ public class UpdaterWindowImpl {
     private SashForm mSashForm;\r
     private List mPageList;\r
     private Composite mPagesRootComposite;\r
-    private LocalPackagesPage mLocalPackagePage;\r
-    private RemotePackagesPage mRemotePackagesPage;\r
     private AvdManagerPage mAvdManagerPage;\r
     private StackLayout mStackLayout;\r
+    private PackagesPage mPackagesPage;\r
+    private LocalPackagesPage mLocalPackagePage;\r
+    private RemotePackagesPage mRemotePackagesPage;\r
 \r
     /**\r
      * Creates a new window. Caller must call open(), which will block.\r
@@ -122,6 +123,12 @@ public class UpdaterWindowImpl {
             }\r
         });\r
 \r
+        mUpdaterData.setWindowShell(mAndroidSdkUpdater);\r
+        mTaskFactory = new ProgressTaskFactory(mAndroidSdkUpdater);\r
+        mUpdaterData.setTaskFactory(mTaskFactory);\r
+        mUpdaterData.setImageFactory(new ImageFactory(mAndroidSdkUpdater.getDisplay()));\r
+\r
+\r
         FillLayout fl;\r
         mAndroidSdkUpdater.setLayout(fl = new FillLayout(SWT.HORIZONTAL));\r
         fl.marginHeight = fl.marginWidth = 5;\r
@@ -219,15 +226,14 @@ public class UpdaterWindowImpl {
      */\r
     private void createPages() {\r
         mAvdManagerPage = new AvdManagerPage(mPagesRootComposite, mUpdaterData);\r
-        mLocalPackagePage = new LocalPackagesPage(mPagesRootComposite, mUpdaterData);\r
-        mRemotePackagesPage = new RemotePackagesPage(mPagesRootComposite, mUpdaterData);\r
-    }\r
 \r
-    /**\r
-     * Helper to return the SWT shell.\r
-     */\r
-    private Shell getShell() {\r
-        return mAndroidSdkUpdater;\r
+        // TODO right now the new PackagesPage is experimental and not enabled by default\r
+        if (System.getenv("EXPERIMENTAL") != null) {  //$NON-NLS-1$\r
+            mPackagesPage = new PackagesPage(mPagesRootComposite, mUpdaterData);\r
+        } else {\r
+            mLocalPackagePage = new LocalPackagesPage(mPagesRootComposite, mUpdaterData);\r
+            mRemotePackagesPage = new RemotePackagesPage(mPagesRootComposite, mUpdaterData);\r
+        }\r
     }\r
 \r
     /**\r
@@ -266,16 +272,17 @@ public class UpdaterWindowImpl {
      * Returns true if we should show the window.\r
      */\r
     private boolean postCreate() {\r
-        mUpdaterData.setWindowShell(getShell());\r
-        mTaskFactory = new ProgressTaskFactory(getShell());\r
-        mUpdaterData.setTaskFactory(mTaskFactory);\r
-        mUpdaterData.setImageFactory(new ImageFactory(getShell().getDisplay()));\r
-\r
         setWindowImage(mAndroidSdkUpdater);\r
 \r
         addPage(mAvdManagerPage,     "Virtual devices");\r
-        addPage(mLocalPackagePage,   "Installed packages");\r
-        addPage(mRemotePackagesPage, "Available packages");\r
+\r
+        if (mPackagesPage != null) {  // TODO remove test when experimental is finalized\r
+            addPage(mPackagesPage,      "Packages List");\r
+        } else {\r
+            addPage(mLocalPackagePage,   "Installed packages");\r
+            addPage(mRemotePackagesPage, "Available packages");\r
+        }\r
+\r
         addExtraPages();\r
 \r
         int pageIndex = 0;\r
@@ -287,11 +294,11 @@ public class UpdaterWindowImpl {
             }\r
             i++;\r
         }\r
-        displayPage(pageIndex);\r
-        mPageList.setSelection(pageIndex);\r
 \r
         setupSources();\r
         initializeSettings();\r
+        displayPage(pageIndex);\r
+        mPageList.setSelection(pageIndex);\r
 \r
         if (mUpdaterData.checkIfInitFailed()) {\r
             return false;\r
@@ -395,6 +402,10 @@ public class UpdaterWindowImpl {
                 mPageList.setSelection(index);\r
                 mInternalPageChange = false;\r
             }\r
+\r
+            if (page instanceof IPageListener) {\r
+                ((IPageListener) page).onPageSelected();\r
+            }\r
         }\r
     }\r
 \r
@@ -403,7 +414,6 @@ public class UpdaterWindowImpl {
      */\r
     private void setupSources() {\r
         mUpdaterData.setupDefaultSources();\r
-        mRemotePackagesPage.onSdkReload();\r
     }\r
 \r
     /**\r
index 4a38f75..405a9b2 100755 (executable)
@@ -122,6 +122,10 @@ public class ImageFactory {
             }\r
         }\r
 \r
+        if (object instanceof String) {\r
+            return getImageByName((String) object);\r
+        }\r
+\r
         return null;\r
     }\r
 \r
index abc63d0..4f9eec8 100755 (executable)
@@ -241,6 +241,11 @@ public class UpdaterDataTest extends TestCase {
         }
 
         @Override
+        public String getListDescription() {
+            return this.getClass().getSimpleName();
+        }
+
+        @Override
         public String getShortDescription() {
             return this.getClass().getSimpleName();
         }