diff options
author | Raphael Moll <ralf@android.com> | 2012-09-05 14:32:28 -0700 |
---|---|---|
committer | Raphael Moll <ralf@android.com> | 2012-09-15 20:22:49 -0700 |
commit | 8daea84228bdda8d714f2ab4dfc19a3c2f10271b (patch) | |
tree | f52bc01509b5d7997fb4bfd2fa4b8f330acb286c | |
parent | bd2f7c995a5eb3f3330f95af654d63c6df2ed118 (diff) | |
download | sdk-8daea84228bdda8d714f2ab4dfc19a3c2f10271b.zip sdk-8daea84228bdda8d714f2ab4dfc19a3c2f10271b.tar.gz sdk-8daea84228bdda8d714f2ab4dfc19a3c2f10271b.tar.bz2 |
Refactor PackagesPage to make it testable.
A simple unit test to display that an update is available.
This will get more complex later. The cache is mocked and
the whole test should be independant of the user's actual
settings and local cache, with no network access.
Change-Id: I58ff45895916a14a10f501a9bd664782d777ed42
18 files changed, 1510 insertions, 505 deletions
diff --git a/eclipse/scripts/create_all_symlinks.sh b/eclipse/scripts/create_all_symlinks.sh index 89abd53..8bda0ff 100755 --- a/eclipse/scripts/create_all_symlinks.sh +++ b/eclipse/scripts/create_all_symlinks.sh @@ -105,6 +105,7 @@ set -e # fail early LIBS="" CP_FILES="" + ### BASE ### BASE_PLUGIN_DEST="sdk/eclipse/plugins/com.android.ide.eclipse.base/libs" @@ -122,6 +123,7 @@ BASE_PLUGIN_PREBUILTS="\ LIBS="$LIBS $BASE_PLUGIN_LIBS" CP_FILES="$CP_FILES @:$BASE_PLUGIN_DEST $BASE_PLUGIN_LIBS $BASE_PLUGIN_PREBUILTS" + ### ADT ### ADT_DEST="sdk/eclipse/plugins/com.android.ide.eclipse.adt/libs" @@ -136,6 +138,7 @@ ADT_PREBUILTS="\ LIBS="$LIBS $ADT_LIBS" CP_FILES="$CP_FILES @:$ADT_DEST $ADT_LIBS $ADT_PREBUILTS" + ### DDMS ### DDMS_DEST="sdk/eclipse/plugins/com.android.ide.eclipse.ddms/libs" @@ -170,7 +173,6 @@ if [[ $PLATFORM != "windows-x86" ]]; then fi - ### HIERARCHYVIEWER ### HV_DEST="sdk/eclipse/plugins/com.android.ide.eclipse.hierarchyviewer/libs" @@ -188,6 +190,7 @@ TV_LIBS="traceview" LIBS="$LIBS $TV_LIBS" CP_FILES="$CP_FILES @:$TV_DEST $TV_LIBS" + ### MONITOR ### MONITOR_DEST="sdk/eclipse/plugins/com.android.ide.eclipse.monitor/libs" @@ -196,12 +199,14 @@ MONITOR_LIBS="sdkuilib" LIBS="$LIBS $MONITOR_LIBS" CP_FILES="$CP_FILES @:$MONITOR_DEST $MONITOR_LIBS" + ### SDKMANAGER ### -SDMAN_LIBS="swtmenubar" +SDKMAN_LIBS="swtmenubar" LIBS="$LIBS $SDKMAN_LIBS" + ### GL DEBUGGER ### if [[ $PLATFORM != "windows-x86" ]]; then diff --git a/sdkmanager/libs/sdklib/Android.mk b/sdkmanager/libs/sdklib/Android.mk index 7ff9135..30c4e04 100644 --- a/sdkmanager/libs/sdklib/Android.mk +++ b/sdkmanager/libs/sdklib/Android.mk @@ -26,16 +26,17 @@ LOCAL_JAR_MANIFEST := manifest.txt # sdkmanager/sdklib/manifest.txt # sdkmanager/app/etc/android.bat LOCAL_JAVA_LIBRARIES := \ - layoutlib_api \ common \ - guava-tools \ + commons-codec-1.4 \ commons-compress-1.0 \ + commons-logging-1.1.1 \ + dvlib \ + guava-tools \ httpclient-4.1.1 \ httpcore-4.1 \ httpmime-4.1.1 \ - commons-logging-1.1.1 \ - commons-codec-1.4 \ - dvlib + mkidentity-prebuilt \ + layoutlib_api LOCAL_MODULE := sdklib diff --git a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/sources/SdkSources.java b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/sources/SdkSources.java index 1902101..c89df5e 100755 --- a/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/sources/SdkSources.java +++ b/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/sources/SdkSources.java @@ -420,7 +420,8 @@ public class SdkSources { try { runnable.run(); } catch (Throwable ignore) { - assert ignore == null : "A SdkSource.ChangeListener failed with an exception."; + assert ignore == null : + "A SdkSource.ChangeListener failed with an exception: " + ignore.toString(); } } } diff --git a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/SdkManagerTestCase.java b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/SdkManagerTestCase.java index 46d1db4..86a555a 100755 --- a/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/SdkManagerTestCase.java +++ b/sdkmanager/libs/sdklib/tests/src/com/android/sdklib/SdkManagerTestCase.java @@ -143,6 +143,14 @@ public class SdkManagerTestCase extends TestCase { AndroidLocation.resetFolder(); File addonsDir = new File(sdkDir, SdkConstants.FD_ADDONS); addonsDir.mkdir(); + + File toolsDir = new File(sdkDir, SdkConstants.OS_SDK_TOOLS_FOLDER); + toolsDir.mkdir(); + new File(toolsDir, SdkConstants.androidCmdName()).createNewFile(); + new File(toolsDir, SdkConstants.FN_EMULATOR).createNewFile(); + + // TODO makePlatformTools with at least a source props + File toolsLibEmuDir = new File(sdkDir, SdkConstants.OS_SDK_TOOLS_LIB_FOLDER + "emulator"); toolsLibEmuDir.mkdirs(); new File(toolsLibEmuDir, "snapshots.img").createNewFile(); diff --git a/sdkmanager/libs/sdkuilib/.classpath b/sdkmanager/libs/sdkuilib/.classpath index 4008a80..90f452f 100644 --- a/sdkmanager/libs/sdkuilib/.classpath +++ b/sdkmanager/libs/sdkuilib/.classpath @@ -9,6 +9,11 @@ <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.core.commands_3.6.0.I20100512-1500.jar"/> <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.equinox.common_3.6.0.v20100503.jar"/> <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/eclipse/org.eclipse.jface_3.6.2.M20110210-1200.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/commons-codec-1.4.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/commons-logging-1.1.1.jar"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/httpclient-4.1.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/http-client/src/httpcomponents-client-4.1.1-src.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/httpcore-4.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/http-client/src/httpcomponents-core-4.1-src.zip"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilts/tools/common/http-client/httpmime-4.1.1.jar" sourcepath="/ANDROID_SRC/prebuilts/tools/common/http-client/src/httpcomponents-client-4.1.1-src.zip"/> <classpathentry kind="var" path="ANDROID_OUT_FRAMEWORK/swt.jar"/> <classpathentry combineaccessrules="false" kind="src" path="/layoutlib_api"/> <classpathentry combineaccessrules="false" kind="src" path="/common"/> diff --git a/sdkmanager/libs/sdkuilib/Android.mk b/sdkmanager/libs/sdkuilib/Android.mk index f715d57..9406fdf 100644 --- a/sdkmanager/libs/sdkuilib/Android.mk +++ b/sdkmanager/libs/sdkuilib/Android.mk @@ -25,13 +25,19 @@ LOCAL_JAVA_RESOURCE_DIRS := src # (Note: there is no manifest.txt for sdkuilib.) LOCAL_JAVA_LIBRARIES := \ common \ + commons-codec-1.4 \ + commons-compress-1.0 \ + commons-logging-1.1.1 \ + httpclient-4.1.1 \ + httpcore-4.1 \ + httpmime-4.1.1 \ + org.eclipse.jface_3.6.2.M20110210-1200 \ + org.eclipse.equinox.common_3.6.0.v20100503 \ + org.eclipse.core.commands_3.6.0.I20100512-1500 \ sdklib \ layoutlib_api \ - swtmenubar \ swt \ - org.eclipse.jface_3.6.2.M20110210-1200 \ - org.eclipse.equinox.common_3.6.0.v20100503 \ - org.eclipse.core.commands_3.6.0.I20100512-1500 + swtmenubar LOCAL_MODULE := sdkuilib diff --git a/sdkmanager/libs/sdkuilib/etc/manifest.txt b/sdkmanager/libs/sdkuilib/etc/manifest.txt index 1c93a5d..37def3a 100644 --- a/sdkmanager/libs/sdkuilib/etc/manifest.txt +++ b/sdkmanager/libs/sdkuilib/etc/manifest.txt @@ -1 +1 @@ -Class-Path: sdklib.jar layoutlib_api.jar common.jar swtmenubar.jar swt.jar org.eclipse.jface_3.6.2.M20110210-1200.jar org.eclipse.equinox.common_3.6.0.v20100503.jar org.eclipse.core.commands_3.6.0.I20100512-1500.jar +Class-Path: sdklib.jar layoutlib_api.jar common.jar commons-compress-1.0.jar httpclient-4.1.1.jar httpcore-4.1.jar httpmime-4.1.1.jar commons-logging-1.1.1.jar commons-codec-1.4.jar swtmenubar.jar swt.jar org.eclipse.jface_3.6.2.M20110210-1200.jar org.eclipse.equinox.common_3.6.0.v20100503.jar org.eclipse.core.commands_3.6.0.I20100512-1500.jar diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java index 87cf5f1..7fc1b1e 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/UpdaterData.java @@ -78,7 +78,11 @@ public class UpdaterData implements IUpdaterData { private String mOsSdkRoot; private final LocalSdkParser mLocalSdkParser = new LocalSdkParser(); + /** Holds all sources. Do not use this directly. + * Instead use {@link #getSources()} so that unit tests can override this as needed. */ private final SdkSources mSources = new SdkSources(); + /** Holds settings. Do not use this directly. + * Instead use {@link #getSettingsController()} so that unit tests can override this. */ private final SettingsController mSettingsController; private final ArrayList<ISdkChangeListener> mListeners = new ArrayList<ISdkChangeListener>(); private final ILogger mSdkLog; @@ -129,7 +133,7 @@ public class UpdaterData implements IUpdaterData { public DownloadCache getDownloadCache() { if (mDownloadCache == null) { mDownloadCache = new DownloadCache( - mSettingsController.getSettings().getUseDownloadCache() ? + getSettingsController().getSettings().getUseDownloadCache() ? DownloadCache.Strategy.FRESH_CACHE : DownloadCache.Strategy.DIRECT); } @@ -1044,7 +1048,7 @@ public class UpdaterData implements IUpdaterData { getPackageLoader().loadRemoteAddonsList(monitor); - SdkSource[] sources = mSources.getAllSources(); + SdkSource[] sources = getSources().getAllSources(); monitor.setDescription("Refresh Sources"); monitor.setProgressMax(monitor.getProgress() + sources.length); for (SdkSource source : sources) { diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java index 5353b74..f5a2ed3 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java @@ -32,7 +32,7 @@ import com.android.sdklib.internal.repository.sources.SdkSource; import com.android.sdklib.util.SparseArray; import com.android.sdkuilib.internal.repository.UpdaterData; import com.android.sdkuilib.internal.repository.core.PkgItem.PkgState; -import com.android.sdkuilib.internal.repository.ui.PackagesPage; +import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons; import java.util.ArrayList; import java.util.Collections; @@ -688,9 +688,9 @@ public class PackagesDiffLogic { // Always add the tools & extras categories, even if empty (unlikely anyway) if (needTools) { PkgCategoryApi acat = new PkgCategoryApi( - PkgCategoryApi.KEY_TOOLS, - null, - mUpdaterData.getImageFactory().getImageByName(PackagesPage.ICON_CAT_OTHER)); + PkgCategoryApi.KEY_TOOLS, + null, + mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_OTHER)); synchronized (cats) { cats.add(acat); } @@ -698,9 +698,9 @@ public class PackagesDiffLogic { if (needExtras) { PkgCategoryApi acat = new PkgCategoryApi( - PkgCategoryApi.KEY_EXTRA, - null, - mUpdaterData.getImageFactory().getImageByName(PackagesPage.ICON_CAT_OTHER)); + PkgCategoryApi.KEY_EXTRA, + null, + mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_OTHER)); synchronized (cats) { cats.add(acat); } @@ -733,9 +733,9 @@ public class PackagesDiffLogic { } cat = new PkgCategoryApi( - key, - platformName, - mUpdaterData.getImageFactory().getImageByName(PackagesPage.ICON_CAT_PLATFORM)); + key, + platformName, + mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_PLATFORM)); return cat; } diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java index cdf6b59..b73288b 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java @@ -19,7 +19,7 @@ package com.android.sdkuilib.internal.repository.core; import com.android.sdklib.internal.repository.sources.SdkRepoSource; import com.android.sdklib.internal.repository.sources.SdkSource; import com.android.sdkuilib.internal.repository.UpdaterData; -import com.android.sdkuilib.internal.repository.ui.PackagesPage; +import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons; public class PkgCategorySource extends PkgCategory { @@ -42,8 +42,8 @@ public class PkgCategorySource extends PkgCategory { source, // the source is the key and it can be null source == UNKNOWN_SOURCE ? "Local Packages" : source.toString(), source == UNKNOWN_SOURCE ? - updaterData.getImageFactory().getImageByName(PackagesPage.ICON_PKG_INSTALLED) : - source); + updaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_PKG_INSTALLED) : + source); mSource = source; } diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java index 6f7589c..8adf428 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java @@ -22,6 +22,7 @@ import com.android.sdklib.internal.repository.packages.Package; import com.android.sdklib.internal.repository.sources.SdkSource; import com.android.sdkuilib.internal.repository.ui.PackagesPage; +import org.eclipse.jface.viewers.IInputProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.Viewer; @@ -33,10 +34,10 @@ import java.util.List; */ public class PkgContentProvider implements ITreeContentProvider { - private final Viewer mViewer; + private final IInputProvider mViewer; private boolean mDisplayArchives; - public PkgContentProvider(Viewer viewer) { + public PkgContentProvider(IInputProvider viewer) { mViewer = viewer; } @@ -139,7 +140,7 @@ public class PkgContentProvider implements ITreeContentProvider { } @Override - public void inputChanged(Viewer arg0, Object arg1, Object arg2) { + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // unused } diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ui/PackagesPage.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ui/PackagesPage.java index 566981d..025be46 100755 --- a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ui/PackagesPage.java +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ui/PackagesPage.java @@ -16,20 +16,12 @@ package com.android.sdkuilib.internal.repository.ui; -import com.android.SdkConstants; -import com.android.sdklib.internal.repository.DownloadCache; -import com.android.sdklib.internal.repository.DownloadCache.Strategy; -import com.android.sdklib.internal.repository.IDescription; import com.android.sdklib.internal.repository.ITask; import com.android.sdklib.internal.repository.ITaskMonitor; import com.android.sdklib.internal.repository.archives.Archive; import com.android.sdklib.internal.repository.archives.ArchiveInstaller; import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.sources.SdkSource; import com.android.sdkuilib.internal.repository.UpdaterData; -import com.android.sdkuilib.internal.repository.core.PackageLoader; -import com.android.sdkuilib.internal.repository.core.PackageLoader.ISourceLoadedCallback; -import com.android.sdkuilib.internal.repository.core.PackagesDiffLogic; import com.android.sdkuilib.internal.repository.core.PkgCategory; import com.android.sdkuilib.internal.repository.core.PkgCategoryApi; import com.android.sdkuilib.internal.repository.core.PkgContentProvider; @@ -50,7 +42,6 @@ import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.ICheckStateListener; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.ISelection; -import org.eclipse.jface.viewers.ITableFontProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.ITreeSelection; import org.eclipse.jface.viewers.TreeViewerColumn; @@ -65,7 +56,6 @@ import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; @@ -79,8 +69,6 @@ import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -92,16 +80,7 @@ import java.util.Map.Entry; * 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 { - - public static final String ICON_CAT_OTHER = "pkgcat_other_16.png"; //$NON-NLS-1$ - public static final String ICON_CAT_PLATFORM = "pkgcat_16.png"; //$NON-NLS-1$ - private static final String ICON_SORT_BY_SOURCE = "source_icon16.png"; //$NON-NLS-1$ - private static final String ICON_SORT_BY_API = "platform_pkg_16.png"; //$NON-NLS-1$ - private static final String ICON_PKG_NEW = "pkg_new_16.png"; //$NON-NLS-1$ - private static final String ICON_PKG_INCOMPAT = "pkg_incompat_16.png"; //$NON-NLS-1$ - private static final String ICON_PKG_UPDATE = "pkg_update_16.png"; //$NON-NLS-1$ - public static final String ICON_PKG_INSTALLED = "pkg_installed_16.png"; //$NON-NLS-1$ +public final class PackagesPage extends Composite implements ISdkChangeListener { enum MenuAction { RELOAD (SWT.NONE, "Reload"), @@ -133,13 +112,13 @@ public class PackagesPage extends Composite implements ISdkChangeListener { private final Map<MenuAction, MenuItem> mMenuActions = new HashMap<MenuAction, MenuItem>(); + private final PackagesPageImpl mImpl; private final SdkInvocationContext mContext; - private final UpdaterData mUpdaterData; - private final PackagesDiffLogic mDiffLogic; private boolean mDisplayArchives = false; private boolean mOperationPending; + private Composite mGroupPackages; private Text mTextSdkOsPath; private Button mCheckSortSource; private Button mCheckSortApi; @@ -148,17 +127,11 @@ public class PackagesPage extends Composite implements ISdkChangeListener { private Button mCheckFilterNew; private Composite mGroupOptions; private Composite mGroupSdk; - private Group mGroupPackages; 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 Font mTreeFontItalic; private TreeColumn mTreeColumnName; + private CheckboxTreeViewer mTreeViewer; public PackagesPage( Composite parent, @@ -166,25 +139,45 @@ public class PackagesPage extends Composite implements ISdkChangeListener { UpdaterData updaterData, SdkInvocationContext context) { super(parent, swtStyle); - mUpdaterData = updaterData; - mContext = context; + mImpl = new PackagesPageImpl(updaterData) { + @Override + protected boolean isUiDisposed() { + return mGroupPackages == null || mGroupPackages.isDisposed(); + }; + @Override + protected void syncExec(Runnable runnable) { + if (!isUiDisposed()) { + mGroupPackages.getDisplay().syncExec(runnable); + } + }; + @Override + protected void refreshViewerInput() { + PackagesPage.this.refreshViewerInput(); + } + + @Override + protected boolean isSortByApi() { + return PackagesPage.this.isSortByApi(); + } - mDiffLogic = new PackagesDiffLogic(updaterData); + @Override + protected Font getTreeFontItalic() { + return mTreeFontItalic; + } + + @Override + protected void loadPackages(boolean useLocalCache, boolean overrideExisting) { + PackagesPage.this.loadPackages(useLocalCache, overrideExisting); + } + }; + mContext = context; createContents(this); postCreate(); //$hide$ } public void performFirstLoad() { - // First a package loader is created that only checks - // the local cache xml files. It populates the package - // list based on what the client got last, essentially. - loadPackages(true /*useLocalCache*/, false /*overrideExisting*/); - - // Next a regular package loader is created that will - // respect the expiration and refresh parameters of the - // download cache. - loadPackages(false /*useLocalCache*/, true /*overrideExisting*/); + mImpl.performFirstLoad(); } @SuppressWarnings("unused") @@ -202,12 +195,39 @@ public class PackagesPage extends Composite implements ISdkChangeListener { GridDataBuilder.create(mTextSdkOsPath).hFill().vCenter().hGrab(); mTextSdkOsPath.setEnabled(false); - mGroupPackages = new Group(parent, SWT.NONE); + Group groupPackages = new Group(parent, SWT.NONE); + mGroupPackages = groupPackages; GridDataBuilder.create(mGroupPackages).fill().grab().hSpan(2); - mGroupPackages.setText("Packages"); - GridLayoutBuilder.create(mGroupPackages).columns(1); + groupPackages.setText("Packages"); + GridLayoutBuilder.create(groupPackages).columns(1); + + mTreeViewer = new CheckboxTreeViewer(groupPackages, SWT.BORDER); + mImpl.setITreeViewer(new PackagesPageImpl.ICheckboxTreeViewer() { + @Override + public Object getInput() { + return mTreeViewer.getInput(); + } - mTreeViewer = new CheckboxTreeViewer(mGroupPackages, SWT.BORDER); + @Override + public void setInput(List<PkgCategory> cats) { + mTreeViewer.setInput(cats); + } + + @Override + public void setContentProvider(PkgContentProvider pkgContentProvider) { + mTreeViewer.setContentProvider(pkgContentProvider); + } + + @Override + public void refresh() { + mTreeViewer.refresh(); + } + + @Override + public Object[] getCheckedElements() { + return mTreeViewer.getCheckedElements(); + } + }); mTreeViewer.addFilter(new ViewerFilter() { @Override public boolean select(Viewer viewer, Object parentElement, Object element) { @@ -229,39 +249,45 @@ public class PackagesPage extends Composite implements ISdkChangeListener { } }); - mTree = mTreeViewer.getTree(); - mTree.setLinesVisible(true); - mTree.setHeaderVisible(true); - GridDataBuilder.create(mTree).fill().grab(); + Tree tree = mTreeViewer.getTree(); + tree.setLinesVisible(true); + tree.setHeaderVisible(true); + GridDataBuilder.create(tree).fill().grab(); // column name icon is set when loading depending on the current filter type // (e.g. API level or source) - mColumnName = new TreeViewerColumn(mTreeViewer, SWT.NONE); - mTreeColumnName = mColumnName.getColumn(); + TreeViewerColumn columnName = new TreeViewerColumn(mTreeViewer, SWT.NONE); + mTreeColumnName = columnName.getColumn(); mTreeColumnName.setText("Name"); mTreeColumnName.setWidth(340); - mColumnApi = new TreeViewerColumn(mTreeViewer, SWT.NONE); - TreeColumn treeColumn2 = mColumnApi.getColumn(); + TreeViewerColumn columnApi = new TreeViewerColumn(mTreeViewer, SWT.NONE); + TreeColumn treeColumn2 = columnApi.getColumn(); treeColumn2.setText("API"); treeColumn2.setAlignment(SWT.CENTER); treeColumn2.setWidth(50); - mColumnRevision = new TreeViewerColumn(mTreeViewer, SWT.NONE); - TreeColumn treeColumn3 = mColumnRevision.getColumn(); + TreeViewerColumn columnRevision = new TreeViewerColumn(mTreeViewer, SWT.NONE); + TreeColumn treeColumn3 = columnRevision.getColumn(); treeColumn3.setText("Rev."); treeColumn3.setToolTipText("Revision currently installed"); treeColumn3.setAlignment(SWT.CENTER); treeColumn3.setWidth(50); - mColumnStatus = new TreeViewerColumn(mTreeViewer, SWT.NONE); - TreeColumn treeColumn4 = mColumnStatus.getColumn(); + TreeViewerColumn columnStatus = new TreeViewerColumn(mTreeViewer, SWT.NONE); + TreeColumn treeColumn4 = columnStatus.getColumn(); treeColumn4.setText("Status"); treeColumn4.setAlignment(SWT.LEAD); treeColumn4.setWidth(190); - mGroupOptions = new Composite(mGroupPackages, SWT.NONE); + mImpl.setIColumns( + wrapColumn(columnName), + wrapColumn(columnApi), + wrapColumn(columnRevision), + wrapColumn(columnStatus)); + + mGroupOptions = new Composite(groupPackages, SWT.NONE); GridDataBuilder.create(mGroupOptions).hFill().vCenter().hGrab(); GridLayoutBuilder.create(mGroupOptions).columns(6).noMargins(); @@ -392,9 +418,18 @@ public class PackagesPage extends Composite implements ISdkChangeListener { }); } + private PackagesPageImpl.ITreeViewerColumn wrapColumn(final TreeViewerColumn column) { + return new PackagesPageImpl.ITreeViewerColumn() { + @Override + public void setLabelProvider(ColumnLabelProvider labelProvider) { + column.setLabelProvider(labelProvider); + } + }; + } + private Image getImage(String filename) { - if (mUpdaterData != null) { - ImageFactory imgFactory = mUpdaterData.getImageFactory(); + if (mImpl.mUpdaterData != null) { + ImageFactory imgFactory = mImpl.mUpdaterData.getImageFactory(); if (imgFactory != null) { return imgFactory.getImageByName(filename); } @@ -418,19 +453,19 @@ public class PackagesPage extends Composite implements ISdkChangeListener { switch (action) { case RELOAD: - fullReload(); + mImpl.fullReload(); break; case SHOW_ADDON_SITES: - AddonSitesDialog d = new AddonSitesDialog(getShell(), mUpdaterData); + AddonSitesDialog d = new AddonSitesDialog(getShell(), mImpl.mUpdaterData); if (d.open()) { - loadPackages(); + mImpl.loadPackages(); } break; case TOGGLE_SHOW_ARCHIVES: mDisplayArchives = !mDisplayArchives; // Force the viewer to be refreshed - ((PkgContentProvider) mTreeViewer.getContentProvider()).setDisplayArchives( - mDisplayArchives); + ((PkgContentProvider) mTreeViewer.getContentProvider()). + setDisplayArchives(mDisplayArchives); mTreeViewer.setInput(null); refreshViewerInput(); syncViewerSelection(); @@ -533,29 +568,23 @@ public class PackagesPage extends Composite implements ISdkChangeListener { } private void postCreate() { - if (mUpdaterData != null) { - mTextSdkOsPath.setText(mUpdaterData.getOsSdkRoot()); + mImpl.postCreate(); + + if (mImpl.mUpdaterData != null) { + mTextSdkOsPath.setText(mImpl.mUpdaterData.getOsSdkRoot()); } - mTreeViewer.setContentProvider(new PkgContentProvider(mTreeViewer)); ((PkgContentProvider) mTreeViewer.getContentProvider()).setDisplayArchives( - mDisplayArchives); - ColumnViewerToolTipSupport.enableFor(mTreeViewer, ToolTip.NO_RECREATE); + mDisplayArchives); - mColumnApi.setLabelProvider( - new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mColumnApi))); - mColumnName.setLabelProvider( - new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mColumnName))); - mColumnStatus.setLabelProvider( - new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mColumnStatus))); - mColumnRevision.setLabelProvider( - new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mColumnRevision))); + ColumnViewerToolTipSupport.enableFor(mTreeViewer, ToolTip.NO_RECREATE); - FontData fontData = mTree.getFont().getFontData()[0]; + Tree tree = mTreeViewer.getTree(); + FontData fontData = tree.getFont().getFontData()[0]; fontData.setStyle(SWT.ITALIC); - mTreeFontItalic = new Font(mTree.getDisplay(), fontData); + mTreeFontItalic = new Font(tree.getDisplay(), fontData); - mTree.addDisposeListener(new DisposeListener() { + tree.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { mTreeFontItalic.dispose(); @@ -564,52 +593,8 @@ public class PackagesPage extends Composite implements ISdkChangeListener { }); } - /** - * Performs a full reload by removing all cached packages data, including the platforms - * and addons from the sdkmanager instance. This will perform a full local parsing - * as well as a full reload of the remote data (by fetching all sources again.) - */ - private void fullReload() { - // Clear all source information, forcing them to be refreshed. - mUpdaterData.getSources().clearAllPackages(); - // Clear and reload all local data too. - localReload(); - } - - /** - * Performs a full reload of all the local package information, including the platforms - * and addons from the sdkmanager instance. This will perform a full local parsing. - * <p/> - * This method does NOT force a new fetch of the remote sources. - * - * @see #fullReload() - */ - private void localReload() { - // Clear all source caches, otherwise loading will use the cached data - mUpdaterData.getLocalSdkParser().clearPackages(); - mUpdaterData.getSdkManager().reloadSdk(mUpdaterData.getSdkLog()); - loadPackages(); - } - - /** - * Performs a "normal" reload of the package information, use the default download - * cache and refreshing strategy as needed. - */ - private void loadPackages() { - loadPackages(false /*useLocalCache*/, false /*overrideExisting*/); - } - - /** - * Performs a reload of the package information. - * - * @param useLocalCache When true, the {@link PackageLoader} is switched to use - * a specific {@link DownloadCache} using the {@link Strategy#ONLY_CACHE}, meaning - * it will only use data from the local cache. It will not try to fetch or refresh - * manifests. This is used once the very first time the sdk manager window opens - * and is typically followed by a regular load with refresh. - */ - private void loadPackages(final boolean useLocalCache, final boolean overrideExisting) { - if (mUpdaterData == null) { + private void loadPackages(boolean useLocalCache, boolean overrideExisting) { + if (mImpl.mUpdaterData == null) { return; } @@ -619,7 +604,7 @@ public class PackagesPage extends Composite implements ISdkChangeListener { // action done after loadPackages must check the UI hasn't been // disposed yet. Otherwise hilarity ensues. - final boolean displaySortByApi = isSortByApi(); + boolean displaySortByApi = isSortByApi(); if (mTreeColumnName.isDisposed()) { // If the UI got disposed, don't try to load anything since we won't be @@ -627,76 +612,11 @@ public class PackagesPage extends Composite implements ISdkChangeListener { return; } - mTreeColumnName.setImage(getImage(displaySortByApi ? ICON_SORT_BY_API - : ICON_SORT_BY_SOURCE)); - - PackageLoader packageLoader = null; - if (useLocalCache) { - packageLoader = - new PackageLoader(mUpdaterData, new DownloadCache(Strategy.ONLY_CACHE)); - } else { - packageLoader = mUpdaterData.getPackageLoader(); - } - assert packageLoader != null; - - mDiffLogic.updateStart(); - packageLoader.loadPackages(overrideExisting, new ISourceLoadedCallback() { - @Override - public boolean onUpdateSource(SdkSource source, Package[] newPackages) { - // This runs in a thread and must not access UI directly. - final boolean changed = mDiffLogic.updateSourcePackages( - displaySortByApi, source, newPackages); + mTreeColumnName.setImage(getImage( + displaySortByApi ? PackagesPageIcons.ICON_SORT_BY_API + : PackagesPageIcons.ICON_SORT_BY_SOURCE)); - if (!mGroupPackages.isDisposed()) { - mGroupPackages.getDisplay().syncExec(new Runnable() { - @Override - public void run() { - if (changed || - mTreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) { - refreshViewerInput(); - } - } - }); - } - - // Return true to tell the loader to continue with the next source. - // Return false to stop the loader if any UI has been disposed, which can - // happen if the user is trying to close the window during the load operation. - return !mGroupPackages.isDisposed(); - } - - @Override - public void onLoadCompleted() { - // This runs in a thread and must not access UI directly. - final boolean changed = mDiffLogic.updateEnd(displaySortByApi); - - if (!mGroupPackages.isDisposed()) { - mGroupPackages.getDisplay().syncExec(new Runnable() { - @Override - public void run() { - if (changed || - mTreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) { - refreshViewerInput(); - } - - if (!useLocalCache && - mDiffLogic.isFirstLoadComplete() && - !mGroupPackages.isDisposed()) { - // At the end of the first load, if nothing is selected then - // automatically select all new and update packages. - Object[] checked = mTreeViewer.getCheckedElements(); - if (checked == null || checked.length == 0) { - onSelectNewUpdates( - false, //selectNew - true, //selectUpdates, - true); //selectTop - } - } - } - }); - } - } - }); + mImpl.loadPackagesImpl(useLocalCache, overrideExisting); } private void refreshViewerInput() { @@ -704,16 +624,9 @@ public class PackagesPage extends Composite implements ISdkChangeListener { // Since the official Android source gets loaded first, it makes the // window look non-empty a lot sooner. if (!mGroupPackages.isDisposed()) { - - List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi()); - if (mTreeViewer.getInput() != cats) { - // set initial input - mTreeViewer.setInput(cats); - } else { - // refresh existing, which preserves the expanded state, the selection - // and the checked state. - mTreeViewer.refresh(); - } + try { + mImpl.setViewerInput(); + } catch (Exception ignore) {} // set the initial expanded state expandInitial(mTreeViewer.getInput()); @@ -787,11 +700,12 @@ public class PackagesPage extends Composite implements ISdkChangeListener { if (mTreeViewer != null && !mTreeViewer.getTree().isDisposed()) { boolean enablePreviews = - mUpdaterData.getSettingsController().getSettings().getEnablePreviews(); + mImpl.mUpdaterData.getSettingsController().getSettings().getEnablePreviews(); mTreeViewer.setExpandedState(elem, true); nextCategory: for (Object pkg : - ((ITreeContentProvider) mTreeViewer.getContentProvider()).getChildren(elem)) { + ((ITreeContentProvider) mTreeViewer.getContentProvider()). + getChildren(elem)) { if (pkg instanceof PkgCategory) { PkgCategory cat = (PkgCategory) pkg; @@ -845,7 +759,8 @@ public class PackagesPage extends Composite implements ISdkChangeListener { return; } - ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider(); + ITreeContentProvider provider = + (ITreeContentProvider) mTreeViewer.getContentProvider(); Object[] children = provider.getElements(elem); if (children == null) { return; @@ -874,7 +789,8 @@ public class PackagesPage extends Composite implements ISdkChangeListener { boolean checked, boolean fixChildren, boolean fixParent) { - ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider(); + ITreeContentProvider provider = + (ITreeContentProvider) mTreeViewer.getContentProvider(); // fix the item itself if (checked != mTreeViewer.getChecked(elem)) { @@ -958,11 +874,7 @@ public class PackagesPage extends Composite implements ISdkChangeListener { */ private void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) { // This does not update the tree itself, syncViewerSelection does it below. - mDiffLogic.checkNewUpdateItems( - selectNew, - selectUpdates, - selectTop, - SdkConstants.CURRENT_PLATFORM); + mImpl.onSelectNewUpdates(selectNew, selectUpdates, selectTop); syncViewerSelection(); updateButtonsState(); } @@ -972,7 +884,7 @@ public class PackagesPage extends Composite implements ISdkChangeListener { */ private void onDeselectAll() { // This does not update the tree itself, syncViewerSelection does it below. - mDiffLogic.uncheckAllItems(); + mImpl.onDeselectAll(); syncViewerSelection(); updateButtonsState(); } @@ -983,8 +895,10 @@ public class PackagesPage extends Composite implements ISdkChangeListener { * This does not update the tree itself. */ private void copySelection(boolean fromSourceToApi) { - List<PkgItem> fromItems = mDiffLogic.getAllPkgItems(!fromSourceToApi, fromSourceToApi); - List<PkgItem> toItems = mDiffLogic.getAllPkgItems(fromSourceToApi, !fromSourceToApi); + List<PkgItem> fromItems = + mImpl.mDiffLogic.getAllPkgItems(!fromSourceToApi, fromSourceToApi); + List<PkgItem> toItems = + mImpl.mDiffLogic.getAllPkgItems(fromSourceToApi, !fromSourceToApi); // deselect all targets for (PkgItem item : toItems) { @@ -1090,12 +1004,12 @@ public class PackagesPage extends Composite implements ISdkChangeListener { ArrayList<Archive> archives = new ArrayList<Archive>(); getArchivesForInstall(archives); - if (mUpdaterData != null) { + if (mImpl.mUpdaterData != null) { boolean needsRefresh = false; try { beginOperationPending(); - List<Archive> installed = mUpdaterData.updateOrInstallAll_WithGUI( + List<Archive> installed = mImpl.mUpdaterData.updateOrInstallAll_WithGUI( archives, mCheckFilterObsolete.getSelection() /* includeObsoletes */, mContext == SdkInvocationContext.IDE ? @@ -1107,7 +1021,7 @@ public class PackagesPage extends Composite implements ISdkChangeListener { if (needsRefresh) { // The local package list has changed, make sure to refresh it - localReload(); + mImpl.localReload(); } } } @@ -1219,7 +1133,7 @@ public class PackagesPage extends Composite implements ISdkChangeListener { try { beginOperationPending(); - mUpdaterData.getTaskFactory().start("Delete Package", new ITask() { + mImpl.mUpdaterData.getTaskFactory().start("Delete Package", new ITask() { @Override public void run(ITaskMonitor monitor) { monitor.setProgressMax(archives.size() + 1); @@ -1245,7 +1159,7 @@ public class PackagesPage extends Composite implements ISdkChangeListener { endOperationPending(); // The local package list has changed, make sure to refresh it - localReload(); + mImpl.localReload(); } } } @@ -1335,236 +1249,6 @@ public class PackagesPage extends Composite implements ISdkChangeListener { // ---------------------- - public class PkgCellLabelProvider extends ColumnLabelProvider implements ITableFontProvider { - - private final TreeViewerColumn mColumn; - - public PkgCellLabelProvider(TreeViewerColumn column) { - super(); - mColumn = column; - } - - @Override - public String getText(Object element) { - - if (mColumn == mColumnName) { - if (element instanceof PkgCategory) { - return ((PkgCategory) element).getLabel(); - } else if (element instanceof PkgItem) { - return getPkgItemName((PkgItem) element); - } else if (element instanceof IDescription) { - return ((IDescription) element).getShortDescription(); - } - - } else if (mColumn == mColumnApi) { - int api = -1; - if (element instanceof PkgItem) { - api = ((PkgItem) element).getApi(); - } - if (api >= 1) { - return Integer.toString(api); - } - - } else if (mColumn == mColumnRevision) { - if (element instanceof PkgItem) { - PkgItem pkg = (PkgItem) element; - return pkg.getRevision().toShortString(); - } - - } else if (mColumn == mColumnStatus) { - if (element instanceof PkgItem) { - PkgItem pkg = (PkgItem) element; - - switch(pkg.getState()) { - case INSTALLED: - Package update = pkg.getUpdatePkg(); - if (update != null) { - return String.format( - "Update available: rev. %1$s", - update.getRevision().toShortString()); - } - return "Installed"; - - case NEW: - Package p = pkg.getMainPackage(); - if (p != null && p.hasCompatibleArchive()) { - return "Not installed"; - } else { - return String.format("Not compatible with %1$s", - SdkConstants.currentPlatformName()); - } - } - return pkg.getState().toString(); - - } else if (element instanceof Package) { - // This is an update package. - return "New revision " + ((Package) element).getRevision().toShortString(); - } - } - - return ""; //$NON-NLS-1$ - } - - private String getPkgItemName(PkgItem item) { - String name = item.getName().trim(); - - if (isSortByApi()) { - // When sorting by API, the package name might contains the API number - // or the platform name at the end. If we find it, cut it out since it's - // redundant. - - PkgCategoryApi cat = (PkgCategoryApi) findCategoryForItem(item); - String apiLabel = cat.getApiLabel(); - String platLabel = cat.getPlatformName(); - - if (platLabel != null && name.endsWith(platLabel)) { - return name.substring(0, name.length() - platLabel.length()); - - } else if (apiLabel != null && name.endsWith(apiLabel)) { - return name.substring(0, name.length() - apiLabel.length()); - - } else if (platLabel != null && item.isObsolete() && name.indexOf(platLabel) > 0) { - // For obsolete items, the format is "<base name> <platform name> (Obsolete)" - // so in this case only accept removing a platform name that is not at - // the end. - name = name.replace(platLabel, ""); //$NON-NLS-1$ - } - } - - // Collapse potential duplicated spacing - name = name.replaceAll(" +", " "); //$NON-NLS-1$ //$NON-NLS-2$ - - return name; - } - - private PkgCategory findCategoryForItem(PkgItem item) { - List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi()); - for (PkgCategory cat : cats) { - for (PkgItem i : cat.getItems()) { - if (i == item) { - return cat; - } - } - } - - return null; - } - - @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()); - } else if (element instanceof PkgItem) { - return imgFactory.getImageForObject(((PkgItem) element).getMainPackage()); - } - return imgFactory.getImageForObject(element); - - } else if (mColumn == mColumnStatus && element instanceof PkgItem) { - PkgItem pi = (PkgItem) element; - switch(pi.getState()) { - case INSTALLED: - if (pi.hasUpdatePkg()) { - return imgFactory.getImageByName(ICON_PKG_UPDATE); - } else { - return imgFactory.getImageByName(ICON_PKG_INSTALLED); - } - case NEW: - Package p = pi.getMainPackage(); - if (p != null && p.hasCompatibleArchive()) { - return imgFactory.getImageByName(ICON_PKG_NEW); - } else { - return imgFactory.getImageByName(ICON_PKG_INCOMPAT); - } - } - } - } - return super.getImage(element); - } - - // -- ITableFontProvider - - @Override - public Font getFont(Object element, int columnIndex) { - if (element instanceof PkgItem) { - if (((PkgItem) element).getState() == PkgState.NEW) { - return mTreeFontItalic; - } - } else if (element instanceof Package) { - // update package - return mTreeFontItalic; - } - return super.getFont(element); - } - - // -- Tooltip support - - @Override - public String getToolTipText(Object element) { - PkgItem pi = element instanceof PkgItem ? (PkgItem) element : null; - if (pi != null) { - element = pi.getMainPackage(); - } - if (element instanceof IDescription) { - String s = getTooltipDescription((IDescription) element); - - if (pi != null && pi.hasUpdatePkg()) { - s += "\n-----------------" + //$NON-NLS-1$ - "\nUpdate Available:\n" + //$NON-NLS-1$ - getTooltipDescription(pi.getUpdatePkg()); - } - - return s; - } - return super.getToolTipText(element); - } - - private String getTooltipDescription(IDescription element) { - String s = element.getLongDescription(); - if (element instanceof Package) { - Package p = (Package) element; - - if (!p.isLocal()) { - // For non-installed item, try to find a download size - for (Archive a : p.getArchives()) { - if (!a.isLocal() && a.isCompatible()) { - s += '\n' + a.getSizeDescription(); - break; - } - } - } - - // Display info about where this package comes/came from - SdkSource src = p.getParentSource(); - if (src != null) { - try { - URL url = new URL(src.getUrl()); - String host = url.getHost(); - if (p.isLocal()) { - s += String.format("\nInstalled from %1$s", host); - } else { - s += String.format("\nProvided by %1$s", host); - } - } catch (MalformedURLException ignore) { - } - } - } - return s; - } - - @Override - public Point getToolTipShift(Object object) { - return new Point(15, 5); - } - - @Override - public int getToolTipDisplayDelayTime(Object object) { - return 500; - } - } // --- Implementation of ISdkChangeListener --- @@ -1577,7 +1261,7 @@ public class PackagesPage extends Composite implements ISdkChangeListener { public void onSdkReload() { // The sdkmanager finished reloading its data. We must not call localReload() from here // since we don't want to alter the sdkmanager's data that just finished loading. - loadPackages(); + mImpl.loadPackages(); } @Override diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java new file mode 100755 index 0000000..4fe8fca --- /dev/null +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012 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.ui; + + +/** + * Icons used by {@link PackagesPage}. + */ +public class PackagesPageIcons { + + public static final String ICON_CAT_OTHER = "pkgcat_other_16.png"; //$NON-NLS-1$ + public static final String ICON_CAT_PLATFORM = "pkgcat_16.png"; //$NON-NLS-1$ + public static final String ICON_SORT_BY_SOURCE = "source_icon16.png"; //$NON-NLS-1$ + public static final String ICON_SORT_BY_API = "platform_pkg_16.png"; //$NON-NLS-1$ + public static final String ICON_PKG_NEW = "pkg_new_16.png"; //$NON-NLS-1$ + public static final String ICON_PKG_INCOMPAT = "pkg_incompat_16.png"; //$NON-NLS-1$ + public static final String ICON_PKG_UPDATE = "pkg_update_16.png"; //$NON-NLS-1$ + public static final String ICON_PKG_INSTALLED = "pkg_installed_16.png"; //$NON-NLS-1$ +} diff --git a/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java new file mode 100755 index 0000000..3ca0ee3 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java @@ -0,0 +1,568 @@ +/* + * Copyright (C) 2012 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.ui; + +import com.android.SdkConstants; +import com.android.sdklib.internal.repository.DownloadCache; +import com.android.sdklib.internal.repository.DownloadCache.Strategy; +import com.android.sdklib.internal.repository.IDescription; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdkuilib.internal.repository.UpdaterData; +import com.android.sdkuilib.internal.repository.core.PackageLoader; +import com.android.sdkuilib.internal.repository.core.PackageLoader.ISourceLoadedCallback; +import com.android.sdkuilib.internal.repository.core.PackagesDiffLogic; +import com.android.sdkuilib.internal.repository.core.PkgCategory; +import com.android.sdkuilib.internal.repository.core.PkgCategoryApi; +import com.android.sdkuilib.internal.repository.core.PkgContentProvider; +import com.android.sdkuilib.internal.repository.core.PkgItem; +import com.android.sdkuilib.internal.repository.core.PkgItem.PkgState; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; + +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.IInputProvider; +import org.eclipse.jface.viewers.ITableFontProvider; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +/** + * Base class for {@link PackagesPage} that holds most of the logic to display + * the tree/list of packages. This class holds most of the logic and {@link PackagesPage} + * holds most of the UI (creating the UI, dealing with menus and buttons and tree + * selection.) This makes it easier to test the functionality by mocking only a + * subset of the UI. + */ +abstract class PackagesPageImpl { + + final UpdaterData mUpdaterData; + final PackagesDiffLogic mDiffLogic; + + private ICheckboxTreeViewer mITreeViewer; + private ITreeViewerColumn mIColumnName; + private ITreeViewerColumn mIColumnApi; + private ITreeViewerColumn mIColumnRevision; + private ITreeViewerColumn mIColumnStatus; + + PackagesPageImpl(UpdaterData updaterData) { + mUpdaterData = updaterData; + mDiffLogic = new PackagesDiffLogic(updaterData); + } + + /** + * Utility method that derived classes can override to check whether the UI is disposed. + * When the UI is disposed, most operations that affect the UI will be bypassed. + * @return True if UI is not available and should not be touched. + */ + abstract protected boolean isUiDisposed(); + + /** + * Utility method to execute a runnable on the main UI thread. + * Will do nothing if {@link #isUiDisposed()} returns false. + * @param runnable The runnable to execute on the main UI thread. + */ + abstract protected void syncExec(Runnable runnable); + + void performFirstLoad() { + // First a package loader is created that only checks + // the local cache xml files. It populates the package + // list based on what the client got last, essentially. + loadPackages(true /*useLocalCache*/, false /*overrideExisting*/); + + // Next a regular package loader is created that will + // respect the expiration and refresh parameters of the + // download cache. + loadPackages(false /*useLocalCache*/, true /*overrideExisting*/); + } + + public void setITreeViewer(ICheckboxTreeViewer iTreeViewer) { + mITreeViewer = iTreeViewer; + } + + public void setIColumns( + ITreeViewerColumn columnName, + ITreeViewerColumn columnApi, + ITreeViewerColumn columnRevision, + ITreeViewerColumn columnStatus) { + mIColumnName = columnName; + mIColumnApi = columnApi; + mIColumnRevision = columnRevision; + mIColumnStatus = columnStatus; + } + + void postCreate() { + // Caller needs to call setITreeViewer before this. + assert mITreeViewer != null; + // Caller needs to call setIColumns before this. + assert mIColumnApi != null; + assert mIColumnName != null; + assert mIColumnStatus != null; + assert mIColumnRevision != null; + + mITreeViewer.setContentProvider(new PkgContentProvider(mITreeViewer)); + + mIColumnApi.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnApi))); + mIColumnName.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnName))); + mIColumnStatus.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnStatus))); + mIColumnRevision.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnRevision))); + } + + /** + * Performs a full reload by removing all cached packages data, including the platforms + * and addons from the sdkmanager instance. This will perform a full local parsing + * as well as a full reload of the remote data (by fetching all sources again.) + */ + void fullReload() { + // Clear all source information, forcing them to be refreshed. + mUpdaterData.getSources().clearAllPackages(); + // Clear and reload all local data too. + localReload(); + } + + /** + * Performs a full reload of all the local package information, including the platforms + * and addons from the sdkmanager instance. This will perform a full local parsing. + * <p/> + * This method does NOT force a new fetch of the remote sources. + * + * @see #fullReload() + */ + void localReload() { + // Clear all source caches, otherwise loading will use the cached data + mUpdaterData.getLocalSdkParser().clearPackages(); + mUpdaterData.getSdkManager().reloadSdk(mUpdaterData.getSdkLog()); + loadPackages(); + } + + /** + * Performs a "normal" reload of the package information, use the default download + * cache and refreshing strategy as needed. + */ + void loadPackages() { + loadPackages(false /*useLocalCache*/, false /*overrideExisting*/); + } + + /** + * Performs a reload of the package information. + * + * @param useLocalCache When true, the {@link PackageLoader} is switched to use + * a specific {@link DownloadCache} using the {@link Strategy#ONLY_CACHE}, meaning + * it will only use data from the local cache. It will not try to fetch or refresh + * manifests. This is used once the very first time the sdk manager window opens + * and is typically followed by a regular load with refresh. + */ + abstract protected void loadPackages(boolean useLocalCache, boolean overrideExisting); + + /** + * Actual implementation of {@link #loadPackages(boolean, boolean)}. + * Derived implementations must call this to do the actual work after setting up the UI. + */ + void loadPackagesImpl(final boolean useLocalCache, final boolean overrideExisting) { + if (mUpdaterData == null) { + return; + } + + final boolean displaySortByApi = isSortByApi(); + + PackageLoader packageLoader = getPackageLoader(useLocalCache); + assert packageLoader != null; + + mDiffLogic.updateStart(); + packageLoader.loadPackages(overrideExisting, new ISourceLoadedCallback() { + @Override + public boolean onUpdateSource(SdkSource source, Package[] newPackages) { + // This runs in a thread and must not access UI directly. + final boolean changed = mDiffLogic.updateSourcePackages( + displaySortByApi, source, newPackages); + + syncExec(new Runnable() { + @Override + public void run() { + if (changed || + mITreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) { + refreshViewerInput(); + } + } + }); + + // Return true to tell the loader to continue with the next source. + // Return false to stop the loader if any UI has been disposed, which can + // happen if the user is trying to close the window during the load operation. + return !isUiDisposed(); + } + + @Override + public void onLoadCompleted() { + // This runs in a thread and must not access UI directly. + final boolean changed = mDiffLogic.updateEnd(displaySortByApi); + + syncExec(new Runnable() { + @Override + public void run() { + if (changed || + mITreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) { + try { + refreshViewerInput(); + } catch (Exception ignore) {} + } + + if (!useLocalCache && + mDiffLogic.isFirstLoadComplete() && + !isUiDisposed()) { + // At the end of the first load, if nothing is selected then + // automatically select all new and update packages. + Object[] checked = mITreeViewer.getCheckedElements(); + if (checked == null || checked.length == 0) { + onSelectNewUpdates( + false, //selectNew + true, //selectUpdates, + true); //selectTop + } + } + } + }); + } + }); + } + + /** + * Used by {@link #loadPackagesImpl(boolean, boolean)} to get the package + * loader for the first or second pass update. When starting the manager + * starts with a first pass that reads only from the local cache, with no + * extra network access. That's {@code useLocalCache} being true. + * <p/> + * Leter it does a second pass with {@code useLocalCache} set to false + * and actually uses the download cache specified in {@link UpdaterData}. + * + * This is extracted so that we can control this cache via unit tests. + */ + protected PackageLoader getPackageLoader(boolean useLocalCache) { + if (useLocalCache) { + return new PackageLoader(mUpdaterData, new DownloadCache(Strategy.ONLY_CACHE)); + } else { + return mUpdaterData.getPackageLoader(); + } + } + + /** + * Overridden by the UI to respond to a request to refresh the tree viewer + * when the input has changed. + * The implementation must call {@link #setViewerInput()} somehow and will + * also need to adjust the expand state of the tree items and/or update + * some buttons or other state. + */ + abstract protected void refreshViewerInput(); + + /** + * Invoked from {@link #refreshViewerInput()} to actually either set the + * input of the tree viewer or refresh it if it's the <em>same</em> input + * object. + */ + protected void setViewerInput() { + List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi()); + if (mITreeViewer.getInput() != cats) { + // set initial input + mITreeViewer.setInput(cats); + } else { + // refresh existing, which preserves the expanded state, the selection + // and the checked state. + mITreeViewer.refresh(); + } + } + + /** + * Overridden by the UI to determine if the tree should display packages sorted + * by API (returns true) or by repository source (returns false.) + */ + abstract protected boolean isSortByApi(); + + /** + * Checks all PkgItems that are either new or have updates or select top platform + * for initial run. + */ + void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) { + // This does not update the tree itself, syncViewerSelection does it in the caller. + mDiffLogic.checkNewUpdateItems( + selectNew, + selectUpdates, + selectTop, + SdkConstants.CURRENT_PLATFORM); + } + + /** + * Deselect all checked PkgItems. + */ + void onDeselectAll() { + // This does not update the tree itself, syncViewerSelection does it in the caller. + mDiffLogic.uncheckAllItems(); + } + + // ---------------------- + + abstract protected Font getTreeFontItalic(); + + class PkgCellLabelProvider extends ColumnLabelProvider implements ITableFontProvider { + + private final ITreeViewerColumn mColumn; + + public PkgCellLabelProvider(ITreeViewerColumn column) { + super(); + mColumn = column; + } + + @Override + public String getText(Object element) { + + if (mColumn == mIColumnName) { + if (element instanceof PkgCategory) { + return ((PkgCategory) element).getLabel(); + } else if (element instanceof PkgItem) { + return getPkgItemName((PkgItem) element); + } else if (element instanceof IDescription) { + return ((IDescription) element).getShortDescription(); + } + + } else if (mColumn == mIColumnApi) { + int api = -1; + if (element instanceof PkgItem) { + api = ((PkgItem) element).getApi(); + } + if (api >= 1) { + return Integer.toString(api); + } + + } else if (mColumn == mIColumnRevision) { + if (element instanceof PkgItem) { + PkgItem pkg = (PkgItem) element; + return pkg.getRevision().toShortString(); + } + + } else if (mColumn == mIColumnStatus) { + if (element instanceof PkgItem) { + PkgItem pkg = (PkgItem) element; + + switch(pkg.getState()) { + case INSTALLED: + Package update = pkg.getUpdatePkg(); + if (update != null) { + return String.format( + "Update available: rev. %1$s", + update.getRevision().toShortString()); + } + return "Installed"; + + case NEW: + Package p = pkg.getMainPackage(); + if (p != null && p.hasCompatibleArchive()) { + return "Not installed"; + } else { + return String.format("Not compatible with %1$s", + SdkConstants.currentPlatformName()); + } + } + return pkg.getState().toString(); + + } else if (element instanceof Package) { + // This is an update package. + return "New revision " + ((Package) element).getRevision().toShortString(); + } + } + + return ""; //$NON-NLS-1$ + } + + private String getPkgItemName(PkgItem item) { + String name = item.getName().trim(); + + if (isSortByApi()) { + // When sorting by API, the package name might contains the API number + // or the platform name at the end. If we find it, cut it out since it's + // redundant. + + PkgCategoryApi cat = (PkgCategoryApi) findCategoryForItem(item); + String apiLabel = cat.getApiLabel(); + String platLabel = cat.getPlatformName(); + + if (platLabel != null && name.endsWith(platLabel)) { + return name.substring(0, name.length() - platLabel.length()); + + } else if (apiLabel != null && name.endsWith(apiLabel)) { + return name.substring(0, name.length() - apiLabel.length()); + + } else if (platLabel != null && item.isObsolete() && name.indexOf(platLabel) > 0) { + // For obsolete items, the format is "<base name> <platform name> (Obsolete)" + // so in this case only accept removing a platform name that is not at + // the end. + name = name.replace(platLabel, ""); //$NON-NLS-1$ + } + } + + // Collapse potential duplicated spacing + name = name.replaceAll(" +", " "); //$NON-NLS-1$ //$NON-NLS-2$ + + return name; + } + + private PkgCategory findCategoryForItem(PkgItem item) { + List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi()); + for (PkgCategory cat : cats) { + for (PkgItem i : cat.getItems()) { + if (i == item) { + return cat; + } + } + } + + return null; + } + + @Override + public Image getImage(Object element) { + ImageFactory imgFactory = mUpdaterData.getImageFactory(); + + if (imgFactory != null) { + if (mColumn == mIColumnName) { + if (element instanceof PkgCategory) { + return imgFactory.getImageForObject(((PkgCategory) element).getIconRef()); + } else if (element instanceof PkgItem) { + return imgFactory.getImageForObject(((PkgItem) element).getMainPackage()); + } + return imgFactory.getImageForObject(element); + + } else if (mColumn == mIColumnStatus && element instanceof PkgItem) { + PkgItem pi = (PkgItem) element; + switch(pi.getState()) { + case INSTALLED: + if (pi.hasUpdatePkg()) { + return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_UPDATE); + } else { + return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_INSTALLED); + } + case NEW: + Package p = pi.getMainPackage(); + if (p != null && p.hasCompatibleArchive()) { + return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_NEW); + } else { + return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_INCOMPAT); + } + } + } + } + return super.getImage(element); + } + + // -- ITableFontProvider + + @Override + public Font getFont(Object element, int columnIndex) { + if (element instanceof PkgItem) { + if (((PkgItem) element).getState() == PkgState.NEW) { + return getTreeFontItalic(); + } + } else if (element instanceof Package) { + // update package + return getTreeFontItalic(); + } + return super.getFont(element); + } + + // -- Tooltip support + + @Override + public String getToolTipText(Object element) { + PkgItem pi = element instanceof PkgItem ? (PkgItem) element : null; + if (pi != null) { + element = pi.getMainPackage(); + } + if (element instanceof IDescription) { + String s = getTooltipDescription((IDescription) element); + + if (pi != null && pi.hasUpdatePkg()) { + s += "\n-----------------" + //$NON-NLS-1$ + "\nUpdate Available:\n" + //$NON-NLS-1$ + getTooltipDescription(pi.getUpdatePkg()); + } + + return s; + } + return super.getToolTipText(element); + } + + private String getTooltipDescription(IDescription element) { + String s = element.getLongDescription(); + if (element instanceof Package) { + Package p = (Package) element; + + if (!p.isLocal()) { + // For non-installed item, try to find a download size + for (Archive a : p.getArchives()) { + if (!a.isLocal() && a.isCompatible()) { + s += '\n' + a.getSizeDescription(); + break; + } + } + } + + // Display info about where this package comes/came from + SdkSource src = p.getParentSource(); + if (src != null) { + try { + URL url = new URL(src.getUrl()); + String host = url.getHost(); + if (p.isLocal()) { + s += String.format("\nInstalled from %1$s", host); + } else { + s += String.format("\nProvided by %1$s", host); + } + } catch (MalformedURLException ignore) { + } + } + } + return s; + } + + @Override + public Point getToolTipShift(Object object) { + return new Point(15, 5); + } + + @Override + public int getToolTipDisplayDelayTime(Object object) { + return 500; + } + } + + interface ICheckboxTreeViewer extends IInputProvider { + void setContentProvider(PkgContentProvider pkgContentProvider); + void refresh(); + void setInput(List<PkgCategory> cats); + Object[] getCheckedElements(); + } + + interface ITreeViewerColumn { + void setLabelProvider(ColumnLabelProvider labelProvider); + } +} diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockDownloadCache.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockDownloadCache.java new file mode 100755 index 0000000..4adb9e2 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockDownloadCache.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2012 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.CanceledByUserException; +import com.android.sdklib.internal.repository.DownloadCache; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.utils.Pair; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.ProtocolVersion; +import org.apache.http.message.BasicHttpResponse; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; + +/** A mock UpdaterData that simply records what would have been installed. */ +public class MockDownloadCache extends DownloadCache { + + private final File mCacheRoot; + + /** Map url => payload bytes, http code response. + * If the payload pair is null, an exception such as FNF is thrown. */ + private final Map<String, Payload> mDirectPayloads = new HashMap<String, Payload>(); + /** Map url => payload bytes, http code response. + * If the payload pair is null, an exception such as FNF is thrown. */ + private final Map<String, Payload> mCachedPayloads = new HashMap<String, Payload>(); + + private final Map<String, Integer> mDirectHits = new TreeMap<String, Integer>(); + private final Map<String, Integer> mCachedHits = new TreeMap<String, Integer>(); + + private Strategy mOverrideStrategy; + + public final static int THROW_FNF = -1; + + /** + * Creates a download cache with a {@code DIRECT} strategy and + * no root {@code $HOME/.android} folder, which effectively disables the cache. + */ + public MockDownloadCache() { + super(DownloadCache.Strategy.DIRECT); + mCacheRoot = null; + } + + /** + * Creates a download with the given strategy and the given cache root. + */ + public MockDownloadCache(DownloadCache.Strategy strategy, File cacheRoot) { + super(strategy); + mCacheRoot = cacheRoot; + } + + @Override + protected File initCacheRoot() { + return mCacheRoot; + } + + /** + * Override the {@link DownloadCache.Strategy} of the cache. + * This lets us set it temporarily to {@link DownloadCache.Strategy#ONLY_CACHE}, + * which will force {@link #openCachedUrl(String, ITaskMonitor)} to throw an FNF, + * essentially simulating an empty cache at first. + * <p/> + * Setting it back to null reverts the behavior to its default. + */ + public void overrideStrategy(DownloadCache.Strategy strategy) { + mOverrideStrategy = strategy; + } + + /** + * Register a direct payload response. + * + * @param url The URL to match. + * @param httpCode The expected response code. + * Use {@link #THROW_FNF} to mean an FNF should be thrown (which is what the + * httpClient stack seems to return instead of {@link HttpStatus#SC_NOT_FOUND}.) + * @param content The payload to return. + * As a shortcut a null will be replaced by an empty byte array. + */ + public void registerDirectPayload(String url, int httpCode, byte[] content) { + mDirectPayloads.put(url, new Payload(httpCode, content)); + } + + /** + * Register a cached payload response. + * + * @param url The URL to match. + * @param content The payload to return or null to throw a FNF. + */ + public void registerCachedPayload(String url, byte[] content) { + mCachedPayloads.put(url, + new Payload(content == null ? THROW_FNF : HttpStatus.SC_OK, content)); + } + + public String[] getDirectHits() { + ArrayList<String> list = new ArrayList<String>(); + synchronized (mDirectHits) { + for (Entry<String, Integer> entry : mDirectHits.entrySet()) { + list.add(String.format("<%1$s : %2$d>", + entry.getKey(), entry.getValue().intValue())); + } + } + return list.toArray(new String[list.size()]); + } + + public String[] getCachedHits() { + ArrayList<String> list = new ArrayList<String>(); + synchronized (mCachedHits) { + for (Entry<String, Integer> entry : mCachedHits.entrySet()) { + list.add(String.format("<%1$s : %2$d>", + entry.getKey(), entry.getValue().intValue())); + } + } + return list.toArray(new String[list.size()]); + } + + public void clearDirectHits() { + synchronized (mDirectHits) { + mDirectHits.clear(); + } + } + + public void clearCachedHits() { + synchronized (mCachedHits) { + mCachedHits.clear(); + } + } + + /** + * Override openDirectUrl to return one of the registered payloads or throw a FNF exception. + * This totally ignores the cache's {@link DownloadCache.Strategy}. + */ + @Override + public Pair<InputStream, HttpResponse> openDirectUrl( + String urlString, + Header[] headers, + ITaskMonitor monitor) throws IOException, CanceledByUserException { + + synchronized (mDirectHits) { + Integer count = mDirectHits.get(urlString); + mDirectHits.put(urlString, (count == null ? 0 : count.intValue()) + 1); + } + + Payload payload = mDirectPayloads.get(urlString); + + if (payload == null || payload.mHttpCode == THROW_FNF) { + throw new FileNotFoundException(urlString); + } + + byte[] content = payload.mContent; + if (content == null) { + content = new byte[0]; + } + + InputStream is = new ByteArrayInputStream(content); + HttpResponse hr = new BasicHttpResponse( + new ProtocolVersion("HTTP", 1, 1), + payload.mHttpCode, + "Http-Code-" + payload.mHttpCode); + + return Pair.of(is, hr); + } + + /** + * Override openCachedUrl to return one of the registered payloads or throw a FNF exception. + * This totally ignores the cache's {@link DownloadCache.Strategy}. + * It will however throw a FNF if {@link #overrideStrategy(Strategy)} is set to + * {@link DownloadCache.Strategy#ONLY_CACHE}. + */ + @Override + public InputStream openCachedUrl(String urlString, ITaskMonitor monitor) + throws IOException, CanceledByUserException { + + synchronized (mCachedHits) { + Integer count = mCachedHits.get(urlString); + mCachedHits.put(urlString, (count == null ? 0 : count.intValue()) + 1); + } + + if (Strategy.ONLY_CACHE.equals(mOverrideStrategy)) { + // Override the cache to read only "local cached" data. + // In this first phase, we assume there's nothing cached. + // TODO register first-pass files later. + throw new FileNotFoundException(urlString); + } + + Payload payload = mCachedPayloads.get(urlString); + + if (payload == null || payload.mHttpCode != HttpStatus.SC_OK) { + throw new FileNotFoundException(urlString); + } + + byte[] content = payload.mContent; + if (content == null) { + content = new byte[0]; + } + + return new ByteArrayInputStream(content); + } + + private static class Payload { + final byte[] mContent; + final int mHttpCode; + + Payload(int httpCode, byte[] content) { + mHttpCode = httpCode; + mContent = content; + } + } + +} diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockUpdaterData.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockUpdaterData.java index 45e3163..f19bfcc 100755 --- a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockUpdaterData.java +++ b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/MockUpdaterData.java @@ -25,6 +25,8 @@ import com.android.sdklib.internal.repository.MockEmptySdkManager; import com.android.sdklib.internal.repository.NullTaskMonitor; import com.android.sdklib.internal.repository.archives.ArchiveInstaller; import com.android.sdklib.internal.repository.archives.ArchiveReplacement; +import com.android.sdklib.internal.repository.sources.SdkSourceCategory; +import com.android.sdklib.internal.repository.sources.SdkSources; import com.android.sdklib.mock.MockLog; import com.android.sdkuilib.internal.repository.SettingsController.Settings; import com.android.sdkuilib.internal.repository.icons.ImageFactory; @@ -33,7 +35,6 @@ import com.android.utils.NullLogger; import org.eclipse.swt.graphics.Image; -import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -45,8 +46,17 @@ public class MockUpdaterData extends UpdaterData { private final List<ArchiveReplacement> mInstalled = new ArrayList<ArchiveReplacement>(); - private DownloadCache mMockDownloadCache; + private DownloadCache mMockDownloadCache = new MockDownloadCache(); + private final SdkSources mMockSdkSources = new SdkSources() { + @Override + public void loadUserAddons(ILogger log) { + // This source does not load user addons. + removeAll(SdkSourceCategory.USER_ADDONS); + }; + }; + + /** Creates a {@link MockUpdaterData} using a {@link MockEmptySdkManager}. */ public MockUpdaterData() { super(SDK_PATH, new MockLog()); @@ -54,6 +64,14 @@ public class MockUpdaterData extends UpdaterData { setImageFactory(new NullImageFactory()); } + /** Creates a {@link MockUpdaterData} using the given {@link SdkManager}. */ + public MockUpdaterData(SdkManager sdkManager) { + super(sdkManager.getLocation(), new MockLog()); + setSdkManager(sdkManager); + setTaskFactory(new MockTaskFactory()); + setImageFactory(new NullImageFactory()); + } + /** Gives access to the internal {@link #installArchives(List, int)}. */ public void _installArchives(List<ArchiveInfo> result) { installArchives(result, 0/*flags*/); @@ -75,9 +93,19 @@ public class MockUpdaterData extends UpdaterData { return createSettingsController(getSdkLog()); } + /** Override original implementation to do nothing. */ @Override public void reloadSdk() { - // bypass original implementation + // nop + } + + /** + * Override original implementation to return a mock SdkSources that + * does not load user add-ons from the local .android/repository.cfg file. + */ + @Override + public SdkSources getSources() { + return mMockSdkSources; } /** Returns a mock installer that simply records what would have been installed. */ @@ -98,30 +126,23 @@ public class MockUpdaterData extends UpdaterData { }; } - /** - * Lazily initializes and returns a mock download cache that doesn't use the - * local disk and doesn't cache anything. - */ + /** Returns a mock download cache. */ @Override public DownloadCache getDownloadCache() { - if (mMockDownloadCache == null) { - mMockDownloadCache = new DownloadCache(DownloadCache.Strategy.DIRECT) { - @Override - protected File initCacheRoot() { - // returns null, preventing the cache from using the default - // $HOME/.android folder; this effectively disables the cache. - return null; - } - }; - } return mMockDownloadCache; } + /** Overrides the mock download cache. */ + public void setMockDownloadCache(DownloadCache mockDownloadCache) { + mMockDownloadCache = mockDownloadCache; + } + public void overrideSetting(String key, boolean boolValue) { SettingsController sc = getSettingsController(); assert sc instanceof MockSettingsController; ((MockSettingsController)sc).overrideSetting(key, boolValue); } + //------------ public static SettingsController createSettingsController(ILogger sdkLog) { diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/ui/MockPackagesPageImpl.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/ui/MockPackagesPageImpl.java new file mode 100755 index 0000000..9ab36c1 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/ui/MockPackagesPageImpl.java @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2012 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.ui; + +import com.android.sdklib.internal.repository.DownloadCache; +import com.android.sdklib.internal.repository.DownloadCache.Strategy; +import com.android.sdklib.util.SparseIntArray; +import com.android.sdkuilib.internal.repository.MockDownloadCache; +import com.android.sdkuilib.internal.repository.UpdaterData; +import com.android.sdkuilib.internal.repository.core.PackageLoader; +import com.android.sdkuilib.internal.repository.core.PkgCategory; +import com.android.sdkuilib.internal.repository.core.PkgContentProvider; + +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.swt.graphics.Font; + +import java.util.ArrayList; +import java.util.List; + +public class MockPackagesPageImpl extends PackagesPageImpl { + + public MockPackagesPageImpl(UpdaterData updaterData) { + super(updaterData); + } + + /** UI is never disposed in the unit test. */ + @Override + protected boolean isUiDisposed() { + return false; + } + + /** Sync exec always executes immediately in the unit test, no threading is used. */ + @Override + protected void syncExec(Runnable runnable) { + runnable.run(); + } + + private MockTreeViewer mTreeViewer; + + @Override + void postCreate() { + mTreeViewer = new MockTreeViewer(); + setITreeViewer(mTreeViewer); + + setIColumns(new MockTreeColumn(mTreeViewer), // columnName + new MockTreeColumn(mTreeViewer), // columnApi + new MockTreeColumn(mTreeViewer), // columnRevision + new MockTreeColumn(mTreeViewer)); // columnStatus + + super.postCreate(); + } + + @Override + protected void refreshViewerInput() { + super.setViewerInput(); + } + + @Override + protected boolean isSortByApi() { + return true; + } + + @Override + protected Font getTreeFontItalic() { + return null; + } + + @Override + protected void loadPackages(boolean useLocalCache, boolean overrideExisting) { + super.loadPackagesImpl(useLocalCache, overrideExisting); + } + + /** + * In this mock version, we use the default {@link PackageLoader} which will + * use the {@link DownloadCache} from the {@link UpdaterData}. This should be + * the mock download cache, in which case we change the strategy at run-time + * to set it to only-cache on the first manager update. + */ + @Override + protected PackageLoader getPackageLoader(boolean useLocalCache) { + DownloadCache dc = mUpdaterData.getDownloadCache(); + assert dc instanceof MockDownloadCache; + if (dc instanceof MockDownloadCache) { + ((MockDownloadCache) dc).overrideStrategy(useLocalCache ? Strategy.ONLY_CACHE : null); + } + return mUpdaterData.getPackageLoader(); + } + + /** + * Get a dump-out of the tree in a format suitable for unit testing. + */ + public String getMockTreeDisplay() throws Exception { + return mTreeViewer.getTreeDisplay(); + } + + private static class MockTreeViewer implements PackagesPageImpl.ICheckboxTreeViewer { + private final SparseIntArray mWidths = new SparseIntArray(); + private final List<MockTreeColumn> mColumns = new ArrayList<MockTreeColumn>(); + private List<PkgCategory> mInput; + private PkgContentProvider mPkgContentProvider; + private String mLastRefresh; + private static final String SPACE = " "; + + @Override + public void setInput(List<PkgCategory> input) { + mInput = input; + refresh(); + } + + @Override + public Object getInput() { + return mInput; + } + + @Override + public void setContentProvider(PkgContentProvider pkgContentProvider) { + mPkgContentProvider = pkgContentProvider; + } + + @Override + public void refresh() { + // Recompute the display of the tree + StringBuilder sb = new StringBuilder(); + boolean widthChanged = false; + + for (int render = 0; render < (widthChanged ? 2 : 1); render++) { + widthChanged = false; + sb.setLength(0); + for (Object cat : mPkgContentProvider.getElements(mInput)) { + if (cat == null) { + continue; + } + + if (sb.length() > 0) { + sb.append('\n'); + } + + widthChanged |= rowAsString(cat, sb, 3); + + Object[] children = mPkgContentProvider.getElements(cat); + if (children == null) { + continue; + } + for (Object child : children) { + sb.append("\n L_"); + widthChanged |= rowAsString(child, sb, 0); + } + } + } + + mLastRefresh = sb.toString(); + } + + boolean rowAsString(Object element, StringBuilder sb, int space) { + boolean widthChanged = false; + sb.append("[] "); + for (int col = 0; col < mColumns.size(); col++) { + if (col > 0) { + sb.append(" | "); + } + String t = mColumns.get(col).getLabelProvider().getText(element); + if (t == null) { + t = "(null)"; + } + int len = t.length(); + int w = mWidths.get(col); + if (len > w) { + widthChanged = true; + mWidths.put(col, len); + w = len; + } + String pad = len >= w ? "" : SPACE.substring(SPACE.length() - w + len); + if (col == 0 && space > 0) { + sb.append(SPACE.substring(SPACE.length() - space)); + } + if (col >= 1 && col <= 2) { + sb.append(pad); + } + sb.append(t); + if (col == 0 || col > 2) { + sb.append(pad); + } + } + return widthChanged; + } + + @Override + public Object[] getCheckedElements() { + return null; + } + + public void addColumn(MockTreeColumn mockTreeColumn) { + mColumns.add(mockTreeColumn); + } + + public String getTreeDisplay() { + return mLastRefresh; + } + } + + private static class MockTreeColumn implements PackagesPageImpl.ITreeViewerColumn { + private ColumnLabelProvider mLabelProvider; + + public MockTreeColumn(MockTreeViewer treeViewer) { + treeViewer.addColumn(this); + } + + @Override + public void setLabelProvider(ColumnLabelProvider labelProvider) { + mLabelProvider = labelProvider; + } + + public ColumnLabelProvider getLabelProvider() { + return mLabelProvider; + } + } +} diff --git a/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/ui/SdkManagerUpgradeTest.java b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/ui/SdkManagerUpgradeTest.java new file mode 100755 index 0000000..00d9684 --- /dev/null +++ b/sdkmanager/libs/sdkuilib/tests/com/android/sdkuilib/internal/repository/ui/SdkManagerUpgradeTest.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2012 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.ui; + +import com.android.sdklib.SdkManager; +import com.android.sdklib.SdkManagerTestCase; +import com.android.sdklib.repository.SdkRepoConstants; +import com.android.sdkuilib.internal.repository.MockDownloadCache; +import com.android.sdkuilib.internal.repository.MockUpdaterData; + +import java.util.Arrays; + +public class SdkManagerUpgradeTest extends SdkManagerTestCase { + + @Override + public void setUp() throws Exception { + super.setUp(); + } + + @Override + public void tearDown() throws Exception { + super.tearDown(); + } + + /** + * Create a mock page and list the current SDK state + */ + public void testPackagesPage1() throws Exception { + SdkManager sdkman = getSdkManager(); + + MockUpdaterData updaterData = new MockUpdaterData(sdkman); + MockDownloadCache cache = (MockDownloadCache) updaterData.getDownloadCache(); + updaterData.setupDefaultSources(); + + MockPackagesPageImpl pageImpl = new MockPackagesPageImpl(updaterData); + pageImpl.postCreate(); + pageImpl.performFirstLoad(); + + // We have no network access possible and no mock download cache items. + // The only thing visible in the display are the local packages as set by + // the fake locally-installed SDK. + String actual = pageImpl.getMockTreeDisplay(); + assertEquals( + "[] Tools | | | \n" + + " L_[] Android SDK Tools | | 0 | Installed\n" + + "[] Android 0.0 (API 0) | | | \n" + + " L_[] SDK Platform | | 1 | Installed\n" + + " L_[] Sources for Android SDK | | 0 | Installed\n" + + "[] Extras | | | ", + actual); + + assertEquals( + "[]", // there are no direct downloads till we try to install. + Arrays.toString(cache.getDirectHits())); + assertEquals( + "[<https://dl-ssl.google.com/android/repository/addons_list-1.xml : 1>, " + + "<https://dl-ssl.google.com/android/repository/addons_list-2.xml : 1>, " + + "<https://dl-ssl.google.com/android/repository/repository-5.xml : 2>, " + + "<https://dl-ssl.google.com/android/repository/repository-6.xml : 2>, " + + "<https://dl-ssl.google.com/android/repository/repository-7.xml : 2>, " + + "<https://dl-ssl.google.com/android/repository/repository.xml : 2>]", + Arrays.toString(cache.getCachedHits())); + + + // Now prepare a tools update on the server and reload + setupToolsXml1(cache); + cache.clearDirectHits(); + cache.clearCachedHits(); + pageImpl.fullReload(); + + actual = pageImpl.getMockTreeDisplay(); + assertEquals( + "[] Tools | | | \n" + + " L_[] Android SDK Tools | | 0 | Update available: rev. 20.0.3\n" + + " L_[] Android SDK Platform-tools | | 14 | Not installed \n" + + "[] Android 0.0 (API 0) | | | \n" + + " L_[] SDK Platform | | 1 | Installed \n" + + " L_[] Sources for Android SDK | | 0 | Installed \n" + + "[] Extras | | | ", + actual); + + assertEquals( + "[]", // there are no direct downloads till we try to install. + Arrays.toString(cache.getDirectHits())); + assertEquals( + "[<https://dl-ssl.google.com/android/repository/repository-5.xml : 1>, " + + "<https://dl-ssl.google.com/android/repository/repository-6.xml : 1>, " + + "<https://dl-ssl.google.com/android/repository/repository-7.xml : 1>, " + + "<https://dl-ssl.google.com/android/repository/repository.xml : 1>]", + Arrays.toString(cache.getCachedHits())); + + + // We should get the same display if we restart the manager page from scratch + // (e.g. simulate a first load) + + cache.clearDirectHits(); + cache.clearCachedHits(); + pageImpl = new MockPackagesPageImpl(updaterData); + pageImpl.postCreate(); + pageImpl.performFirstLoad(); + + actual = pageImpl.getMockTreeDisplay(); + assertEquals( + "[] Tools | | | \n" + + " L_[] Android SDK Tools | | 0 | Update available: rev. 20.0.3\n" + + " L_[] Android SDK Platform-tools | | 14 | Not installed \n" + + "[] Android 0.0 (API 0) | | | \n" + + " L_[] SDK Platform | | 1 | Installed \n" + + " L_[] Sources for Android SDK | | 0 | Installed \n" + + "[] Extras | | | ", + actual); + + assertEquals( + "[]", // there are no direct downloads till we try to install. + Arrays.toString(cache.getDirectHits())); + assertEquals( + "[<https://dl-ssl.google.com/android/repository/repository-5.xml : 1>, " + + "<https://dl-ssl.google.com/android/repository/repository-6.xml : 1>, " + + "<https://dl-ssl.google.com/android/repository/repository-7.xml : 1>, " + + "<https://dl-ssl.google.com/android/repository/repository.xml : 1>]", + Arrays.toString(cache.getCachedHits())); + } + + private void setupToolsXml1(MockDownloadCache cache) throws Exception { + String repoXml = + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + + "<sdk:sdk-repository xmlns:sdk=\"http://schemas.android.com/sdk/android/repository/7\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" + + "<sdk:license id=\"android-sdk-license\" type=\"text\">Blah blah blah.</sdk:license>\n" + + "\n" + + "<sdk:platform-tool>\n" + + " <sdk:revision>\n" + + " <sdk:major>14</sdk:major>\n" + + " </sdk:revision>\n" + + " <sdk:archives>\n" + + " <sdk:archive arch=\"any\" os=\"windows\">\n" + + " <sdk:size>11159472</sdk:size>\n" + + " <sdk:checksum type=\"sha1\">6028258d8f2fba14d8b40c3cf507afa0289aaa13</sdk:checksum>\n" + + " <sdk:url>platform-tools_r14-windows.zip</sdk:url>\n" + + " </sdk:archive>\n" + + " <sdk:archive arch=\"any\" os=\"linux\">\n" + + " <sdk:size>10985068</sdk:size>\n" + + " <sdk:checksum type=\"sha1\">6e2bc329c9485eb383172cbc2cde8b0c0cd1843f</sdk:checksum>\n" + + " <sdk:url>platform-tools_r14-linux.zip</sdk:url>\n" + + " </sdk:archive>\n" + + " <sdk:archive arch=\"any\" os=\"macosx\">\n" + + " <sdk:size>11342461</sdk:size>\n" + + " <sdk:checksum type=\"sha1\">4a015090c6a209fc33972acdbc65745e0b3c08b9</sdk:checksum>\n" + + " <sdk:url>platform-tools_r14-macosx.zip</sdk:url>\n" + + " </sdk:archive>\n" + + " </sdk:archives>\n" + + "</sdk:platform-tool>\n" + + "\n" + + "<sdk:tool>\n" + + " <sdk:revision>\n" + + " <sdk:major>20</sdk:major>\n" + + " <sdk:minor>0</sdk:minor>\n" + + " <sdk:micro>3</sdk:micro>\n" + + " </sdk:revision>\n" + + " <sdk:min-platform-tools-rev>\n" + + " <sdk:major>12</sdk:major>\n" + + " </sdk:min-platform-tools-rev>\n" + + " <sdk:archives>\n" + + " <sdk:archive arch=\"any\" os=\"windows\">\n" + + " <sdk:size>90272048</sdk:size>\n" + + " <sdk:checksum type=\"sha1\">54fb94168e631e211910f88aa40c532205730dd4</sdk:checksum>\n" + + " <sdk:url>tools_r20.0.3-windows.zip</sdk:url>\n" + + " </sdk:archive>\n" + + " <sdk:archive arch=\"any\" os=\"linux\">\n" + + " <sdk:size>82723559</sdk:size>\n" + + " <sdk:checksum type=\"sha1\">09bc633b406ae81981e3a0db19426acbb01ef219</sdk:checksum>\n" + + " <sdk:url>tools_r20.0.3-linux.zip</sdk:url>\n" + + " </sdk:archive>\n" + + " <sdk:archive arch=\"any\" os=\"macosx\">\n" + + " <sdk:size>58197071</sdk:size>\n" + + " <sdk:checksum type=\"sha1\">09cee5ff3226277a6f0c07dcd29cba4ffc2e1da4</sdk:checksum>\n" + + " <sdk:url>tools_r20.0.3-macosx.zip</sdk:url>\n" + + " </sdk:archive>\n" + + " </sdk:archives>\n" + + "</sdk:tool>\n" + + "\n" + + "</sdk:sdk-repository>\n"; + + String url = SdkRepoConstants.URL_GOOGLE_SDK_SITE + + String.format(SdkRepoConstants.URL_DEFAULT_FILENAME, SdkRepoConstants.NS_LATEST_VERSION); + + cache.registerCachedPayload(url, repoXml.getBytes("UTF-8")); + } + +} |