1/*
2 * Copyright (C) 2011 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.core;
18
19import com.android.SdkConstants;
20import com.android.sdklib.AndroidVersion;
21import com.android.sdklib.IAndroidTarget;
22import com.android.sdklib.internal.repository.packages.ExtraPackage;
23import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider;
24import com.android.sdklib.internal.repository.packages.IFullRevisionProvider;
25import com.android.sdklib.internal.repository.packages.Package;
26import com.android.sdklib.internal.repository.packages.Package.UpdateInfo;
27import com.android.sdklib.internal.repository.packages.PlatformPackage;
28import com.android.sdklib.internal.repository.packages.PlatformToolPackage;
29import com.android.sdklib.internal.repository.packages.SystemImagePackage;
30import com.android.sdklib.internal.repository.packages.ToolPackage;
31import com.android.sdklib.internal.repository.sources.SdkSource;
32import com.android.sdklib.util.SparseArray;
33import com.android.sdkuilib.internal.repository.UpdaterData;
34import com.android.sdkuilib.internal.repository.core.PkgItem.PkgState;
35import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons;
36
37import java.util.ArrayList;
38import java.util.Collections;
39import java.util.Comparator;
40import java.util.HashMap;
41import java.util.HashSet;
42import java.util.Iterator;
43import java.util.List;
44import java.util.Map;
45import java.util.Set;
46
47/**
48 * Helper class that separates the logic of package management from the UI
49 * so that we can test it using head-less unit tests.
50 */
51public class PackagesDiffLogic {
52    private final UpdaterData mUpdaterData;
53    private boolean mFirstLoadComplete = true;
54
55    public PackagesDiffLogic(UpdaterData updaterData) {
56        mUpdaterData = updaterData;
57    }
58
59    /**
60     * Removes all the internal state and resets the object.
61     * Useful for testing.
62     */
63    public void clear() {
64        mFirstLoadComplete = true;
65        mOpApi.clear();
66        mOpSource.clear();
67    }
68
69    /** Return mFirstLoadComplete and resets it to false.
70     * All following calls will returns false. */
71    public boolean isFirstLoadComplete() {
72        boolean b = mFirstLoadComplete;
73        mFirstLoadComplete = false;
74        return b;
75    }
76
77    /**
78     * Mark all new and update PkgItems as checked.
79     *
80     * @param selectNew If true, select all new packages (except the rc/preview ones).
81     * @param selectUpdates If true, select all update packages.
82     * @param selectTop If true, select the top platform.
83     *   If the top platform has nothing installed, select all items in it (except the rc/preview);
84     *   If it is partially installed, at least select the platform and system images if none of
85     *   the system images are installed.
86     * @param currentPlatform The {@link SdkConstants#currentPlatform()} value.
87     */
88    public void checkNewUpdateItems(
89            boolean selectNew,
90            boolean selectUpdates,
91            boolean selectTop,
92            int currentPlatform) {
93        int maxApi = 0;
94        Set<Integer> installedPlatforms = new HashSet<Integer>();
95        SparseArray<List<PkgItem>> platformItems = new SparseArray<List<PkgItem>>();
96
97        // sort items in platforms... directly deal with new/update items
98        List<PkgItem> allItems = getAllPkgItems(true /*byApi*/, true /*bySource*/);
99        for (PkgItem item : allItems) {
100            if (!item.hasCompatibleArchive()) {
101                // Ignore items that have no archive compatible with the current platform.
102                continue;
103            }
104
105            // Get the main package's API level. We don't need to look at the updates
106            // since by definition they should target the same API level.
107            int api = 0;
108            Package p = item.getMainPackage();
109            if (p instanceof IAndroidVersionProvider) {
110                api = ((IAndroidVersionProvider) p).getAndroidVersion().getApiLevel();
111            }
112
113            if (selectTop && api > 0) {
114                // Keep track of the max api seen
115                maxApi = Math.max(maxApi, api);
116
117                // keep track of what platform is currently installed (that is, has at least
118                // one thing installed.)
119                if (item.getState() == PkgState.INSTALLED) {
120                    installedPlatforms.add(api);
121                }
122
123                // for each platform, collect all its related item for later use below.
124                List<PkgItem> items = platformItems.get(api);
125                if (items == null) {
126                    platformItems.put(api, items = new ArrayList<PkgItem>());
127                }
128                items.add(item);
129            }
130
131            if ((selectUpdates || selectNew) &&
132                    item.getState() == PkgState.NEW &&
133                    !item.getRevision().isPreview()) {
134                boolean sameFound = false;
135                Package newPkg = item.getMainPackage();
136                if (newPkg instanceof IFullRevisionProvider) {
137                    // We have a potential new non-preview package; but this kind of package
138                    // supports having previews, which means we want to make sure we're not
139                    // offering an older "new" non-preview if there's a newer preview installed.
140                    //
141                    // We should get into this odd situation only when updating an RC/preview
142                    // by a final release pkg.
143
144                    IFullRevisionProvider newPkg2 = (IFullRevisionProvider) newPkg;
145                    for (PkgItem item2 : allItems) {
146                        if (item2.getState() == PkgState.INSTALLED) {
147                            Package installed = item2.getMainPackage();
148
149                            if (installed.getRevision().isPreview() &&
150                                    newPkg2.sameItemAs(installed, true /*ignorePreviews*/)) {
151                                sameFound = true;
152
153                                if (installed.canBeUpdatedBy(newPkg) == UpdateInfo.UPDATE) {
154                                    item.setChecked(true);
155                                    break;
156                                }
157                            }
158                        }
159                    }
160                }
161
162                if (selectNew && !sameFound) {
163                    item.setChecked(true);
164                }
165
166            } else if (selectUpdates && item.hasUpdatePkg()) {
167                item.setChecked(true);
168            }
169        }
170
171        List<PkgItem> items = platformItems.get(maxApi);
172        if (selectTop && maxApi > 0 && items != null) {
173            if (!installedPlatforms.contains(maxApi)) {
174                // If the top platform has nothing installed at all, select everything in it
175                for (PkgItem item : items) {
176                    if ((item.getState() == PkgState.NEW && !item.getRevision().isPreview()) ||
177                            item.hasUpdatePkg()) {
178                        item.setChecked(true);
179                    }
180                }
181
182            } else {
183                // The top platform has at least one thing installed.
184
185                // First make sure the platform package itself is installed, or select it.
186                for (PkgItem item : items) {
187                     Package p = item.getMainPackage();
188                     if (p instanceof PlatformPackage &&
189                             item.getState() == PkgState.NEW && !item.getRevision().isPreview()) {
190                         item.setChecked(true);
191                         break;
192                     }
193                }
194
195                // Check we have at least one system image installed, otherwise select them
196                boolean hasSysImg = false;
197                for (PkgItem item : items) {
198                    Package p = item.getMainPackage();
199                    if (p instanceof PlatformPackage && item.getState() == PkgState.INSTALLED) {
200                        if (item.hasUpdatePkg() && item.isChecked()) {
201                            // If the installed platform is scheduled for update, look for the
202                            // system image in the update package, not the current one.
203                            p = item.getUpdatePkg();
204                            if (p instanceof PlatformPackage) {
205                                hasSysImg = ((PlatformPackage) p).getIncludedAbi() != null;
206                            }
207                        } else {
208                            // Otherwise look into the currently installed platform
209                            hasSysImg = ((PlatformPackage) p).getIncludedAbi() != null;
210                        }
211                        if (hasSysImg) {
212                            break;
213                        }
214                    }
215                    if (p instanceof SystemImagePackage && item.getState() == PkgState.INSTALLED) {
216                        hasSysImg = true;
217                        break;
218                    }
219                }
220                if (!hasSysImg) {
221                    // No system image installed.
222                    // Try whether the current platform or its update would bring one.
223
224                    for (PkgItem item : items) {
225                         Package p = item.getMainPackage();
226                         if (p instanceof PlatformPackage) {
227                             if (item.getState() == PkgState.NEW &&
228                                     !item.getRevision().isPreview() &&
229                                     ((PlatformPackage) p).getIncludedAbi() != null) {
230                                 item.setChecked(true);
231                                 hasSysImg = true;
232                             } else if (item.hasUpdatePkg()) {
233                                 p = item.getUpdatePkg();
234                                 if (p instanceof PlatformPackage &&
235                                         ((PlatformPackage) p).getIncludedAbi() != null) {
236                                     item.setChecked(true);
237                                     hasSysImg = true;
238                                 }
239                             }
240                         }
241                    }
242                }
243                if (!hasSysImg) {
244                    // No system image in the platform, try a system image package
245                    for (PkgItem item : items) {
246                        Package p = item.getMainPackage();
247                        if (p instanceof SystemImagePackage && item.getState() == PkgState.NEW) {
248                            item.setChecked(true);
249                        }
250                    }
251                }
252            }
253        }
254
255        if (selectTop && currentPlatform == SdkConstants.PLATFORM_WINDOWS) {
256            // On Windows, we'll also auto-select the USB driver
257            for (PkgItem item : getAllPkgItems(true /*byApi*/, true /*bySource*/)) {
258                Package p = item.getMainPackage();
259                if (p instanceof ExtraPackage &&
260                        item.getState() == PkgState.NEW &&
261                        !item.getRevision().isPreview()) {
262                    ExtraPackage ep = (ExtraPackage) p;
263                    if (ep.getVendorId().equals("google") &&            //$NON-NLS-1$
264                            ep.getPath().equals("usb_driver")) {        //$NON-NLS-1$
265                        item.setChecked(true);
266                    }
267                }
268            }
269        }
270    }
271
272    /**
273     * Mark all PkgItems as not checked.
274     */
275    public void uncheckAllItems() {
276        for (PkgItem item : getAllPkgItems(true /*byApi*/, true /*bySource*/)) {
277            item.setChecked(false);
278        }
279    }
280
281    /**
282     * An update operation, customized to either sort by API or sort by source.
283     */
284    abstract class UpdateOp {
285        private final Set<SdkSource> mVisitedSources = new HashSet<SdkSource>();
286        private final List<PkgCategory> mCategories = new ArrayList<PkgCategory>();
287        private final Set<PkgCategory> mCatsToRemove = new HashSet<PkgCategory>();
288        private final Set<PkgItem> mItemsToRemove = new HashSet<PkgItem>();
289        private final Map<Package, PkgItem> mUpdatesToRemove = new HashMap<Package, PkgItem>();
290
291        /** Removes all internal state. */
292        public void clear() {
293            mVisitedSources.clear();
294            mCategories.clear();
295        }
296
297        /** Retrieve the sorted category list. */
298        public List<PkgCategory> getCategories() {
299            return mCategories;
300        }
301
302        /** Retrieve the category key for the given package, either local or remote. */
303        public abstract Object getCategoryKey(Package pkg);
304
305        /** Modified {@code currentCategories} to add default categories. */
306        public abstract void addDefaultCategories();
307
308        /** Creates the category for the given key and returns it. */
309        public abstract PkgCategory createCategory(Object catKey);
310        /** Adjust attributes of an existing category. */
311        public abstract void adjustCategory(PkgCategory cat, Object catKey);
312
313        /** Sorts the category list (but not the items within the categories.) */
314        public abstract void sortCategoryList();
315
316        /** Called after items of a given category have changed. Used to sort the
317         * items and/or adjust the category name. */
318        public abstract void postCategoryItemsChanged();
319
320        public void updateStart() {
321            mVisitedSources.clear();
322
323            // Note that default categories are created after the unused ones so that
324            // the callback can decide whether they should be marked as unused or not.
325            mCatsToRemove.clear();
326            mItemsToRemove.clear();
327            mUpdatesToRemove.clear();
328            for (PkgCategory cat : mCategories) {
329                mCatsToRemove.add(cat);
330                List<PkgItem> items = cat.getItems();
331                mItemsToRemove.addAll(items);
332                for (PkgItem item : items) {
333                    if (item.hasUpdatePkg()) {
334                        mUpdatesToRemove.put(item.getUpdatePkg(), item);
335                    }
336                }
337            }
338
339            addDefaultCategories();
340        }
341
342        public boolean updateSourcePackages(SdkSource source, Package[] newPackages) {
343            mVisitedSources.add(source);
344            if (source == null) {
345                return processLocals(this, newPackages);
346            } else {
347                return processSource(this, source, newPackages);
348            }
349        }
350
351        public boolean updateEnd() {
352            boolean hasChanged = false;
353
354            // Remove unused categories & items at the end of the update
355            synchronized (mCategories) {
356                for (PkgCategory unusedCat : mCatsToRemove) {
357                    if (mCategories.remove(unusedCat)) {
358                        hasChanged  = true;
359                    }
360                }
361            }
362
363            for (PkgCategory cat : mCategories) {
364                for (Iterator<PkgItem> itemIt = cat.getItems().iterator(); itemIt.hasNext(); ) {
365                    PkgItem item = itemIt.next();
366                    if (mItemsToRemove.contains(item)) {
367                        itemIt.remove();
368                        hasChanged  = true;
369                    } else if (item.hasUpdatePkg() &&
370                            mUpdatesToRemove.containsKey(item.getUpdatePkg())) {
371                        item.removeUpdate();
372                        hasChanged  = true;
373                    }
374                }
375            }
376
377            mCatsToRemove.clear();
378            mItemsToRemove.clear();
379            mUpdatesToRemove.clear();
380
381            return hasChanged;
382        }
383
384        public boolean isKeep(PkgItem item) {
385            return !mItemsToRemove.contains(item);
386        }
387
388        public void keep(Package pkg) {
389            mUpdatesToRemove.remove(pkg);
390        }
391
392        public void keep(PkgItem item) {
393            mItemsToRemove.remove(item);
394        }
395
396        public void keep(PkgCategory cat) {
397            mCatsToRemove.remove(cat);
398        }
399
400        public void dontKeep(PkgItem item) {
401            mItemsToRemove.add(item);
402        }
403
404        public void dontKeep(PkgCategory cat) {
405            mCatsToRemove.add(cat);
406        }
407    }
408
409    private final UpdateOpApi    mOpApi    = new UpdateOpApi();
410    private final UpdateOpSource mOpSource = new UpdateOpSource();
411
412    public List<PkgCategory> getCategories(boolean displayIsSortByApi) {
413        return displayIsSortByApi ? mOpApi.getCategories() : mOpSource.getCategories();
414    }
415
416    public List<PkgItem> getAllPkgItems(boolean byApi, boolean bySource) {
417        List<PkgItem> items = new ArrayList<PkgItem>();
418
419        if (byApi) {
420            List<PkgCategory> cats = getCategories(true /*displayIsSortByApi*/);
421            synchronized (cats) {
422                for (PkgCategory cat : cats) {
423                    items.addAll(cat.getItems());
424                }
425            }
426        }
427
428        if (bySource) {
429            List<PkgCategory> cats = getCategories(false /*displayIsSortByApi*/);
430            synchronized (cats) {
431                for (PkgCategory cat : cats) {
432                    items.addAll(cat.getItems());
433                }
434            }
435        }
436
437        return items;
438    }
439
440    public void updateStart() {
441        mOpApi.updateStart();
442        mOpSource.updateStart();
443    }
444
445    public boolean updateSourcePackages(
446            boolean displayIsSortByApi,
447            SdkSource source,
448            Package[] newPackages) {
449
450        boolean apiListChanged = mOpApi.updateSourcePackages(source, newPackages);
451        boolean sourceListChanged = mOpSource.updateSourcePackages(source, newPackages);
452        return displayIsSortByApi ? apiListChanged : sourceListChanged;
453    }
454
455    public boolean updateEnd(boolean displayIsSortByApi) {
456        boolean apiListChanged = mOpApi.updateEnd();
457        boolean sourceListChanged = mOpSource.updateEnd();
458        return displayIsSortByApi ? apiListChanged : sourceListChanged;
459    }
460
461
462    /** Process all local packages. Returns true if something changed. */
463    private boolean processLocals(UpdateOp op, Package[] packages) {
464        boolean hasChanged = false;
465        List<PkgCategory> cats = op.getCategories();
466        Set<PkgItem> keep = new HashSet<PkgItem>();
467
468        // For all locally installed packages, check they are either listed
469        // as installed or create new installed items for them.
470
471        nextPkg: for (Package localPkg : packages) {
472            // Check to see if we already have the exact same package
473            // (type & revision) marked as installed.
474            for (PkgCategory cat : cats) {
475                for (PkgItem currItem : cat.getItems()) {
476                    if (currItem.getState() == PkgState.INSTALLED &&
477                            currItem.isSameMainPackageAs(localPkg)) {
478                        // This package is already listed as installed.
479                        op.keep(currItem);
480                        op.keep(cat);
481                        keep.add(currItem);
482                        continue nextPkg;
483                    }
484                }
485            }
486
487            // If not found, create a new installed package item
488            keep.add(addNewItem(op, localPkg, PkgState.INSTALLED));
489            hasChanged = true;
490        }
491
492        // Remove installed items that we don't want to keep anymore. They would normally be
493        // cleanup up in UpdateOp.updateEnd(); however it's easier to remove them before we
494        // run processSource() to avoid merging updates in items that would be removed later.
495
496        for (PkgCategory cat : cats) {
497            for (Iterator<PkgItem> itemIt = cat.getItems().iterator(); itemIt.hasNext(); ) {
498                PkgItem item = itemIt.next();
499                if (item.getState() == PkgState.INSTALLED && !keep.contains(item)) {
500                    itemIt.remove();
501                    hasChanged = true;
502                }
503            }
504        }
505
506        if (hasChanged) {
507            op.postCategoryItemsChanged();
508        }
509
510        return hasChanged;
511    }
512
513    /**
514     * {@link PkgState}s to check in {@link #processSource(UpdateOp, SdkSource, Package[])}.
515     * The order matters.
516     * When installing the diff will have both the new and the installed item and we
517     * need to merge with the installed one before the new one.
518     */
519    private final static PkgState[] PKG_STATES = { PkgState.INSTALLED, PkgState.NEW };
520
521    /** Process all remote packages. Returns true if something changed. */
522    private boolean processSource(UpdateOp op, SdkSource source, Package[] packages) {
523        boolean hasChanged = false;
524        List<PkgCategory> cats = op.getCategories();
525
526        boolean enablePreviews =
527            mUpdaterData.getSettingsController().getSettings().getEnablePreviews();
528
529        nextPkg: for (Package newPkg : packages) {
530
531            if (!enablePreviews && newPkg.getRevision().isPreview()) {
532                // This is a preview and previews are not enabled. Ignore the package.
533                continue nextPkg;
534            }
535
536            for (PkgCategory cat : cats) {
537                for (PkgState state : PKG_STATES) {
538                    for (Iterator<PkgItem> currItemIt = cat.getItems().iterator();
539                                           currItemIt.hasNext(); ) {
540                        PkgItem currItem = currItemIt.next();
541                        // We need to merge with installed items first. When installing
542                        // the diff will have both the new and the installed item and we
543                        // need to merge with the installed one before the new one.
544                        if (currItem.getState() != state) {
545                            continue;
546                        }
547                        // Only process current items if they represent the same item (but
548                        // with a different revision number) than the new package.
549                        Package mainPkg = currItem.getMainPackage();
550                        if (!mainPkg.sameItemAs(newPkg)) {
551                            continue;
552                        }
553
554                        // Check to see if we already have the exact same package
555                        // (type & revision) marked as main or update package.
556                        if (currItem.isSameMainPackageAs(newPkg)) {
557                            op.keep(currItem);
558                            op.keep(cat);
559                            continue nextPkg;
560                        } else if (currItem.hasUpdatePkg() &&
561                                currItem.isSameUpdatePackageAs(newPkg)) {
562                            op.keep(currItem.getUpdatePkg());
563                            op.keep(cat);
564                            continue nextPkg;
565                        }
566
567                        switch (currItem.getState()) {
568                        case NEW:
569                            if (newPkg.getRevision().compareTo(mainPkg.getRevision()) < 0) {
570                                if (!op.isKeep(currItem)) {
571                                    // The new item has a lower revision than the current one,
572                                    // but the current one hasn't been marked as being kept so
573                                    // it's ok to downgrade it.
574                                    currItemIt.remove();
575                                    addNewItem(op, newPkg, PkgState.NEW);
576                                    hasChanged = true;
577                                }
578                            } else if (newPkg.getRevision().compareTo(mainPkg.getRevision()) > 0) {
579                                // We have a more recent new version, remove the current one
580                                // and replace by a new one
581                                currItemIt.remove();
582                                addNewItem(op, newPkg, PkgState.NEW);
583                                hasChanged = true;
584                            }
585                            break;
586                        case INSTALLED:
587                            // if newPkg.revision<=mainPkg.revision: it's already installed, ignore.
588                            if (newPkg.getRevision().compareTo(mainPkg.getRevision()) > 0) {
589                                // This is a new update for the main package.
590                                if (currItem.mergeUpdate(newPkg)) {
591                                    op.keep(currItem.getUpdatePkg());
592                                    op.keep(cat);
593                                    hasChanged = true;
594                                }
595                            }
596                            break;
597                        }
598                        continue nextPkg;
599                    }
600                }
601            }
602            // If not found, create a new package item
603            addNewItem(op, newPkg, PkgState.NEW);
604            hasChanged = true;
605        }
606
607        if (hasChanged) {
608            op.postCategoryItemsChanged();
609        }
610
611        return hasChanged;
612    }
613
614    private PkgItem addNewItem(UpdateOp op, Package pkg, PkgState state) {
615        List<PkgCategory> cats = op.getCategories();
616        Object catKey = op.getCategoryKey(pkg);
617        PkgCategory cat = findCurrentCategory(cats, catKey);
618
619        if (cat == null) {
620            // This is a new category. Create it and add it to the list.
621            cat = op.createCategory(catKey);
622            synchronized (cats) {
623                cats.add(cat);
624            }
625            op.sortCategoryList();
626        } else {
627            // Not a new category. Give op a chance to adjust the category attributes
628            op.adjustCategory(cat, catKey);
629        }
630
631        PkgItem item = new PkgItem(pkg, state);
632        op.keep(item);
633        cat.getItems().add(item);
634        op.keep(cat);
635        return item;
636    }
637
638    private PkgCategory findCurrentCategory(
639            List<PkgCategory> currentCategories,
640            Object categoryKey) {
641        for (PkgCategory cat : currentCategories) {
642            if (cat.getKey().equals(categoryKey)) {
643                return cat;
644            }
645        }
646        return null;
647    }
648
649    /**
650     * {@link UpdateOp} describing the Sort-by-API operation.
651     */
652    private class UpdateOpApi extends UpdateOp {
653        @Override
654        public Object getCategoryKey(Package pkg) {
655            // Sort by API
656
657            if (pkg instanceof IAndroidVersionProvider) {
658                return ((IAndroidVersionProvider) pkg).getAndroidVersion();
659
660            } else if (pkg instanceof ToolPackage || pkg instanceof PlatformToolPackage) {
661                if (pkg.getRevision().isPreview()) {
662                    return PkgCategoryApi.KEY_TOOLS_PREVIEW;
663                } else {
664                    return PkgCategoryApi.KEY_TOOLS;
665                }
666            } else {
667                return PkgCategoryApi.KEY_EXTRA;
668            }
669        }
670
671        @Override
672        public void addDefaultCategories() {
673            boolean needTools = true;
674            boolean needExtras = true;
675
676            List<PkgCategory> cats = getCategories();
677            for (PkgCategory cat : cats) {
678                if (cat.getKey().equals(PkgCategoryApi.KEY_TOOLS)) {
679                    // Mark them as no unused to prevent their removal in updateEnd().
680                    keep(cat);
681                    needTools = false;
682                } else if (cat.getKey().equals(PkgCategoryApi.KEY_EXTRA)) {
683                    keep(cat);
684                    needExtras = false;
685                }
686            }
687
688            // Always add the tools & extras categories, even if empty (unlikely anyway)
689            if (needTools) {
690                PkgCategoryApi acat = new PkgCategoryApi(
691                   PkgCategoryApi.KEY_TOOLS,
692                   null,
693                   mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_OTHER));
694                synchronized (cats) {
695                    cats.add(acat);
696                }
697            }
698
699            if (needExtras) {
700                PkgCategoryApi acat = new PkgCategoryApi(
701                   PkgCategoryApi.KEY_EXTRA,
702                   null,
703                   mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_OTHER));
704                synchronized (cats) {
705                    cats.add(acat);
706                }
707            }
708        }
709
710        @Override
711        public PkgCategory createCategory(Object catKey) {
712            // Create API category.
713            PkgCategory cat = null;
714
715            assert catKey instanceof AndroidVersion;
716            AndroidVersion key = (AndroidVersion) catKey;
717
718            // We should not be trying to recreate the tools or extra categories.
719            assert !key.equals(PkgCategoryApi.KEY_TOOLS) && !key.equals(PkgCategoryApi.KEY_EXTRA);
720
721            // We need a label for the category.
722            // If we have an API level, try to get the info from the SDK Manager.
723            // If we don't (e.g. when installing a new platform that isn't yet available
724            // locally in the SDK Manager), it's OK we'll try to find the first platform
725            // package available.
726            String platformName = null;
727            for (IAndroidTarget target :
728                    mUpdaterData.getSdkManager().getTargets()) {
729                if (target.isPlatform() && key.equals(target.getVersion())) {
730                    platformName = target.getVersionName();
731                    break;
732                }
733            }
734
735            cat = new PkgCategoryApi(
736                key,
737                platformName,
738                mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_PLATFORM));
739
740            return cat;
741        }
742
743        @Override
744        public void adjustCategory(PkgCategory cat, Object catKey) {
745            // Pass. Nothing to do for API-sorted categories
746        }
747
748        @Override
749        public void sortCategoryList() {
750            // Sort the categories list.
751            // We always want categories in order tools..platforms..extras.
752            // For platform, we compare in descending order (o2-o1).
753            // This order is achieved by having the category keys ordered as
754            // needed for the sort to just do what we expect.
755
756            synchronized (getCategories()) {
757                Collections.sort(getCategories(), new Comparator<PkgCategory>() {
758                    @Override
759                    public int compare(PkgCategory cat1, PkgCategory cat2) {
760                        assert cat1 instanceof PkgCategoryApi;
761                        assert cat2 instanceof PkgCategoryApi;
762                        assert cat1.getKey() instanceof AndroidVersion;
763                        assert cat2.getKey() instanceof AndroidVersion;
764                        AndroidVersion v1 = (AndroidVersion) cat1.getKey();
765                        AndroidVersion v2 = (AndroidVersion) cat2.getKey();
766                        return v2.compareTo(v1);
767                    }
768                });
769            }
770        }
771
772        @Override
773        public void postCategoryItemsChanged() {
774            // Sort the items
775            for (PkgCategory cat : getCategories()) {
776                Collections.sort(cat.getItems());
777
778                // When sorting by API, we can't always get the platform name
779                // from the package manager. In this case at the very end we
780                // look for a potential platform package we can use to extract
781                // the platform version name (e.g. '1.5') from the first suitable
782                // platform package we can find.
783
784                assert cat instanceof PkgCategoryApi;
785                PkgCategoryApi pac = (PkgCategoryApi) cat;
786                if (pac.getPlatformName() == null) {
787                    // Check whether we can get the actual platform version name (e.g. "1.5")
788                    // from the first Platform package we find in this category.
789
790                    for (PkgItem item : cat.getItems()) {
791                        Package p = item.getMainPackage();
792                        if (p instanceof PlatformPackage) {
793                            String platformName = ((PlatformPackage) p).getVersionName();
794                            if (platformName != null) {
795                                pac.setPlatformName(platformName);
796                                break;
797                            }
798                        }
799                    }
800                }
801            }
802
803        }
804    }
805
806    /**
807     * {@link UpdateOp} describing the Sort-by-Source operation.
808     */
809    private class UpdateOpSource extends UpdateOp {
810
811        @Override
812        public boolean updateSourcePackages(SdkSource source, Package[] newPackages) {
813            // When displaying the repo by source, we want to create all the
814            // categories so that they can appear on the UI even if empty.
815            if (source != null) {
816                List<PkgCategory> cats = getCategories();
817                Object catKey = source;
818                PkgCategory cat = findCurrentCategory(cats, catKey);
819
820                if (cat == null) {
821                    // This is a new category. Create it and add it to the list.
822                    cat = createCategory(catKey);
823                    synchronized (cats) {
824                        cats.add(cat);
825                    }
826                    sortCategoryList();
827                }
828
829                keep(cat);
830            }
831
832            return super.updateSourcePackages(source, newPackages);
833        }
834
835        @Override
836        public Object getCategoryKey(Package pkg) {
837            // Sort by source
838            SdkSource source = pkg.getParentSource();
839            if (source == null) {
840                return PkgCategorySource.UNKNOWN_SOURCE;
841            }
842            return source;
843        }
844
845        @Override
846        public void addDefaultCategories() {
847            List<PkgCategory> cats = getCategories();
848            for (PkgCategory cat : cats) {
849                if (cat.getKey().equals(PkgCategorySource.UNKNOWN_SOURCE)) {
850                    // Already present.
851                    return;
852                }
853            }
854
855            // Always add the local categories, even if empty (unlikely anyway)
856            PkgCategorySource cat = new PkgCategorySource(
857                    PkgCategorySource.UNKNOWN_SOURCE,
858                    mUpdaterData);
859            // Mark it so that it can be cleared in updateEnd() if not used.
860            dontKeep(cat);
861            synchronized (cats) {
862                cats.add(cat);
863            }
864        }
865
866        /**
867         * Create a new source category.
868         * <p/>
869         * One issue is that local archives are processed first and we don't have the
870         * full source information on them (e.g. we know the referral URL but not
871         * the referral name of the site).
872         * In this case this will just create {@link PkgCategorySource} where the label isn't
873         * known yet.
874         */
875        @Override
876        public PkgCategory createCategory(Object catKey) {
877            assert catKey instanceof SdkSource;
878            PkgCategory cat = new PkgCategorySource((SdkSource) catKey, mUpdaterData);
879            return cat;
880        }
881
882        /**
883         * Checks whether the category needs to be adjust.
884         * As mentioned in {@link #createCategory(Object)}, local archives are processed
885         * first and result in a {@link PkgCategorySource} where the label isn't known.
886         * Once we process the external source with the actual name, we'll update it.
887         */
888        @Override
889        public void adjustCategory(PkgCategory cat, Object catKey) {
890            assert cat instanceof PkgCategorySource;
891            assert catKey instanceof SdkSource;
892            if (cat instanceof PkgCategorySource) {
893                ((PkgCategorySource) cat).adjustLabel((SdkSource) catKey);
894            }
895        }
896
897        @Override
898        public void sortCategoryList() {
899            // Sort the sources in ascending source name order,
900            // with the local packages always first.
901
902            synchronized (getCategories()) {
903                Collections.sort(getCategories(), new Comparator<PkgCategory>() {
904                    @Override
905                    public int compare(PkgCategory cat1, PkgCategory cat2) {
906                        assert cat1 instanceof PkgCategorySource;
907                        assert cat2 instanceof PkgCategorySource;
908
909                        SdkSource src1 = ((PkgCategorySource) cat1).getSource();
910                        SdkSource src2 = ((PkgCategorySource) cat2).getSource();
911
912                        if (src1 == src2) {
913                            return 0;
914                        } else if (src1 == PkgCategorySource.UNKNOWN_SOURCE) {
915                            return -1;
916                        } else if (src2 == PkgCategorySource.UNKNOWN_SOURCE) {
917                            return 1;
918                        }
919                        assert src1 != null; // true because LOCAL_SOURCE==null
920                        assert src2 != null;
921                        return src1.toString().compareTo(src2.toString());
922                    }
923                });
924            }
925        }
926
927        @Override
928        public void postCategoryItemsChanged() {
929            // Sort the items
930            for (PkgCategory cat : getCategories()) {
931                Collections.sort(cat.getItems());
932            }
933        }
934    }
935}
936