aboutsummaryrefslogtreecommitdiffstats
path: root/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java
blob: 3ca0ee3c893231d859d7567361c919f2fc1ec13e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
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);
    }
}