Java tutorial
/* * 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.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.sdklib.AndroidVersion; import com.android.sdklib.internal.repository.archives.Archive; import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider; import com.android.sdklib.internal.repository.packages.Package; import com.android.sdklib.internal.repository.sources.SdkSource; import com.android.sdklib.internal.repository.updater.ArchiveInfo; import com.android.sdklib.internal.repository.updater.SdkUpdaterLogic; import com.android.sdklib.repository.FullRevision; import com.android.sdklib.repository.License; import com.android.sdkuilib.internal.repository.icons.ImageFactory; import com.android.sdkuilib.ui.GridDialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.custom.StyledText; 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.Image; import org.eclipse.swt.graphics.Point; 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.Control; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TreeMap; /** * Implements an {@link SdkUpdaterChooserDialog}. */ final class SdkUpdaterChooserDialog extends GridDialog { /** Last dialog size for this session. */ private static Point sLastSize; /** Precomputed flag indicating whether the "accept license" radio is checked. */ private boolean mAcceptSameAllLicense; private boolean mInternalLicenseRadioUpdate; // UI fields private SashForm mSashForm; private Composite mPackageRootComposite; private TreeViewer mTreeViewPackage; private Tree mTreePackage; private TreeColumn mTreeColum; private StyledText mPackageText; private Button mLicenseRadioAccept; private Button mLicenseRadioReject; private Button mLicenseRadioAcceptLicense; private Group mPackageTextGroup; private final SwtUpdaterData mSwtUpdaterData; private Group mTableGroup; private Label mErrorLabel; /** * List of all archives to be installed with dependency information. * <p/> * Note: in a lot of cases, we need to find the archive info for a given archive. This * is currently done using a simple linear search, which is fine since we only have a very * limited number of archives to deal with (e.g. < 10 now). We might want to revisit * this later if it becomes an issue. Right now just do the simple thing. * <p/> * Typically we could add a map Archive=>ArchiveInfo later. */ private final Collection<ArchiveInfo> mArchives; /** * Create the dialog. * * @param parentShell The shell to use, typically updaterData.getWindowShell() * @param swtUpdaterData The updater data * @param archives The archives to be installed */ public SdkUpdaterChooserDialog(Shell parentShell, SwtUpdaterData swtUpdaterData, Collection<ArchiveInfo> archives) { super(parentShell, 3, false/*makeColumnsEqual*/); mSwtUpdaterData = swtUpdaterData; mArchives = archives; } @Override protected boolean isResizable() { return true; } /** * Returns the results, i.e. the list of selected new archives to install. * This is similar to the {@link ArchiveInfo} list instance given to the constructor * except only accepted archives are present. * <p/> * An empty list is returned if cancel was chosen. */ public ArrayList<ArchiveInfo> getResult() { ArrayList<ArchiveInfo> ais = new ArrayList<ArchiveInfo>(); if (getReturnCode() == Window.OK) { for (ArchiveInfo ai : mArchives) { if (ai.isAccepted()) { ais.add(ai); } } } return ais; } /** * Create the main content of the dialog. * See also {@link #createButtonBar(Composite)} below. */ @Override public void createDialogContent(Composite parent) { // Sash form mSashForm = new SashForm(parent, SWT.NONE); mSashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1)); // Left part of Sash Form mTableGroup = new Group(mSashForm, SWT.NONE); mTableGroup.setText("Packages"); mTableGroup.setLayout(new GridLayout(1, false/*makeColumnsEqual*/)); mTreeViewPackage = new TreeViewer(mTableGroup, SWT.BORDER | SWT.V_SCROLL | SWT.SINGLE); mTreePackage = mTreeViewPackage.getTree(); mTreePackage.setHeaderVisible(false); mTreePackage.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); mTreePackage.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { onPackageSelected(); //$hide$ } @Override public void widgetDefaultSelected(SelectionEvent e) { onPackageDoubleClick(); } }); mTreeColum = new TreeColumn(mTreePackage, SWT.NONE); mTreeColum.setWidth(100); mTreeColum.setText("Packages"); // Right part of Sash form mPackageRootComposite = new Composite(mSashForm, SWT.NONE); mPackageRootComposite.setLayout(new GridLayout(4, false/*makeColumnsEqual*/)); mPackageRootComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); mPackageTextGroup = new Group(mPackageRootComposite, SWT.NONE); mPackageTextGroup.setText("Package Description && License"); mPackageTextGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 4, 1)); mPackageTextGroup.setLayout(new GridLayout(1, false/*makeColumnsEqual*/)); mPackageText = new StyledText(mPackageTextGroup, SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL); mPackageText.setBackground(getParentShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND)); mPackageText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); mLicenseRadioAccept = new Button(mPackageRootComposite, SWT.RADIO); mLicenseRadioAccept.setText("Accept"); mLicenseRadioAccept.setToolTipText("Accept this package."); mLicenseRadioAccept.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { onLicenseRadioSelected(); } }); mLicenseRadioReject = new Button(mPackageRootComposite, SWT.RADIO); mLicenseRadioReject.setText("Reject"); mLicenseRadioReject.setToolTipText("Reject this package."); mLicenseRadioReject.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { onLicenseRadioSelected(); } }); Link link = new Link(mPackageRootComposite, SWT.NONE); link.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 1, 1)); final String printAction = "Print"; // extracted for NLS, to compare with below. link.setText(String.format("<a>Copy to clipboard</a> | <a>%1$s</a>", printAction)); link.setToolTipText("Copies all text and license to clipboard | Print using system defaults."); link.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { super.widgetSelected(e); if (printAction.equals(e.text)) { mPackageText.print(); } else { Point p = mPackageText.getSelection(); mPackageText.selectAll(); mPackageText.copy(); mPackageText.setSelection(p); } } }); mLicenseRadioAcceptLicense = new Button(mPackageRootComposite, SWT.RADIO); mLicenseRadioAcceptLicense.setText("Accept License"); mLicenseRadioAcceptLicense.setToolTipText("Accept all packages that use the same license."); mLicenseRadioAcceptLicense.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { onLicenseRadioSelected(); } }); mSashForm.setWeights(new int[] { 200, 300 }); } /** * Creates and returns the contents of this dialog's button bar. * <p/> * This reimplements most of the code from the base class with a few exceptions: * <ul> * <li>Enforces 3 columns. * <li>Inserts a full-width error label. * <li>Inserts a help label on the left of the first button. * <li>Renames the OK button into "Install" * </ul> */ @Override protected Control createButtonBar(Composite parent) { Composite composite = new Composite(parent, SWT.NONE); GridLayout layout = new GridLayout(); layout.numColumns = 0; // this is incremented by createButton layout.makeColumnsEqualWidth = false; layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN); layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN); layout.horizontalSpacing = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_SPACING); layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING); composite.setLayout(layout); GridData data = new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1); composite.setLayoutData(data); composite.setFont(parent.getFont()); // Error message area mErrorLabel = new Label(composite, SWT.NONE); mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1)); // Label at the left of the install/cancel buttons Label label = new Label(composite, SWT.NONE); label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); label.setText("[*] Something depends on this package"); label.setEnabled(false); layout.numColumns++; // Add the ok/cancel to the button bar. createButtonsForButtonBar(composite); // the ok button should be an "install" button Button button = getButton(IDialogConstants.OK_ID); button.setText("Install"); return composite; } // -- End of UI, Start of internal logic ---------- // Hide everything down-below from SWT designer //$hide>>$ @Override public void create() { super.create(); // set window title getShell().setText("Choose Packages to Install"); setWindowImage(); // Automatically accept those with an empty license or no license for (ArchiveInfo ai : mArchives) { Archive a = ai.getNewArchive(); if (a != null) { License license = a.getParentPackage().getLicense(); boolean hasLicense = license != null && license.getLicense() != null && license.getLicense().length() > 0; ai.setAccepted(!hasLicense); } } // Fill the list with the replacement packages mTreeViewPackage.setLabelProvider(new NewArchivesLabelProvider()); mTreeViewPackage.setContentProvider(new NewArchivesContentProvider()); mTreeViewPackage.setInput(createTreeInput(mArchives)); mTreeViewPackage.expandAll(); adjustColumnsWidth(); // select first item onPackageSelected(); } /** * Creates the icon of the window shell. */ private void setWindowImage() { String imageName = "android_icon_16.png"; //$NON-NLS-1$ if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) { imageName = "android_icon_128.png"; //$NON-NLS-1$ } if (mSwtUpdaterData != null) { ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); if (imgFactory != null) { getShell().setImage(imgFactory.getImageByName(imageName)); } } } /** * 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 = mTreePackage.getClientArea(); mTreeColum.setWidth(r.width); } }; mTreePackage.addControlListener(resizer); resizer.controlResized(null); } /** * Captures the window size before closing this. * @see #getInitialSize() */ @Override public boolean close() { sLastSize = getShell().getSize(); return super.close(); } /** * Tries to reuse the last window size during this session. * <p/> * Note: the alternative would be to implement {@link #getDialogBoundsSettings()} * since the default {@link #getDialogBoundsStrategy()} is to persist both location * and size. */ @Override protected Point getInitialSize() { if (sLastSize != null) { return sLastSize; } else { // Arbitrary values that look good on my screen and fit on 800x600 return new Point(740, 470); } } /** * Callback invoked when a package item is selected in the list. */ private void onPackageSelected() { Object item = getSelectedItem(); // Update mAcceptSameAllLicense : true if all items under the same license are accepted. ArchiveInfo ai = null; List<ArchiveInfo> list = null; if (item instanceof ArchiveInfo) { ai = (ArchiveInfo) item; Object p = ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()).getParent(ai); if (p instanceof LicenseEntry) { list = ((LicenseEntry) p).getArchives(); } displayPackageInformation(ai); } else if (item instanceof LicenseEntry) { LicenseEntry entry = (LicenseEntry) item; list = entry.getArchives(); displayLicenseInformation(entry); } else { // Fallback, should not happen. displayEmptyInformation(); } // the "Accept License" radio is selected if there's a license with >= 0 items // and they are all in "accepted" state. mAcceptSameAllLicense = list != null && list.size() > 0; if (mAcceptSameAllLicense) { assert list != null; License lic0 = getLicense(list.get(0)); for (ArchiveInfo ai2 : list) { License lic2 = getLicense(ai2); if (ai2.isAccepted() && (lic0 == lic2 || lic0.equals(lic2))) { continue; } else { mAcceptSameAllLicense = false; break; } } } displayMissingDependency(ai); updateLicenceRadios(ai); } /** Returns the currently selected tree item. * @return Either {@link ArchiveInfo} or {@link LicenseEntry} or null. */ private Object getSelectedItem() { ISelection sel = mTreeViewPackage.getSelection(); if (sel instanceof IStructuredSelection) { Object elem = ((IStructuredSelection) sel).getFirstElement(); if (elem instanceof ArchiveInfo || elem instanceof LicenseEntry) { return elem; } } return null; } /** * Information displayed when nothing valid is selected. */ private void displayEmptyInformation() { mPackageText.setText("Please select a package or a license."); } /** * Updates the package description and license text depending on the selected package. * <p/> * Note that right now there is no logic to support more than one level of dependencies * (e.g. A <- B <- C and A is disabled so C should be disabled; currently C's state depends * solely on B's state). We currently don't need this. It would be straightforward to add * if we had a need for it, though. This would require changes to {@link ArchiveInfo} and * {@link SdkUpdaterLogic}. */ private void displayPackageInformation(ArchiveInfo ai) { Archive aNew = ai == null ? null : ai.getNewArchive(); Package pNew = aNew == null ? null : aNew.getParentPackage(); if (pNew == null) { displayEmptyInformation(); return; } assert ai != null; // make Eclipse null detector happy assert aNew != null; mPackageText.setText(""); //$NON-NLS-1$ addSectionTitle("Package Description\n"); addText(pNew.getLongDescription(), "\n\n"); //$NON-NLS-1$ Archive aOld = ai.getReplaced(); if (aOld != null) { Package pOld = aOld.getParentPackage(); FullRevision rOld = pOld.getRevision(); FullRevision rNew = pNew.getRevision(); boolean showRev = true; if (pNew instanceof IAndroidVersionProvider && pOld instanceof IAndroidVersionProvider) { AndroidVersion vOld = ((IAndroidVersionProvider) pOld).getAndroidVersion(); AndroidVersion vNew = ((IAndroidVersionProvider) pNew).getAndroidVersion(); if (!vOld.equals(vNew)) { // Versions are different, so indicate more than just the revision. addText(String.format( "This update will replace API %1$s revision %2$s with API %3$s revision %4$s.\n\n", vOld.getApiString(), rOld.toShortString(), vNew.getApiString(), rNew.toShortString())); showRev = false; } } if (showRev) { addText(String.format("This update will replace revision %1$s with revision %2$s.\n\n", rOld.toShortString(), rNew.toShortString())); } } ArchiveInfo[] aDeps = ai.getDependsOn(); if ((aDeps != null && aDeps.length > 0) || ai.isDependencyFor()) { addSectionTitle("Dependencies\n"); if (aDeps != null && aDeps.length > 0) { addText("Installing this package also requires installing:"); for (ArchiveInfo aDep : aDeps) { addText(String.format("\n- %1$s", aDep.getShortDescription())); } addText("\n\n"); } if (ai.isDependencyFor()) { addText("This package is a dependency for:"); for (ArchiveInfo ai2 : ai.getDependenciesFor()) { addText(String.format("\n- %1$s", ai2.getShortDescription())); } addText("\n\n"); } } addSectionTitle("Archive Description\n"); addText(aNew.getLongDescription(), "\n\n"); //$NON-NLS-1$ License license = pNew.getLicense(); if (license != null) { String text = license.getLicense(); if (text != null) { addSectionTitle("License\n"); addText(text.trim(), "\n\n"); //$NON-NLS-1$ } } addSectionTitle("Site\n"); SdkSource source = pNew.getParentSource(); if (source != null) { addText(source.getShortDescription()); } } /** * Updates the description for a license entry. */ private void displayLicenseInformation(LicenseEntry entry) { List<ArchiveInfo> archives = entry == null ? null : entry.getArchives(); if (archives == null) { // There should not be a license entry without any package in it. displayEmptyInformation(); return; } assert entry != null; mPackageText.setText(""); //$NON-NLS-1$ License license = null; addSectionTitle("Packages\n"); for (ArchiveInfo ai : entry.getArchives()) { Archive aNew = ai.getNewArchive(); if (aNew != null) { Package pNew = aNew.getParentPackage(); if (pNew != null) { if (license == null) { license = pNew.getLicense(); } else { assert license.equals(pNew.getLicense()); // all items have the same license } addText("- ", pNew.getShortDescription(), "\n"); //$NON-NLS-1$ //$NON-NLS-2$ } } } if (license != null) { String text = license.getLicense(); if (text != null) { addSectionTitle("\nLicense\n"); addText(text.trim(), "\n\n"); //$NON-NLS-1$ } } } /** * Computes and displays missing dependencies. * * If there's a selected package, check the dependency for that one. * Otherwise display the first missing dependency of any other package. */ private void displayMissingDependency(ArchiveInfo ai) { String error = null; try { if (ai != null) { if (ai.isAccepted()) { // Case where this package is accepted but blocked by another non-accepted one ArchiveInfo[] adeps = ai.getDependsOn(); if (adeps != null) { for (ArchiveInfo adep : adeps) { if (!adep.isAccepted()) { error = String.format("This package depends on '%1$s'.", adep.getShortDescription()); return; } } } } else { // Case where this package blocks another one when not accepted for (ArchiveInfo adep : ai.getDependenciesFor()) { // It only matters if the blocked one is accepted if (adep.isAccepted()) { error = String.format("Package '%1$s' depends on this one.", adep.getShortDescription()); return; } } } } // If there is no missing dependency on the current selection, // just find the first missing dependency of any other package. for (ArchiveInfo ai2 : mArchives) { if (ai2 == ai) { // We already processed that one above. continue; } if (ai2.isAccepted()) { // The user requested to install this package. // Check if all its dependencies are met. ArchiveInfo[] adeps = ai2.getDependsOn(); if (adeps != null) { for (ArchiveInfo adep : adeps) { if (!adep.isAccepted()) { error = String.format("Package '%1$s' depends on '%2$s'", ai2.getShortDescription(), adep.getShortDescription()); return; } } } } else { // The user did not request to install this package. // Check whether this package blocks another one when not accepted. for (ArchiveInfo adep : ai2.getDependenciesFor()) { // It only matters if the blocked one is accepted // or if it's a local archive that is already installed (these // are marked as implicitly accepted, so it's the same test.) if (adep.isAccepted()) { error = String.format("Package '%1$s' depends on '%2$s'", adep.getShortDescription(), ai2.getShortDescription()); return; } } } } } finally { mErrorLabel.setText(error == null ? "" : error); //$NON-NLS-1$ } } private void addText(String... string) { for (String s : string) { mPackageText.append(s); } } private void addSectionTitle(String string) { String s = mPackageText.getText(); int start = (s == null ? 0 : s.length()); mPackageText.append(string); StyleRange sr = new StyleRange(); sr.start = start; sr.length = string.length(); sr.fontStyle = SWT.BOLD; sr.underline = true; mPackageText.setStyleRange(sr); } private void updateLicenceRadios(ArchiveInfo ai) { if (mInternalLicenseRadioUpdate) { return; } mInternalLicenseRadioUpdate = true; boolean oneAccepted = false; mLicenseRadioAcceptLicense.setSelection(mAcceptSameAllLicense); oneAccepted = ai != null && ai.isAccepted(); mLicenseRadioAccept.setEnabled(ai != null); mLicenseRadioReject.setEnabled(ai != null); mLicenseRadioAccept.setSelection(oneAccepted); mLicenseRadioReject.setSelection(ai != null && ai.isRejected()); // The install button is enabled if there's at least one package accepted. // If the current one isn't, look for another one. boolean missing = mErrorLabel.getText() != null && mErrorLabel.getText().length() > 0; if (!missing && !oneAccepted) { for (ArchiveInfo ai2 : mArchives) { if (ai2.isAccepted()) { oneAccepted = true; break; } } } getButton(IDialogConstants.OK_ID).setEnabled(!missing && oneAccepted); mInternalLicenseRadioUpdate = false; } /** * Callback invoked when one of the radio license buttons is selected. * * - accept/refuse: toggle, update item checkbox * - accept all: set accept-all, check all items with the *same* license */ private void onLicenseRadioSelected() { if (mInternalLicenseRadioUpdate) { return; } mInternalLicenseRadioUpdate = true; Object item = getSelectedItem(); ArchiveInfo ai = (item instanceof ArchiveInfo) ? (ArchiveInfo) item : null; boolean needUpdate = true; if (!mAcceptSameAllLicense && mLicenseRadioAcceptLicense.getSelection()) { // Accept all has been switched on. Mark all packages as accepted List<ArchiveInfo> list = null; if (item instanceof LicenseEntry) { list = ((LicenseEntry) item).getArchives(); } else if (ai != null) { Object p = ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()).getParent(ai); if (p instanceof LicenseEntry) { list = ((LicenseEntry) p).getArchives(); } } if (list != null && list.size() > 0) { mAcceptSameAllLicense = true; for (ArchiveInfo ai2 : list) { ai2.setAccepted(true); ai2.setRejected(false); } } } else if (ai != null && mLicenseRadioAccept.getSelection()) { // Accept only this one mAcceptSameAllLicense = false; ai.setAccepted(true); ai.setRejected(false); } else if (ai != null && mLicenseRadioReject.getSelection()) { // Reject only this one mAcceptSameAllLicense = false; ai.setAccepted(false); ai.setRejected(true); } else { needUpdate = false; } mInternalLicenseRadioUpdate = false; if (needUpdate) { if (mAcceptSameAllLicense) { mTreeViewPackage.refresh(); } else { mTreeViewPackage.refresh(ai); mTreeViewPackage.refresh( ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()).getParent(ai)); } displayMissingDependency(ai); updateLicenceRadios(ai); } } /** * Callback invoked when a package item is double-clicked in the list. */ private void onPackageDoubleClick() { Object item = getSelectedItem(); if (item instanceof ArchiveInfo) { ArchiveInfo ai = (ArchiveInfo) item; boolean wasAccepted = ai.isAccepted(); ai.setAccepted(!wasAccepted); ai.setRejected(wasAccepted); // update state mAcceptSameAllLicense = false; mTreeViewPackage.refresh(ai); // refresh parent since its icon might have changed. mTreeViewPackage .refresh(((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()).getParent(ai)); displayMissingDependency(ai); updateLicenceRadios(ai); } else if (item instanceof LicenseEntry) { mTreeViewPackage.setExpandedState(item, !mTreeViewPackage.getExpandedState(item)); } } /** * Provides the labels for the tree view. * Root branches are {@link LicenseEntry} elements. * Leave nodes are {@link ArchiveInfo} which all have the same license. */ private class NewArchivesLabelProvider extends LabelProvider { @Override public Image getImage(Object element) { if (element instanceof ArchiveInfo) { // Archive icon: accepted (green), rejected (red), not set yet (question mark) ArchiveInfo ai = (ArchiveInfo) element; ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); if (imgFactory != null) { if (ai.isAccepted()) { return imgFactory.getImageByName("accept_icon16.png"); } else if (ai.isRejected()) { return imgFactory.getImageByName("reject_icon16.png"); } return imgFactory.getImageByName("unknown_icon16.png"); } return super.getImage(element); } else if (element instanceof LicenseEntry) { // License icon: green if all below are accepted, red if all rejected, otherwise // no icon. ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); if (imgFactory != null) { boolean allAccepted = true; boolean allRejected = true; for (ArchiveInfo ai : ((LicenseEntry) element).getArchives()) { allAccepted = allAccepted && ai.isAccepted(); allRejected = allRejected && ai.isRejected(); } if (allAccepted && !allRejected) { return imgFactory.getImageByName("accept_icon16.png"); } else if (!allAccepted && allRejected) { return imgFactory.getImageByName("reject_icon16.png"); } } } return null; } @Override public String getText(Object element) { if (element instanceof LicenseEntry) { return ((LicenseEntry) element).getLicenseRef(); } else if (element instanceof ArchiveInfo) { ArchiveInfo ai = (ArchiveInfo) element; String desc = ai.getShortDescription(); if (ai.isDependencyFor()) { desc += " [*]"; } return desc; } assert element instanceof String || element instanceof ArchiveInfo; return null; } } /** * Provides the content for the tree view. * Root branches are {@link LicenseEntry} elements. * Leave nodes are {@link ArchiveInfo} which all have the same license. */ private class NewArchivesContentProvider implements ITreeContentProvider { private List<LicenseEntry> mInput; @Override public void dispose() { // pass } @SuppressWarnings("unchecked") @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // Input should be the result from createTreeInput. if (newInput instanceof List<?> && ((List<?>) newInput).size() > 0 && ((List<?>) newInput).get(0) instanceof LicenseEntry) { mInput = (List<LicenseEntry>) newInput; } else { mInput = null; } } @Override public boolean hasChildren(Object parent) { if (parent instanceof List<?>) { // This is the root of the tree. return true; } else if (parent instanceof LicenseEntry) { return ((LicenseEntry) parent).getArchives().size() > 0; } return false; } @Override public Object[] getElements(Object parent) { return getChildren(parent); } @Override public Object[] getChildren(Object parent) { if (parent instanceof List<?>) { return ((List<?>) parent).toArray(); } else if (parent instanceof LicenseEntry) { return ((LicenseEntry) parent).getArchives().toArray(); } return new Object[0]; } @Override public Object getParent(Object child) { if (child instanceof LicenseEntry) { return ((LicenseEntry) child).getRoot(); } else if (child instanceof ArchiveInfo && mInput != null) { for (LicenseEntry entry : mInput) { if (entry.getArchives().contains(child)) { return entry; } } } return null; } } /** * Represents a branch in the view tree: an entry where all the sub-archive info * share the same license. Contains a link to the share root list for convenience. */ private static class LicenseEntry { private final List<LicenseEntry> mRoot; private final String mLicenseRef; private final List<ArchiveInfo> mArchives; public LicenseEntry(@NonNull List<LicenseEntry> root, @NonNull String licenseRef, @NonNull List<ArchiveInfo> archives) { mRoot = root; mLicenseRef = licenseRef; mArchives = archives; } @NonNull public List<LicenseEntry> getRoot() { return mRoot; } @NonNull public String getLicenseRef() { return mLicenseRef; } @NonNull public List<ArchiveInfo> getArchives() { return mArchives; } } /** * Creates the tree structure based on the given archives. * The current structure is to have a branch per license type, * with all the archives sharing the same license under it. * Elements with no license are left at the root. * * @param archives The non-null collection of archive info to display. Ideally non-empty. * @return A list of {@link LicenseEntry}, each containing a list of {@link ArchiveInfo}. */ @NonNull private List<LicenseEntry> createTreeInput(@NonNull Collection<ArchiveInfo> archives) { // Build an ordered map with all the licenses, ordered by license ref name. final String noLicense = "No license"; //NLS Comparator<String> comp = new Comparator<String>() { @Override public int compare(String s1, String s2) { boolean first1 = noLicense.equals(s1); boolean first2 = noLicense.equals(s2); if (first1 && first2) { return 0; } else if (first1) { return -1; } else if (first2) { return 1; } return s1.compareTo(s2); } }; Map<String, List<ArchiveInfo>> map = new TreeMap<String, List<ArchiveInfo>>(comp); for (ArchiveInfo info : archives) { String ref = noLicense; License license = getLicense(info); if (license != null && license.getLicenseRef() != null) { ref = prettyLicenseRef(license.getLicenseRef()); } List<ArchiveInfo> list = map.get(ref); if (list == null) { map.put(ref, list = new ArrayList<ArchiveInfo>()); } list.add(info); } // Transform result into a list List<LicenseEntry> licensesList = new ArrayList<LicenseEntry>(); for (Map.Entry<String, List<ArchiveInfo>> entry : map.entrySet()) { licensesList.add(new LicenseEntry(licensesList, entry.getKey(), entry.getValue())); } return licensesList; } /** * Helper method to retrieve the {@link License} for a given {@link ArchiveInfo}. * * @param ai The archive info. Can be null. * @return The license for the package owning the archive. Can be null. */ @Nullable private License getLicense(@Nullable ArchiveInfo ai) { if (ai != null) { Archive aNew = ai.getNewArchive(); if (aNew != null) { Package pNew = aNew.getParentPackage(); if (pNew != null) { return pNew.getLicense(); } } } return null; } /** * Reformats the licenseRef to be more human-readable. * It's an XML ref and in practice it looks like [oem-]android-[type]-license. * If it's not a format we can deal with, leave it alone. */ private String prettyLicenseRef(String ref) { // capitalize every word StringBuilder sb = new StringBuilder(); boolean capitalize = true; for (char c : ref.toCharArray()) { if (c >= 'a' && c <= 'z') { if (capitalize) { c = (char) (c + 'A' - 'a'); capitalize = false; } } else { if (c == '-') { c = ' '; } capitalize = true; } sb.append(c); } ref = sb.toString(); // A few acronyms should stay upper-case for (String w : new String[] { "Sdk", "Mips", "Arm" }) { ref = ref.replaceAll(w, w.toUpperCase(Locale.US)); } return ref; } // End of hiding from SWT Designer //$hide<<$ }