[go: nahoru, domu]

blob: 0ab8f01b859b85a8190fada1410a197fc28d1629 [file] [log] [blame]
/*
* 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.AndroidVersion;
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.sdklib.internal.repository.updater.PackageLoader;
import com.android.sdklib.internal.repository.updater.PackageLoader.ISourceLoadedCallback;
import com.android.sdklib.internal.repository.updater.PkgItem;
import com.android.sdklib.internal.repository.updater.PkgItem.PkgState;
import com.android.sdkuilib.internal.repository.SwtUpdaterData;
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.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 SwtUpdaterData mSwtUpdaterData;
final PackagesDiffLogic mDiffLogic;
private ICheckboxTreeViewer mITreeViewer;
private ITreeViewerColumn mIColumnName;
private ITreeViewerColumn mIColumnApi;
private ITreeViewerColumn mIColumnRevision;
private ITreeViewerColumn mIColumnStatus;
PackagesPageImpl(SwtUpdaterData swtUpdaterData) {
mSwtUpdaterData = swtUpdaterData;
mDiffLogic = new PackagesDiffLogic(swtUpdaterData);
}
/**
* 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);
/**
* Synchronizes the 'checked' state of PkgItems in the tree with their internal isChecked state.
*/
abstract protected void syncViewerSelection();
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.
mSwtUpdaterData.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
mSwtUpdaterData.getLocalSdkParser().clearPackages();
mSwtUpdaterData.getSdkManager().reloadSdk(mSwtUpdaterData.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 (mSwtUpdaterData == null) {
return;
}
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(source, newPackages);
syncExec(new Runnable() {
@Override
public void run() {
if (changed ||
mITreeViewer.getInput() != mDiffLogic.getCategories()) {
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();
syncExec(new Runnable() {
@Override
public void run() {
if (changed ||
mITreeViewer.getInput() != mDiffLogic.getCategories()) {
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 SwtUpdaterData}.
*
* This is extracted so that we can control this cache via unit tests.
*/
protected PackageLoader getPackageLoader(boolean useLocalCache) {
if (useLocalCache) {
return new PackageLoader(mSwtUpdaterData, new DownloadCache(Strategy.ONLY_CACHE));
} else {
return mSwtUpdaterData.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();
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();
}
}
/**
* 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);
syncViewerSelection();
}
/**
* 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) {
AndroidVersion version = null;
if (element instanceof PkgItem) {
version = ((PkgItem) element).getAndroidVersion();
}
if (version != null) {
return version.getApiString();
}
} 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();
// 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();
for (PkgCategory cat : cats) {
for (PkgItem i : cat.getItems()) {
if (i == item) {
return cat;
}
}
}
return null;
}
@Override
public Image getImage(Object element) {
ImageFactory imgFactory = mSwtUpdaterData.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);
}
}