/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.sdkuilib.internal.repository;
import com.android.sdklib.internal.repository.Archive;
import com.android.sdklib.internal.repository.IDescription;
import com.android.sdklib.internal.repository.Package;
import com.android.sdklib.internal.repository.SdkAddonSource;
import com.android.sdklib.internal.repository.SdkSource;
import com.android.sdklib.internal.repository.SdkSourceCategory;
import com.android.sdkuilib.repository.ISdkChangeListener;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.ITreeSelection;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import java.util.ArrayList;
public class RemotePackagesPage extends Composite implements ISdkChangeListener {
private final UpdaterData mUpdaterData;
private CheckboxTreeViewer mTreeViewerSources;
private Tree mTreeSources;
private TreeColumn mColumnSource;
private Button mUpdateOnlyCheckBox;
private Group mDescriptionContainer;
private Button mAddSiteButton;
private Button mDeleteSiteButton;
private Button mRefreshButton;
private Button mInstallSelectedButton;
private Label mDescriptionLabel;
private Label mSdkLocLabel;
/**
* Create the composite.
* @param parent The parent of the composite.
* @param updaterData An instance of {@link UpdaterData}.
*/
RemotePackagesPage(Composite parent, UpdaterData updaterData) {
super(parent, SWT.BORDER);
mUpdaterData = updaterData;
createContents(this);
postCreate(); //$hide$
}
private void createContents(Composite parent) {
parent.setLayout(new GridLayout(5, false));
mSdkLocLabel = new Label(parent, SWT.NONE);
mSdkLocLabel.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, true, false, 5, 1));
mSdkLocLabel.setText("SDK Location: " +
(mUpdaterData.getOsSdkRoot() != null ? mUpdaterData.getOsSdkRoot()
: "<unknown>"));
mTreeViewerSources = new CheckboxTreeViewer(parent, SWT.BORDER);
mTreeViewerSources.addCheckStateListener(new ICheckStateListener() {
public void checkStateChanged(CheckStateChangedEvent event) {
onTreeCheckStateChanged(event); //$hide$
}
});
mTreeSources = mTreeViewerSources.getTree();
mTreeSources.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onTreeSelected(); //$hide$
}
});
mTreeSources.setHeaderVisible(true);
mTreeSources.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 5, 1));
mColumnSource = new TreeColumn(mTreeSources, SWT.NONE);
mColumnSource.setWidth(289);
mColumnSource.setText("Packages available for download");
mDescriptionContainer = new Group(parent, SWT.NONE);
mDescriptionContainer.setLayout(new GridLayout(1, false));
mDescriptionContainer.setText("Description");
mDescriptionContainer.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false, 5, 1));
mDescriptionLabel = new Label(mDescriptionContainer, SWT.NONE);
mDescriptionLabel.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
mDescriptionLabel.setText("Line1\nLine2\nLine3"); //$NON-NLS-1$
mAddSiteButton = new Button(parent, SWT.NONE);
mAddSiteButton.setText("Add Add-on Site...");
mAddSiteButton.setToolTipText("Allows you to enter a new add-on site. " +
"Such site can only contribute add-ons and extra packages.");
mAddSiteButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onAddSiteSelected(); //$hide$
}
});
mDeleteSiteButton = new Button(parent, SWT.NONE);
mDeleteSiteButton.setText("Delete Add-on Site...");
mDeleteSiteButton.setToolTipText("Allows you to remove an add-on site. " +
"Built-in default sites cannot be removed.");
mDeleteSiteButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onRemoveSiteSelected(); //$hide$
}
});
mUpdateOnlyCheckBox = new Button(parent, SWT.CHECK);
mUpdateOnlyCheckBox.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 1, 1));
mUpdateOnlyCheckBox.setText("Display updates only");
mUpdateOnlyCheckBox.setToolTipText("When selected, only compatible non-obsolete update packages are shown in the list above.");
mUpdateOnlyCheckBox.setSelection(mUpdaterData.getSettingsController().getShowUpdateOnly());
mUpdateOnlyCheckBox.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent arg0) {
onShowUpdateOnly(); //$hide$
}
});
mRefreshButton = new Button(parent, SWT.NONE);
mRefreshButton.setText("Refresh");
mRefreshButton.setToolTipText("Refreshes the list of packages from open sites.");
mRefreshButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onRefreshSelected(); //$hide$
}
});
mInstallSelectedButton = new Button(parent, SWT.NONE);
mInstallSelectedButton.setText("Install Selected");
mInstallSelectedButton.setToolTipText("Allows you to review all selected packages " +
"and install them.");
mInstallSelectedButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
onInstallSelectedArchives(); //$hide$
}
});
}
@Override
public void dispose() {
mUpdaterData.removeListener(this);
super.dispose();
}
@Override
protected void checkSubclass() {
// Disable the check that prevents subclassing of SWT components
}
// -- Start of internal part ----------
// Hide everything down-below from SWT designer
//$hide>>$
/**
* Called by the constructor right after {@link #createContents(Composite)}.
*/
private void postCreate() {
mUpdaterData.addListeners(this);
adjustColumnsWidth();
updateButtonsState();
}
/**
* Adds a listener to adjust the columns width when the parent is resized.
* <p/>
* If we need something more fancy, we might want to use this:
* http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co
*/
private void adjustColumnsWidth() {
// Add a listener to resize the column to the full width of the table
ControlAdapter resizer = new ControlAdapter() {
@Override
public void controlResized(ControlEvent e) {
Rectangle r = mTreeSources.getClientArea();
mColumnSource.setWidth(r.width);
}
};
mTreeSources.addControlListener(resizer);
resizer.controlResized(null);
}
/**
* Called when an item in the package table viewer is selected.
* If the items is an {@link IDescription} (as it should), this will display its long
* description in the description area. Otherwise when the item is not of the expected
* type or there is no selection, it empties the description area.
*/
private void onTreeSelected() {
updateButtonsState();
ISelection sel = mTreeViewerSources.getSelection();
if (sel instanceof ITreeSelection) {
Object elem = ((ITreeSelection) sel).getFirstElement();
if (elem instanceof IDescription) {
mDescriptionLabel.setText(((IDescription) elem).getLongDescription());
mDescriptionContainer.layout(true);
return;
}
}
mDescriptionLabel.setText(""); //$NON-NLS1-$
}
/**
* Handle checking and unchecking of the tree items.
*
* When unchecking, all sub-tree items checkboxes are cleared too.
* When checking a source, all of its packages are checked too.
* When checking a package, only its compatible archives are checked.
*/
private void onTreeCheckStateChanged(CheckStateChangedEvent event) {
updateButtonsState();
boolean b = event.getChecked();
Object elem = event.getElement(); // Will be Archive or Package or RepoSource
assert event.getSource() == mTreeViewerSources;
// when deselecting, we just deselect all children too
if (b == false) {
mTreeViewerSources.setSubtreeChecked(elem, b);
return;
}
ITreeContentProvider provider =
(ITreeContentProvider) mTreeViewerSources.getContentProvider();
// When selecting, we want to only select compatible archives
// and expand the super nodes.
expandItem(elem, provider);
}
private void expandItem(Object elem, ITreeContentProvider provider) {
if (elem instanceof SdkSource || elem instanceof SdkSourceCategory) {
mTreeViewerSources.setExpandedState(elem, true);
for (Object pkg : provider.getChildren(elem)) {
mTreeViewerSources.setChecked(pkg, true);
expandItem(pkg, provider);
}
} else if (elem instanceof Package) {
selectCompatibleArchives(elem, provider);
}
}
private void selectCompatibleArchives(Object pkg, ITreeContentProvider provider) {
for (Object archive : provider.getChildren(pkg)) {
if (archive instanceof Archive) {
mTreeViewerSources.setChecked(archive, ((Archive) archive).isCompatible());
}
}
}
private void onShowUpdateOnly() {
SettingsController controller = mUpdaterData.getSettingsController();
controller.setShowUpdateOnly(mUpdateOnlyCheckBox.getSelection());
controller.saveSettings();
// Get the list of selected archives
ArrayList<Archive> archives = new ArrayList<Archive>();
for (Object element : mTreeViewerSources.getCheckedElements()) {
if (element instanceof Archive) {
archives.add((Archive) element);
}
// Deselect them all
mTreeViewerSources.setChecked(element, false);
}
mTreeViewerSources.refresh();
// Now reselect those that still exist in the tree but only if they
// are compatible archives
for (Archive a : archives) {
if (a.isCompatible() && mTreeViewerSources.setChecked(a, true)) {
// If we managed to select the archive, also select the parent package.
// Technically we should only select the parent package if *all* the
// compatible archives children are selected. In practice we'll rarely
// have more than one compatible archive per package.
mTreeViewerSources.setChecked(a.getParentPackage(), true);
}
}
updateButtonsState();
}
private void onInstallSelectedArchives() {
ArrayList<Archive> archives = new ArrayList<Archive>();
for (Object element : mTreeViewerSources.getCheckedElements()) {
if (element instanceof Archive) {
archives.add((Archive) element);
}
}
if (mUpdaterData != null) {
mUpdaterData.updateOrInstallAll_WithGUI(
archives,
mUpdateOnlyCheckBox.getSelection() /* includeObsoletes */);
}
}
private void onAddSiteSelected() {
final SdkSource[] knowSources = mUpdaterData.getSources().getAllSources();
String title = "Add Add-on Site URL";
String msg =
"This dialog lets you add the URL of a new add-on site.\n" +
"\n" +
"An add-on site can only provide new add-ons or \"user\" packages.\n" +
"Add-on sites cannot provide standard Android platforms, docs or samples packages.\n" +
"Inserting a URL here will not allow you to clone an official Android repository.\n" +
"\n" +
"Please enter the URL of the repository.xml for the new add-on site:";
InputDialog dlg = new InputDialog(getShell(), title, msg, null, new IInputValidator() {
public String isValid(String newText) {
newText = newText == null ? null : newText.trim();
if (newText == null || newText.length() == 0) {
return "Error: URL field is empty. Please enter a URL.";
}
// A URL should have one of the following prefixes
if (!newText.startsWith("file://") && //$NON-NLS-1$
!newText.startsWith("ftp://") && //$NON-NLS-1$
!newText.startsWith("http://") && //$NON-NLS-1$
!newText.startsWith("https://")) { //$NON-NLS-1$
return "Error: The URL must start by one of file://, ftp://, http:// or https://";
}
// Reject URLs that are already in the source list.
// URLs are generally case-insensitive (except for file:// where it all depends
// on the current OS so we'll ignore this case.)
for (SdkSource s : knowSources) {
if (newText.equalsIgnoreCase(s.getUrl())) {
return "Error : This site is already listed.";
}
}
return null;
}
});
if (dlg.open() == Window.OK) {
String url = dlg.getValue().trim();
mUpdaterData.getSources().add(
SdkSourceCategory.USER_ADDONS,
new SdkAddonSource(url, null/*uiName*/));
onRefreshSelected();
}
}
private void onRemoveSiteSelected() {
boolean changed = false;
ISelection sel = mTreeViewerSources.getSelection();
if (mUpdaterData != null && sel instanceof ITreeSelection) {
for (Object c : ((ITreeSelection) sel).toList()) {
if (c instanceof SdkSource) {
SdkSource source = (SdkSource) c;
if (mUpdaterData.getSources().hasSourceUrl(
SdkSourceCategory.USER_ADDONS, source)) {
String title = "Delete Add-on Site?";
String msg = String.format("Are you sure you want to delete the add-on site '%1$s'?",
source.getUrl());
if (MessageDialog.openQuestion(getShell(), title, msg)) {
mUpdaterData.getSources().remove(source);
changed = true;
}
}
}
}
}
if (changed) {
onRefreshSelected();
}
}
private void onRefreshSelected() {
if (mUpdaterData != null) {
mUpdaterData.refreshSources(false /*forceFetching*/);
}
mTreeViewerSources.refresh();
updateButtonsState();
}
private void updateButtonsState() {
// We install archives, so there should be at least one checked archive.
// Having sites or packages checked does not count.
boolean hasCheckedArchive = false;
Object[] checked = mTreeViewerSources.getCheckedElements();
if (checked != null) {
for (Object c : checked) {
if (c instanceof Archive) {
hasCheckedArchive = true;
break;
}
}
}
// Is there a selected site Source?
boolean hasSelectedUserSource = false;
ISelection sel = mTreeViewerSources.getSelection();
if (sel instanceof ITreeSelection) {
for (Object c : ((ITreeSelection) sel).toList()) {
if (c instanceof SdkSource &&
mUpdaterData.getSources().hasSourceUrl(
SdkSourceCategory.USER_ADDONS, (SdkSource) c)) {
hasSelectedUserSource = true;
break;
}
}
}
mAddSiteButton.setEnabled(true);
mDeleteSiteButton.setEnabled(hasSelectedUserSource);
mRefreshButton.setEnabled(true);
mInstallSelectedButton.setEnabled(hasCheckedArchive);
// set value on the show only update checkbox
mUpdateOnlyCheckBox.setSelection(mUpdaterData.getSettingsController().getShowUpdateOnly());
}
// --- Implementation of ISdkChangeListener ---
public void onSdkLoaded() {
onSdkReload();
}
public void onSdkReload() {
RepoSourcesAdapter sources = mUpdaterData.getSourcesAdapter();
mTreeViewerSources.setContentProvider(sources.getContentProvider());
mTreeViewerSources.setLabelProvider( sources.getLabelProvider());
mTreeViewerSources.setInput(sources);
onTreeSelected();
}
public void preInstallHook() {
// nothing to be done for now.
}
public void postInstallHook() {
// nothing to be done for now.
}
// End of hiding from SWT Designer
//$hide<<$
}