1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.sdkuilib.internal.repository.ui;
18
19import com.android.SdkConstants;
20import com.android.sdklib.internal.repository.DownloadCache;
21import com.android.sdklib.internal.repository.DownloadCache.Strategy;
22import com.android.sdklib.internal.repository.IDescription;
23import com.android.sdklib.internal.repository.archives.Archive;
24import com.android.sdklib.internal.repository.packages.Package;
25import com.android.sdklib.internal.repository.sources.SdkSource;
26import com.android.sdkuilib.internal.repository.UpdaterData;
27import com.android.sdkuilib.internal.repository.core.PackageLoader;
28import com.android.sdkuilib.internal.repository.core.PackageLoader.ISourceLoadedCallback;
29import com.android.sdkuilib.internal.repository.core.PackagesDiffLogic;
30import com.android.sdkuilib.internal.repository.core.PkgCategory;
31import com.android.sdkuilib.internal.repository.core.PkgCategoryApi;
32import com.android.sdkuilib.internal.repository.core.PkgContentProvider;
33import com.android.sdkuilib.internal.repository.core.PkgItem;
34import com.android.sdkuilib.internal.repository.core.PkgItem.PkgState;
35import com.android.sdkuilib.internal.repository.icons.ImageFactory;
36
37import org.eclipse.jface.viewers.ColumnLabelProvider;
38import org.eclipse.jface.viewers.IInputProvider;
39import org.eclipse.jface.viewers.ITableFontProvider;
40import org.eclipse.swt.graphics.Font;
41import org.eclipse.swt.graphics.Image;
42import org.eclipse.swt.graphics.Point;
43
44import java.net.MalformedURLException;
45import java.net.URL;
46import java.util.List;
47
48/**
49 * Base class for {@link PackagesPage} that holds most of the logic to display
50 * the tree/list of packages. This class holds most of the logic and {@link PackagesPage}
51 * holds most of the UI (creating the UI, dealing with menus and buttons and tree
52 * selection.) This makes it easier to test the functionality by mocking only a
53 * subset of the UI.
54 */
55abstract class PackagesPageImpl {
56
57    final UpdaterData mUpdaterData;
58    final PackagesDiffLogic mDiffLogic;
59
60    private ICheckboxTreeViewer mITreeViewer;
61    private ITreeViewerColumn   mIColumnName;
62    private ITreeViewerColumn   mIColumnApi;
63    private ITreeViewerColumn   mIColumnRevision;
64    private ITreeViewerColumn   mIColumnStatus;
65
66    PackagesPageImpl(UpdaterData updaterData) {
67        mUpdaterData = updaterData;
68        mDiffLogic = new PackagesDiffLogic(updaterData);
69    }
70
71    /**
72     * Utility method that derived classes can override to check whether the UI is disposed.
73     * When the UI is disposed, most operations that affect the UI will be bypassed.
74     * @return True if UI is not available and should not be touched.
75     */
76    abstract protected boolean isUiDisposed();
77
78    /**
79     * Utility method to execute a runnable on the main UI thread.
80     * Will do nothing if {@link #isUiDisposed()} returns false.
81     * @param runnable The runnable to execute on the main UI thread.
82     */
83    abstract protected void syncExec(Runnable runnable);
84
85    void performFirstLoad() {
86        // First a package loader is created that only checks
87        // the local cache xml files. It populates the package
88        // list based on what the client got last, essentially.
89        loadPackages(true /*useLocalCache*/, false /*overrideExisting*/);
90
91        // Next a regular package loader is created that will
92        // respect the expiration and refresh parameters of the
93        // download cache.
94        loadPackages(false /*useLocalCache*/, true /*overrideExisting*/);
95    }
96
97    public void setITreeViewer(ICheckboxTreeViewer iTreeViewer) {
98        mITreeViewer = iTreeViewer;
99    }
100
101    public void setIColumns(
102            ITreeViewerColumn columnName,
103            ITreeViewerColumn columnApi,
104            ITreeViewerColumn columnRevision,
105            ITreeViewerColumn columnStatus) {
106        mIColumnName = columnName;
107        mIColumnApi = columnApi;
108        mIColumnRevision = columnRevision;
109        mIColumnStatus = columnStatus;
110    }
111
112    void postCreate() {
113        // Caller needs to call setITreeViewer before this.
114        assert mITreeViewer     != null;
115        // Caller needs to call setIColumns before this.
116        assert mIColumnApi      != null;
117        assert mIColumnName     != null;
118        assert mIColumnStatus   != null;
119        assert mIColumnRevision != null;
120
121        mITreeViewer.setContentProvider(new PkgContentProvider(mITreeViewer));
122
123        mIColumnApi.setLabelProvider(
124                new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnApi)));
125        mIColumnName.setLabelProvider(
126                new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnName)));
127        mIColumnStatus.setLabelProvider(
128                new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnStatus)));
129        mIColumnRevision.setLabelProvider(
130                new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnRevision)));
131    }
132
133    /**
134     * Performs a full reload by removing all cached packages data, including the platforms
135     * and addons from the sdkmanager instance. This will perform a full local parsing
136     * as well as a full reload of the remote data (by fetching all sources again.)
137     */
138    void fullReload() {
139        // Clear all source information, forcing them to be refreshed.
140        mUpdaterData.getSources().clearAllPackages();
141        // Clear and reload all local data too.
142        localReload();
143    }
144
145    /**
146     * Performs a full reload of all the local package information, including the platforms
147     * and addons from the sdkmanager instance. This will perform a full local parsing.
148     * <p/>
149     * This method does NOT force a new fetch of the remote sources.
150     *
151     * @see #fullReload()
152     */
153    void localReload() {
154        // Clear all source caches, otherwise loading will use the cached data
155        mUpdaterData.getLocalSdkParser().clearPackages();
156        mUpdaterData.getSdkManager().reloadSdk(mUpdaterData.getSdkLog());
157        loadPackages();
158    }
159
160    /**
161     * Performs a "normal" reload of the package information, use the default download
162     * cache and refreshing strategy as needed.
163     */
164    void loadPackages() {
165        loadPackages(false /*useLocalCache*/, false /*overrideExisting*/);
166    }
167
168    /**
169     * Performs a reload of the package information.
170     *
171     * @param useLocalCache When true, the {@link PackageLoader} is switched to use
172     *  a specific {@link DownloadCache} using the {@link Strategy#ONLY_CACHE}, meaning
173     *  it will only use data from the local cache. It will not try to fetch or refresh
174     *  manifests. This is used once the very first time the sdk manager window opens
175     *  and is typically followed by a regular load with refresh.
176     */
177    abstract protected void loadPackages(boolean useLocalCache, boolean overrideExisting);
178
179    /**
180     * Actual implementation of {@link #loadPackages(boolean, boolean)}.
181     * Derived implementations must call this to do the actual work after setting up the UI.
182     */
183    void loadPackagesImpl(final boolean useLocalCache, final boolean overrideExisting) {
184        if (mUpdaterData == null) {
185            return;
186        }
187
188        final boolean displaySortByApi = isSortByApi();
189
190        PackageLoader packageLoader = getPackageLoader(useLocalCache);
191        assert packageLoader != null;
192
193        mDiffLogic.updateStart();
194        packageLoader.loadPackages(overrideExisting, new ISourceLoadedCallback() {
195            @Override
196            public boolean onUpdateSource(SdkSource source, Package[] newPackages) {
197                // This runs in a thread and must not access UI directly.
198                final boolean changed = mDiffLogic.updateSourcePackages(
199                        displaySortByApi, source, newPackages);
200
201                syncExec(new Runnable() {
202                    @Override
203                    public void run() {
204                        if (changed ||
205                            mITreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) {
206                                refreshViewerInput();
207                        }
208                    }
209                });
210
211                // Return true to tell the loader to continue with the next source.
212                // Return false to stop the loader if any UI has been disposed, which can
213                // happen if the user is trying to close the window during the load operation.
214                return !isUiDisposed();
215            }
216
217            @Override
218            public void onLoadCompleted() {
219                // This runs in a thread and must not access UI directly.
220                final boolean changed = mDiffLogic.updateEnd(displaySortByApi);
221
222                syncExec(new Runnable() {
223                    @Override
224                    public void run() {
225                        if (changed ||
226                            mITreeViewer.getInput() != mDiffLogic.getCategories(isSortByApi())) {
227                            try {
228                                refreshViewerInput();
229                            } catch (Exception ignore) {}
230                        }
231
232                        if (!useLocalCache &&
233                                mDiffLogic.isFirstLoadComplete() &&
234                                !isUiDisposed()) {
235                            // At the end of the first load, if nothing is selected then
236                            // automatically select all new and update packages.
237                            Object[] checked = mITreeViewer.getCheckedElements();
238                            if (checked == null || checked.length == 0) {
239                                onSelectNewUpdates(
240                                        false, //selectNew
241                                        true,  //selectUpdates,
242                                        true); //selectTop
243                            }
244                        }
245                    }
246                });
247            }
248        });
249    }
250
251    /**
252     * Used by {@link #loadPackagesImpl(boolean, boolean)} to get the package
253     * loader for the first or second pass update. When starting the manager
254     * starts with a first pass that reads only from the local cache, with no
255     * extra network access. That's {@code useLocalCache} being true.
256     * <p/>
257     * Leter it does a second pass with {@code useLocalCache} set to false
258     * and actually uses the download cache specified in {@link UpdaterData}.
259     *
260     * This is extracted so that we can control this cache via unit tests.
261     */
262    protected PackageLoader getPackageLoader(boolean useLocalCache) {
263        if (useLocalCache) {
264            return new PackageLoader(mUpdaterData, new DownloadCache(Strategy.ONLY_CACHE));
265        } else {
266            return mUpdaterData.getPackageLoader();
267        }
268    }
269
270    /**
271     * Overridden by the UI to respond to a request to refresh the tree viewer
272     * when the input has changed.
273     * The implementation must call {@link #setViewerInput()} somehow and will
274     * also need to adjust the expand state of the tree items and/or update
275     * some buttons or other state.
276     */
277    abstract protected void refreshViewerInput();
278
279    /**
280     * Invoked from {@link #refreshViewerInput()} to actually either set the
281     * input of the tree viewer or refresh it if it's the <em>same</em> input
282     * object.
283     */
284    protected void setViewerInput() {
285        List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi());
286        if (mITreeViewer.getInput() != cats) {
287            // set initial input
288            mITreeViewer.setInput(cats);
289        } else {
290            // refresh existing, which preserves the expanded state, the selection
291            // and the checked state.
292            mITreeViewer.refresh();
293        }
294    }
295
296    /**
297     * Overridden by the UI to determine if the tree should display packages sorted
298     * by API (returns true) or by repository source (returns false.)
299     */
300    abstract protected boolean isSortByApi();
301
302    /**
303     * Checks all PkgItems that are either new or have updates or select top platform
304     * for initial run.
305     */
306    void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) {
307        // This does not update the tree itself, syncViewerSelection does it in the caller.
308        mDiffLogic.checkNewUpdateItems(
309                selectNew,
310                selectUpdates,
311                selectTop,
312                SdkConstants.CURRENT_PLATFORM);
313    }
314
315    /**
316     * Deselect all checked PkgItems.
317     */
318    void onDeselectAll() {
319        // This does not update the tree itself, syncViewerSelection does it in the caller.
320        mDiffLogic.uncheckAllItems();
321    }
322
323    // ----------------------
324
325    abstract protected Font getTreeFontItalic();
326
327    class PkgCellLabelProvider extends ColumnLabelProvider implements ITableFontProvider {
328
329        private final ITreeViewerColumn mColumn;
330
331        public PkgCellLabelProvider(ITreeViewerColumn column) {
332            super();
333            mColumn = column;
334        }
335
336        @Override
337        public String getText(Object element) {
338
339            if (mColumn == mIColumnName) {
340                if (element instanceof PkgCategory) {
341                    return ((PkgCategory) element).getLabel();
342                } else if (element instanceof PkgItem) {
343                    return getPkgItemName((PkgItem) element);
344                } else if (element instanceof IDescription) {
345                    return ((IDescription) element).getShortDescription();
346                }
347
348            } else if (mColumn == mIColumnApi) {
349                int api = -1;
350                if (element instanceof PkgItem) {
351                    api = ((PkgItem) element).getApi();
352                }
353                if (api >= 1) {
354                    return Integer.toString(api);
355                }
356
357            } else if (mColumn == mIColumnRevision) {
358                if (element instanceof PkgItem) {
359                    PkgItem pkg = (PkgItem) element;
360                    return pkg.getRevision().toShortString();
361                }
362
363            } else if (mColumn == mIColumnStatus) {
364                if (element instanceof PkgItem) {
365                    PkgItem pkg = (PkgItem) element;
366
367                    switch(pkg.getState()) {
368                    case INSTALLED:
369                        Package update = pkg.getUpdatePkg();
370                        if (update != null) {
371                            return String.format(
372                                    "Update available: rev. %1$s",
373                                    update.getRevision().toShortString());
374                        }
375                        return "Installed";
376
377                    case NEW:
378                        Package p = pkg.getMainPackage();
379                        if (p != null && p.hasCompatibleArchive()) {
380                            return "Not installed";
381                        } else {
382                            return String.format("Not compatible with %1$s",
383                                    SdkConstants.currentPlatformName());
384                        }
385                    }
386                    return pkg.getState().toString();
387
388                } else if (element instanceof Package) {
389                    // This is an update package.
390                    return "New revision " + ((Package) element).getRevision().toShortString();
391                }
392            }
393
394            return ""; //$NON-NLS-1$
395        }
396
397        private String getPkgItemName(PkgItem item) {
398            String name = item.getName().trim();
399
400            if (isSortByApi()) {
401                // When sorting by API, the package name might contains the API number
402                // or the platform name at the end. If we find it, cut it out since it's
403                // redundant.
404
405                PkgCategoryApi cat = (PkgCategoryApi) findCategoryForItem(item);
406                String apiLabel = cat.getApiLabel();
407                String platLabel = cat.getPlatformName();
408
409                if (platLabel != null && name.endsWith(platLabel)) {
410                    return name.substring(0, name.length() - platLabel.length());
411
412                } else if (apiLabel != null && name.endsWith(apiLabel)) {
413                    return name.substring(0, name.length() - apiLabel.length());
414
415                } else if (platLabel != null && item.isObsolete() && name.indexOf(platLabel) > 0) {
416                    // For obsolete items, the format is "<base name> <platform name> (Obsolete)"
417                    // so in this case only accept removing a platform name that is not at
418                    // the end.
419                    name = name.replace(platLabel, ""); //$NON-NLS-1$
420                }
421            }
422
423            // Collapse potential duplicated spacing
424            name = name.replaceAll(" +", " "); //$NON-NLS-1$ //$NON-NLS-2$
425
426            return name;
427        }
428
429        private PkgCategory findCategoryForItem(PkgItem item) {
430            List<PkgCategory> cats = mDiffLogic.getCategories(isSortByApi());
431            for (PkgCategory cat : cats) {
432                for (PkgItem i : cat.getItems()) {
433                    if (i == item) {
434                        return cat;
435                    }
436                }
437            }
438
439            return null;
440        }
441
442        @Override
443        public Image getImage(Object element) {
444            ImageFactory imgFactory = mUpdaterData.getImageFactory();
445
446            if (imgFactory != null) {
447                if (mColumn == mIColumnName) {
448                    if (element instanceof PkgCategory) {
449                        return imgFactory.getImageForObject(((PkgCategory) element).getIconRef());
450                    } else if (element instanceof PkgItem) {
451                        return imgFactory.getImageForObject(((PkgItem) element).getMainPackage());
452                    }
453                    return imgFactory.getImageForObject(element);
454
455                } else if (mColumn == mIColumnStatus && element instanceof PkgItem) {
456                    PkgItem pi = (PkgItem) element;
457                    switch(pi.getState()) {
458                    case INSTALLED:
459                        if (pi.hasUpdatePkg()) {
460                            return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_UPDATE);
461                        } else {
462                            return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_INSTALLED);
463                        }
464                    case NEW:
465                        Package p = pi.getMainPackage();
466                        if (p != null && p.hasCompatibleArchive()) {
467                            return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_NEW);
468                        } else {
469                            return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_INCOMPAT);
470                        }
471                    }
472                }
473            }
474            return super.getImage(element);
475        }
476
477        // -- ITableFontProvider
478
479        @Override
480        public Font getFont(Object element, int columnIndex) {
481            if (element instanceof PkgItem) {
482                if (((PkgItem) element).getState() == PkgState.NEW) {
483                    return getTreeFontItalic();
484                }
485            } else if (element instanceof Package) {
486                // update package
487                return getTreeFontItalic();
488            }
489            return super.getFont(element);
490        }
491
492        // -- Tooltip support
493
494        @Override
495        public String getToolTipText(Object element) {
496            PkgItem pi = element instanceof PkgItem ? (PkgItem) element : null;
497            if (pi != null) {
498                element = pi.getMainPackage();
499            }
500            if (element instanceof IDescription) {
501                String s = getTooltipDescription((IDescription) element);
502
503                if (pi != null && pi.hasUpdatePkg()) {
504                    s += "\n-----------------" +        //$NON-NLS-1$
505                         "\nUpdate Available:\n" +      //$NON-NLS-1$
506                         getTooltipDescription(pi.getUpdatePkg());
507                }
508
509                return s;
510            }
511            return super.getToolTipText(element);
512        }
513
514        private String getTooltipDescription(IDescription element) {
515            String s = element.getLongDescription();
516            if (element instanceof Package) {
517                Package p = (Package) element;
518
519                if (!p.isLocal()) {
520                    // For non-installed item, try to find a download size
521                    for (Archive a : p.getArchives()) {
522                        if (!a.isLocal() && a.isCompatible()) {
523                            s += '\n' + a.getSizeDescription();
524                            break;
525                        }
526                    }
527                }
528
529                // Display info about where this package comes/came from
530                SdkSource src = p.getParentSource();
531                if (src != null) {
532                    try {
533                        URL url = new URL(src.getUrl());
534                        String host = url.getHost();
535                        if (p.isLocal()) {
536                            s += String.format("\nInstalled from %1$s", host);
537                        } else {
538                            s += String.format("\nProvided by %1$s", host);
539                        }
540                    } catch (MalformedURLException ignore) {
541                    }
542                }
543            }
544            return s;
545        }
546
547        @Override
548        public Point getToolTipShift(Object object) {
549            return new Point(15, 5);
550        }
551
552        @Override
553        public int getToolTipDisplayDelayTime(Object object) {
554            return 500;
555        }
556    }
557
558    interface ICheckboxTreeViewer extends IInputProvider {
559        void setContentProvider(PkgContentProvider pkgContentProvider);
560        void refresh();
561        void setInput(List<PkgCategory> cats);
562        Object[] getCheckedElements();
563    }
564
565    interface ITreeViewerColumn {
566        void setLabelProvider(ColumnLabelProvider labelProvider);
567    }
568}
569