• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.sdkuilib.internal.repository.sdkman2;
18 
19 import com.android.sdklib.IAndroidTarget;
20 import com.android.sdklib.internal.repository.IPackageVersion;
21 import com.android.sdklib.internal.repository.Package;
22 import com.android.sdklib.internal.repository.PlatformPackage;
23 import com.android.sdklib.internal.repository.PlatformToolPackage;
24 import com.android.sdklib.internal.repository.SdkSource;
25 import com.android.sdklib.internal.repository.ToolPackage;
26 import com.android.sdklib.internal.repository.Package.UpdateInfo;
27 import com.android.sdkuilib.internal.repository.UpdaterData;
28 import com.android.sdkuilib.internal.repository.sdkman2.PkgItem.PkgState;
29 
30 import java.net.URL;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Collection;
34 import java.util.Collections;
35 import java.util.Comparator;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.Iterator;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Set;
42 
43 /**
44  * Helper class that separates the logic of package management from the UI
45  * so that we can test it using head-less unit tests.
46  */
47 class PackagesDiffLogic {
48     private final PackageLoader mPackageLoader;
49     private final UpdaterData mUpdaterData;
50     private boolean mFirstLoadComplete = true;
51 
PackagesDiffLogic(UpdaterData updaterData)52     public PackagesDiffLogic(UpdaterData updaterData) {
53         mUpdaterData = updaterData;
54         mPackageLoader = new PackageLoader(updaterData);
55     }
56 
getPackageLoader()57     public PackageLoader getPackageLoader() {
58         return mPackageLoader;
59     }
60 
61     /**
62      * Removes all the internal state and resets the object.
63      * Useful for testing.
64      */
clear()65     public void clear() {
66         mFirstLoadComplete = true;
67         mOpApi.clear();
68         mOpSource.clear();
69     }
70 
71     /** Return mFirstLoadComplete and resets it to false.
72      * All following calls will returns false. */
isFirstLoadComplete()73     public boolean isFirstLoadComplete() {
74         boolean b = mFirstLoadComplete;
75         mFirstLoadComplete = false;
76         return b;
77     }
78 
79     /**
80      * Mark all new and update PkgItems as checked.
81      * <p/>
82      * Try to be smart and check whether any platform is installed.
83      * The heuristic is:
84      * <ul>
85      * <li> For extras with no platform dependency, or for tools & platform-tools,
86      *          just select new and updates.
87      * <li> For anything that depends on a platform:
88      * <li> Always select the top platform and all its packages.
89      * <li> If some platform is partially installed, selected anything new/update for it.
90      * </ul>
91      */
checkNewUpdateItems(boolean selectNew, boolean selectUpdates)92     public void checkNewUpdateItems(boolean selectNew, boolean selectUpdates) {
93         int maxApi = 0;
94         Set<Integer> installedPlatforms = new HashSet<Integer>();
95         Map<Integer, List<PkgItem>> platformItems = new HashMap<Integer, List<PkgItem>>();
96 
97         // sort items in platforms... directly deal with items with no platform
98         for (PkgItem item : getAllPkgItems(true /*byApi*/, true /*bySource*/)) {
99             if (!item.hasCompatibleArchive()) {
100                 // Ignore items that have no archive compatible with the current platform.
101                 continue;
102             }
103 
104             // Get the main package's API level. We don't need to look at the updates
105             // since by definition they should target the same API level.
106             int api = 0;
107             Package p = item.getMainPackage();
108             if (p instanceof IPackageVersion) {
109                 api = ((IPackageVersion) p).getVersion().getApiLevel();
110             }
111 
112             if (api > 0) {
113                 maxApi = Math.max(maxApi, api);
114 
115                 // keep track of what platform is currently installed and its items
116                 if (item.getState() == PkgState.INSTALLED) {
117                     installedPlatforms.add(api);
118                 }
119                 List<PkgItem> items = platformItems.get(api);
120                 if (items == null) {
121                     platformItems.put(api, items = new ArrayList<PkgItem>());
122                 }
123                 items.add(item);
124             } else {
125                 // not a plaform package...
126                 if ((selectNew && item.getState() == PkgState.NEW) ||
127                         (selectUpdates && item.hasUpdatePkg())) {
128                     item.setChecked(true);
129                 }
130             }
131         }
132 
133         // If there are some platforms installed. Pickup anything new in them.
134         for (Integer api : installedPlatforms) {
135             List<PkgItem> items = platformItems.get(api);
136             if (items != null) {
137                 for (PkgItem item : items) {
138                     if ((selectNew && item.getState() == PkgState.NEW) ||
139                             (selectUpdates && item.hasUpdatePkg())) {
140                         item.setChecked(true);
141                     }
142                 }
143             }
144         }
145 
146         // Whether we have platforms installed or not, select everything from the top platform.
147         if (maxApi > 0) {
148             List<PkgItem> items = platformItems.get(maxApi);
149             if (items != null) {
150                 for (PkgItem item : items) {
151                     if ((selectNew && item.getState() == PkgState.NEW) ||
152                             (selectUpdates && item.hasUpdatePkg())) {
153                         item.setChecked(true);
154                     }
155                 }
156             }
157         }
158     }
159 
160     /**
161      * Mark all PkgItems as not checked.
162      */
uncheckAllItems()163     public void uncheckAllItems() {
164         for (PkgItem item : getAllPkgItems(true /*byApi*/, true /*bySource*/)) {
165             item.setChecked(false);
166         }
167     }
168 
169     /**
170      * An update operation, customized to either sort by API or sort by source.
171      */
172     abstract class UpdateOp {
173         private final Set<SdkSource> mVisitedSources = new HashSet<SdkSource>();
174         protected final List<PkgCategory> mCategories = new ArrayList<PkgCategory>();
175 
176         /** Removes all internal state. */
clear()177         public void clear() {
178             mVisitedSources.clear();
179             mCategories.clear();
180         }
181 
182         /** Retrieve the sorted category list. */
getCategories()183         public List<PkgCategory> getCategories() {
184             return mCategories;
185         }
186 
187         /** Retrieve the category key for the given package, either local or remote. */
getCategoryKey(Package pkg)188         public abstract Object getCategoryKey(Package pkg);
189 
190         /** Modified {@code currentCategories} to add default categories. */
addDefaultCategories()191         public abstract void addDefaultCategories();
192 
193         /** Creates the category for the given key and returns it. */
createCategory(Object catKey)194         public abstract PkgCategory createCategory(Object catKey);
195 
196         /** Sorts the category list (but not the items within the categories.) */
sortCategoryList()197         public abstract void sortCategoryList();
198 
199         /** Called after items of a given category have changed. Used to sort the
200          * items and/or adjust the category name. */
postCategoryItemsChanged()201         public abstract void postCategoryItemsChanged();
202 
203         /** Add the new package or merge it as an update or does nothing if this package
204          * is already part of the category items.
205          * Returns true if the category item list has changed. */
mergeNewPackage(Package newPackage, PkgCategory cat)206         public abstract boolean mergeNewPackage(Package newPackage, PkgCategory cat);
207 
updateStart()208         public void updateStart() {
209             mVisitedSources.clear();
210 
211             // Note that default categories are created after the unused ones so that
212             // the callback can decide whether they should be marked as unused or not.
213             for (PkgCategory cat : mCategories) {
214                 cat.setUnused(true);
215             }
216 
217             addDefaultCategories();
218         }
219 
updateSourcePackages(SdkSource source, Package[] newPackages)220         public boolean updateSourcePackages(SdkSource source, Package[] newPackages) {
221             if (newPackages.length > 0) {
222                 mVisitedSources.add(source);
223             }
224             if (source == null) {
225                 return processLocals(this, newPackages);
226             } else {
227                 return processSource(this, source, newPackages);
228             }
229         }
230 
updateEnd()231         public boolean updateEnd() {
232             boolean hasChanged = false;
233 
234             // Remove unused categories
235             synchronized (mCategories) {
236                 for (Iterator<PkgCategory> catIt = mCategories.iterator(); catIt.hasNext(); ) {
237                     PkgCategory cat = catIt.next();
238                     if (cat.isUnused()) {
239                         catIt.remove();
240                         hasChanged  = true;
241                         continue;
242                     }
243 
244                     // Remove all *remote* items which obsolete source we have not been visited.
245                     // This detects packages which have disappeared from a remote source during an
246                     // update and removes from the current list.
247                     // Locally installed item are never removed.
248                     for (Iterator<PkgItem> itemIt = cat.getItems().iterator();
249                             itemIt.hasNext(); ) {
250                         PkgItem item = itemIt.next();
251                         if (item.getState() == PkgState.NEW &&
252                                 !mVisitedSources.contains(item.getSource())) {
253                             itemIt.remove();
254                             hasChanged  = true;
255                         }
256                     }
257                 }
258             }
259             return hasChanged;
260         }
261 
262     }
263 
264     private final UpdateOpApi    mOpApi    = new UpdateOpApi();
265     private final UpdateOpSource mOpSource = new UpdateOpSource();
266 
getCategories(boolean displayIsSortByApi)267     public List<PkgCategory> getCategories(boolean displayIsSortByApi) {
268         return displayIsSortByApi ? mOpApi.getCategories() : mOpSource.getCategories();
269     }
270 
getAllPkgItems(boolean byApi, boolean bySource)271     public List<PkgItem> getAllPkgItems(boolean byApi, boolean bySource) {
272         List<PkgItem> items = new ArrayList<PkgItem>();
273 
274         if (byApi) {
275             List<PkgCategory> cats = getCategories(true /*displayIsSortByApi*/);
276             synchronized (cats) {
277                 for (PkgCategory cat : cats) {
278                     items.addAll(cat.getItems());
279                 }
280             }
281         }
282 
283         if (bySource) {
284             List<PkgCategory> cats = getCategories(false /*displayIsSortByApi*/);
285             synchronized (cats) {
286                 for (PkgCategory cat : cats) {
287                     items.addAll(cat.getItems());
288                 }
289             }
290         }
291 
292         return items;
293     }
294 
updateStart()295     public void updateStart() {
296         mOpApi.updateStart();
297         mOpSource.updateStart();
298     }
299 
updateSourcePackages( boolean displayIsSortByApi, SdkSource source, Package[] newPackages)300     public boolean updateSourcePackages(
301             boolean displayIsSortByApi,
302             SdkSource source,
303             Package[] newPackages) {
304 
305         boolean apiListChanged = mOpApi.updateSourcePackages(source, newPackages);
306         boolean sourceListChanged = mOpSource.updateSourcePackages(source, newPackages);
307         return displayIsSortByApi ? apiListChanged : sourceListChanged;
308     }
309 
updateEnd(boolean displayIsSortByApi)310     public boolean updateEnd(boolean displayIsSortByApi) {
311         boolean apiListChanged = mOpApi.updateEnd();
312         boolean sourceListChanged = mOpSource.updateEnd();
313         return displayIsSortByApi ? apiListChanged : sourceListChanged;
314     }
315 
316     /** Process all local packages. Returns true if something changed. */
processLocals(UpdateOp op, Package[] packages)317     private boolean processLocals(UpdateOp op, Package[] packages) {
318         boolean hasChanged = false;
319         Set<Package> newPackages = new HashSet<Package>(Arrays.asList(packages));
320         Set<Package> unusedPackages = new HashSet<Package>(newPackages);
321 
322         assert newPackages.size() == packages.length;
323 
324         // Upgrade NEW items to INSTALLED for any local package we already know about.
325         // We can't just change the state of the NEW item to INSTALLED, we also need its
326         // installed package/archive information and so we swap them in-place in the items list.
327 
328         for (PkgCategory cat : op.getCategories()) {
329             List<PkgItem> items = cat.getItems();
330             for (int i = 0; i < items.size(); i++) {
331                 PkgItem item = items.get(i);
332 
333                 if (item.hasUpdatePkg()) {
334                     Package newPkg = setContainsLocalPackage(newPackages, item.getUpdatePkg());
335                     if (newPkg != null) {
336                         // This item has an update package that is now installed.
337                         PkgItem installed = new PkgItem(newPkg, PkgState.INSTALLED);
338                         removePackageFromSet(unusedPackages, newPkg);
339                         item.removeUpdate();
340                         items.add(installed);
341                         cat.setUnused(false);
342                         hasChanged = true;
343                     }
344                 }
345 
346                 Package newPkg = setContainsLocalPackage(newPackages, item.getMainPackage());
347                 if (newPkg != null) {
348                     removePackageFromSet(unusedPackages, newPkg);
349                     if (item.getState() == PkgState.NEW) {
350                         // This item has a main package that is now installed.
351                         replace(items, i, new PkgItem(newPkg, PkgState.INSTALLED));
352                         cat.setUnused(false);
353                         hasChanged = true;
354                     }
355                 }
356             }
357         }
358 
359         // Remove INSTALLED items if their package isn't listed anymore in locals
360         for (PkgCategory cat : op.getCategories()) {
361             List<PkgItem> items = cat.getItems();
362             for (int i = 0; i < items.size(); i++) {
363                 PkgItem item = items.get(i);
364 
365                 if (item.getState() == PkgState.INSTALLED) {
366                     Package newPkg = setContainsLocalPackage(newPackages, item.getMainPackage());
367                     if (newPkg == null) {
368                         items.remove(i--);
369                         hasChanged = true;
370                     }
371                 }
372             }
373         }
374 
375         // Create new 'installed' items for any local package we haven't processed yet
376         for (Package newPackage : unusedPackages) {
377             Object catKey = op.getCategoryKey(newPackage);
378             PkgCategory cat = findCurrentCategory(op.getCategories(), catKey);
379 
380             if (cat == null) {
381                 // This is a new category. Create it and add it to the list.
382                 cat = op.createCategory(catKey);
383                 op.getCategories().add(cat);
384                 op.sortCategoryList();
385             }
386 
387             cat.getItems().add(new PkgItem(newPackage, PkgState.INSTALLED));
388             cat.setUnused(false);
389             hasChanged = true;
390         }
391 
392         if (hasChanged) {
393             op.postCategoryItemsChanged();
394         }
395 
396         return hasChanged;
397     }
398 
399     /**
400      * Replaces the item at {@code index} in {@code list} with the new {@code obj} element.
401      * This uses {@link ArrayList#set(int, Object)} if possible, remove+add otherwise.
402      *
403      * @return The old item at the same index position.
404      * @throws IndexOutOfBoundsException if index out of range (index < 0 || index >= size()).
405      */
replace(List<T> list, int index, T obj)406     private <T> T replace(List<T> list, int index, T obj) {
407         if (list instanceof ArrayList<?>) {
408             return ((ArrayList<T>) list).set(index, obj);
409         } else {
410             T old = list.remove(index);
411             list.add(index, obj);
412             return old;
413         }
414     }
415 
416     /**
417      * Checks whether the {@code newPackages} set contains a package that is the
418      * same as {@code pkgToFind}.
419      * This is based on Package being the same from an install point of view rather than
420      * pure object equality.
421      * @return The matching package from the {@code newPackages} set or null if not found.
422      */
setContainsLocalPackage(Collection<Package> newPackages, Package pkgToFind)423     private Package setContainsLocalPackage(Collection<Package> newPackages, Package pkgToFind) {
424         // Most of the time, local packages don't have the exact same hash code
425         // as new ones since the objects are similar but not exactly the same,
426         // for example their installed OS path cannot match (by definition) so
427         // their hash code do not match when used with Set.contains().
428 
429         for (Package newPkg : newPackages) {
430             // Two packages are the same if they are compatible types,
431             // do not update each other and have the same revision number.
432             if (pkgToFind.canBeUpdatedBy(newPkg) == UpdateInfo.NOT_UPDATE &&
433                     newPkg.getRevision() == pkgToFind.getRevision()) {
434                 return newPkg;
435             }
436         }
437 
438         return null;
439     }
440 
441     /**
442      * Removes the given package from the set.
443      * This is based on Package being the same from an install point of view rather than
444      * pure object equality.
445      */
removePackageFromSet(Collection<Package> packages, Package pkgToFind)446     private void removePackageFromSet(Collection<Package> packages, Package pkgToFind) {
447         // First try to remove the package based on its hash code. This can fail
448         // for a variety of reasons, as explained in setContainsLocalPackage().
449         if (packages.remove(pkgToFind)) {
450             return;
451         }
452 
453         for (Package pkg : packages) {
454             // Two packages are the same if they are compatible types,
455             // or not updates of each other and have the same revision number.
456             if (pkgToFind.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE &&
457                     pkg.getRevision() == pkgToFind.getRevision()) {
458                 packages.remove(pkg);
459                 // Implementation detail: we can get away with using Collection.remove()
460                 // whilst in the for iterator because we return right away (otherwise the
461                 // iterator would complain the collection just changed.)
462                 return;
463             }
464         }
465     }
466 
467     /**
468      * Removes any package from the set that is equal or lesser than {@code pkgToFind}.
469      * This is based on Package being the same from an install point of view rather than
470      * pure object equality.
471      * </p>
472      * This is a slight variation on {@link #removePackageFromSet(Collection, Package)}
473      * where we remove from the set any package that is similar to {@code pkgToFind}
474      * and has either the same revision number or a <em>lesser</em> revision number.
475      * An example of this use-case is there's an installed local package in rev 5
476      * (that is the pkgToFind) and there's a remote package in rev 3 (in the package list),
477      * in which case we 'forget' the rev 3 package even exists.
478      */
removePackageOrLesserFromSet(Collection<Package> packages, Package pkgToFind)479     private void removePackageOrLesserFromSet(Collection<Package> packages, Package pkgToFind) {
480         for (Iterator<Package> it = packages.iterator(); it.hasNext(); ) {
481             Package pkg = it.next();
482 
483             // Two packages are the same if they are compatible types,
484             // or not updates of each other and have the same revision number.
485             if (pkgToFind.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE &&
486                     pkg.getRevision() <= pkgToFind.getRevision()) {
487                 it.remove();
488             }
489         }
490     }
491 
492     /** Process all remote packages. Returns true if something changed. */
processSource(UpdateOp op, SdkSource source, Package[] packages)493     private boolean processSource(UpdateOp op, SdkSource source, Package[] packages) {
494         boolean hasChanged = false;
495         // Note: unusedPackages must respect the original packages order. It can't be a set.
496         List<Package> unusedPackages = new ArrayList<Package>(Arrays.asList(packages));
497         Set<Package> newPackages = new HashSet<Package>(unusedPackages);
498 
499         assert source != null;
500         assert newPackages.size() == packages.length;
501 
502         // Remove any items or updates that are no longer in the source's packages
503         for (PkgCategory cat : op.getCategories()) {
504             List<PkgItem> items = cat.getItems();
505             for (int i = 0; i < items.size(); i++) {
506                 PkgItem item = items.get(i);
507 
508                 if (!isSourceCompatible(item, source)) {
509                     continue;
510                 }
511 
512                 // Try to prune current items that are no longer on the remote site.
513                 // Installed items have been dealt with the local source, so only
514                 // change new items here.
515                 if (item.getState() == PkgState.NEW) {
516                     Package newPkg = setContainsLocalPackage(newPackages, item.getMainPackage());
517                     if (newPkg == null) {
518                         // This package is no longer part of the source.
519                         items.remove(i--);
520                         hasChanged = true;
521                         continue;
522                     }
523                 }
524 
525                 cat.setUnused(false);
526                 removePackageOrLesserFromSet(unusedPackages, item.getMainPackage());
527 
528                 if (item.hasUpdatePkg()) {
529                     Package newPkg = setContainsLocalPackage(newPackages, item.getUpdatePkg());
530                     if (newPkg != null) {
531                         removePackageFromSet(unusedPackages, newPkg);
532                     } else {
533                         // This update is no longer part of the source
534                         item.removeUpdate();
535                         hasChanged = true;
536                     }
537                 }
538             }
539         }
540 
541         // Add any new unknown packages
542         for (Package newPackage : unusedPackages) {
543             Object catKey = op.getCategoryKey(newPackage);
544             PkgCategory cat = findCurrentCategory(op.getCategories(), catKey);
545 
546             if (cat == null) {
547                 // This is a new category. Create it and add it to the list.
548                 cat = op.createCategory(catKey);
549                 op.getCategories().add(cat);
550                 op.sortCategoryList();
551             }
552 
553             // Add the new package or merge it as an update
554             hasChanged |= op.mergeNewPackage(newPackage, cat);
555         }
556 
557         if (hasChanged) {
558             op.postCategoryItemsChanged();
559         }
560 
561         return hasChanged;
562     }
563 
isSourceCompatible(PkgItem currentItem, SdkSource newItemSource)564     private boolean isSourceCompatible(PkgItem currentItem, SdkSource newItemSource) {
565         SdkSource currentSource = currentItem.getSource();
566 
567         // Only process items matching the current source.
568         if (currentSource == newItemSource) {
569             // Object identity, so definitely the same source. Accept it.
570             return true;
571 
572         } else if (currentSource != null && newItemSource != null &&
573                 !currentSource.getClass().equals(newItemSource.getClass())) {
574             // Both sources don't have the same type (e.g. sdk repository versus add-on repository)
575             return false;
576 
577         } else if (currentSource != null && currentSource.equals(newItemSource)) {
578             // Same source. Accept it.
579             return true;
580 
581         } else if (currentSource == null && currentItem.getState() == PkgState.INSTALLED) {
582             // Accept it.
583             // If a locally installed item has no source, it probably has been
584             // manually installed. In this case just match any remote source.
585             return true;
586 
587         } else if (currentSource != null && currentSource.getUrl().startsWith("file://")) {
588             // Heuristic: Probably a manual local install. Accept it.
589             return true;
590 
591         } else {
592             // Reject the source mismatch. The idea is that if two remote repositories
593             // have similar packages, we don't want to merge them together and have
594             // one hide the other. This is a design error from the repository owners
595             // and we want the case to be blatant so that we can get it fixed.
596 
597             if (currentSource != null && newItemSource != null) {
598                 try {
599                     URL url1 = new URL(currentSource.getUrl());
600                     URL url2 = new URL(newItemSource.getUrl());
601 
602                     // Make an exception if both URLs have the same host name & domain name.
603                     if (url1.sameFile(url2) || url1.getHost().equals(url2.getHost())) {
604                         return true;
605                     }
606                 } catch (Exception ignore) {
607                     // Ignore MalformedURLException or other exceptions
608                 }
609             }
610 
611             return false;
612         }
613     }
614 
findCurrentCategory( List<PkgCategory> currentCategories, Object categoryKey)615     private PkgCategory findCurrentCategory(
616             List<PkgCategory> currentCategories,
617             Object categoryKey) {
618         for (PkgCategory cat : currentCategories) {
619             if (cat.getKey().equals(categoryKey)) {
620                 return cat;
621             }
622         }
623         return null;
624     }
625 
626     /**
627      * {@link UpdateOp} describing the Sort-by-API operation.
628      */
629     private class UpdateOpApi extends UpdateOp {
630         @Override
getCategoryKey(Package pkg)631         public Object getCategoryKey(Package pkg) {
632             // Sort by API
633 
634             if (pkg instanceof IPackageVersion) {
635                 return ((IPackageVersion) pkg).getVersion().getApiLevel();
636 
637             } else if (pkg instanceof ToolPackage || pkg instanceof PlatformToolPackage) {
638                 return PkgCategoryApi.KEY_TOOLS;
639 
640             } else {
641                 return PkgCategoryApi.KEY_EXTRA;
642             }
643         }
644 
645         @Override
addDefaultCategories()646         public void addDefaultCategories() {
647             boolean needTools = true;
648             boolean needExtras = true;
649 
650             for (PkgCategory cat : mCategories) {
651                 if (cat.getKey().equals(PkgCategoryApi.KEY_TOOLS)) {
652                     // Mark them as no unused to prevent their removal in updateEnd().
653                     cat.setUnused(false);
654                     needTools = false;
655                 } else if (cat.getKey().equals(PkgCategoryApi.KEY_EXTRA)) {
656                     cat.setUnused(false);
657                     needExtras = false;
658                 }
659             }
660 
661             // Always add the tools & extras categories, even if empty (unlikely anyway)
662             if (needTools) {
663                 PkgCategoryApi acat = new PkgCategoryApi(
664                         PkgCategoryApi.KEY_TOOLS,
665                         null,
666                         mUpdaterData.getImageFactory().getImageByName(PackagesPage.ICON_CAT_OTHER));
667                 synchronized (mCategories) {
668                     mCategories.add(acat);
669                 }
670             }
671 
672             if (needExtras) {
673                 PkgCategoryApi acat = new PkgCategoryApi(
674                         PkgCategoryApi.KEY_EXTRA,
675                         null,
676                         mUpdaterData.getImageFactory().getImageByName(PackagesPage.ICON_CAT_OTHER));
677                 synchronized (mCategories) {
678                     mCategories.add(acat);
679                 }
680             }
681         }
682 
683         @Override
createCategory(Object catKey)684         public PkgCategory createCategory(Object catKey) {
685             // Create API category.
686             PkgCategory cat = null;
687 
688             assert catKey instanceof Integer;
689             int apiKey = ((Integer) catKey).intValue();
690 
691             // We need a label for the category.
692             // If we have an API level, try to get the info from the SDK Manager.
693             // If we don't (e.g. when installing a new platform that isn't yet available
694             // locally in the SDK Manager), it's OK we'll try to find the first platform
695             // package available.
696             String platformName = null;
697             if (apiKey >= 1 && apiKey != PkgCategoryApi.KEY_TOOLS) {
698                 for (IAndroidTarget target :
699                         mUpdaterData.getSdkManager().getTargets()) {
700                     if (target.isPlatform() &&
701                             target.getVersion().getApiLevel() == apiKey) {
702                         platformName = target.getVersionName();
703                         break;
704                     }
705                 }
706             }
707 
708             cat = new PkgCategoryApi(
709                     apiKey,
710                     platformName,
711                     mUpdaterData.getImageFactory().getImageByName(PackagesPage.ICON_CAT_PLATFORM));
712 
713             return cat;
714         }
715 
716         @Override
mergeNewPackage(Package newPackage, PkgCategory cat)717         public boolean mergeNewPackage(Package newPackage, PkgCategory cat) {
718             // First check if the new package could be an update
719             // to an existing package
720             for (PkgItem item : cat.getItems()) {
721                 if (!isSourceCompatible(item, newPackage.getParentSource())) {
722                     continue;
723                 }
724 
725                 if (item.isSameMainPackageAs(newPackage)) {
726                     // Seems like this isn't really a new item after all.
727                     cat.setUnused(false);
728                     // Return false since we're not changing anything.
729                     return false;
730                 } else if (item.mergeUpdate(newPackage)) {
731                     // The new package is an update for the existing package
732                     // and has been merged in the PkgItem as such.
733                     cat.setUnused(false);
734                     // Return true to indicate we changed something.
735                     return true;
736                 }
737             }
738 
739             // This is truly a new item.
740             cat.getItems().add(new PkgItem(newPackage, PkgState.NEW));
741             cat.setUnused(false);
742             return true; // something has changed
743         }
744 
745         @Override
sortCategoryList()746         public void sortCategoryList() {
747             // Sort the categories list.
748             // We always want categories in order tools..platforms..extras.
749             // For platform, we compare in descending order (o2-o1).
750             // This order is achieved by having the category keys ordered as
751             // needed for the sort to just do what we expect.
752 
753             synchronized (mCategories) {
754                 Collections.sort(mCategories, new Comparator<PkgCategory>() {
755                     public int compare(PkgCategory cat1, PkgCategory cat2) {
756                         assert cat1 instanceof PkgCategoryApi;
757                         assert cat2 instanceof PkgCategoryApi;
758                         int api1 = ((Integer) cat1.getKey()).intValue();
759                         int api2 = ((Integer) cat2.getKey()).intValue();
760                         return api2 - api1;
761                     }
762                 });
763             }
764         }
765 
766         @Override
postCategoryItemsChanged()767         public void postCategoryItemsChanged() {
768             // Sort the items
769             for (PkgCategory cat : mCategories) {
770                 Collections.sort(cat.getItems());
771 
772                 // When sorting by API, we can't always get the platform name
773                 // from the package manager. In this case at the very end we
774                 // look for a potential platform package we can use to extract
775                 // the platform version name (e.g. '1.5') from the first suitable
776                 // platform package we can find.
777 
778                 assert cat instanceof PkgCategoryApi;
779                 PkgCategoryApi pac = (PkgCategoryApi) cat;
780                 if (pac.getPlatformName() == null) {
781                     // Check whether we can get the actual platform version name (e.g. "1.5")
782                     // from the first Platform package we find in this category.
783 
784                     for (PkgItem item : cat.getItems()) {
785                         Package p = item.getMainPackage();
786                         if (p instanceof PlatformPackage) {
787                             String platformName = ((PlatformPackage) p).getVersionName();
788                             if (platformName != null) {
789                                 pac.setPlatformName(platformName);
790                                 break;
791                             }
792                         }
793                     }
794                 }
795             }
796 
797         }
798     }
799 
800     /**
801      * {@link UpdateOp} describing the Sort-by-Source operation.
802      */
803     private class UpdateOpSource extends UpdateOp {
804         @Override
getCategoryKey(Package pkg)805         public Object getCategoryKey(Package pkg) {
806             // Sort by source
807             SdkSource source = pkg.getParentSource();
808             if (source == null) {
809                 return PkgCategorySource.UNKNOWN_SOURCE;
810             }
811             return source;
812         }
813 
814         @Override
addDefaultCategories()815         public void addDefaultCategories() {
816             for (PkgCategory cat : mCategories) {
817                 if (cat.getKey().equals(PkgCategorySource.UNKNOWN_SOURCE)) {
818                     // Already present.
819                     return;
820                 }
821             }
822 
823             // Always add the local categories, even if empty (unlikely anyway)
824             PkgCategorySource cat = new PkgCategorySource(
825                     PkgCategorySource.UNKNOWN_SOURCE,
826                     mUpdaterData);
827             // Mark it as unused so that it can be cleared in updateEnd() if not used.
828             cat.setUnused(true);
829             synchronized (mCategories) {
830                 mCategories.add(cat);
831             }
832         }
833 
834         @Override
createCategory(Object catKey)835         public PkgCategory createCategory(Object catKey) {
836             assert catKey instanceof SdkSource;
837             PkgCategory cat = new PkgCategorySource((SdkSource) catKey, mUpdaterData);
838             return cat;
839 
840         }
841 
842         @Override
mergeNewPackage(Package newPackage, PkgCategory cat)843         public boolean mergeNewPackage(Package newPackage, PkgCategory cat) {
844             // First check if the new package could be an update
845             // to an existing package
846             for (PkgItem item : cat.getItems()) {
847                 if (item.isSameMainPackageAs(newPackage)) {
848                     // Seems like this isn't really a new item after all.
849                     cat.setUnused(false);
850                     // Return false since we're not changing anything.
851                     return false;
852                 } else if (item.mergeUpdate(newPackage)) {
853                     // The new package is an update for the existing package
854                     // and has been merged in the PkgItem as such.
855                     cat.setUnused(false);
856                     // Return true to indicate we changed something.
857                     return true;
858                 }
859             }
860 
861             // This is truly a new item.
862             cat.getItems().add(new PkgItem(newPackage, PkgState.NEW));
863             cat.setUnused(false);
864             return true; // something has changed
865         }
866 
867         @Override
sortCategoryList()868         public void sortCategoryList() {
869             // Sort the sources in ascending source name order,
870             // with the local packages always first.
871 
872             synchronized (mCategories) {
873                 Collections.sort(mCategories, new Comparator<PkgCategory>() {
874                     public int compare(PkgCategory cat1, PkgCategory cat2) {
875                         assert cat1 instanceof PkgCategorySource;
876                         assert cat2 instanceof PkgCategorySource;
877 
878                         SdkSource src1 = ((PkgCategorySource) cat1).getSource();
879                         SdkSource src2 = ((PkgCategorySource) cat2).getSource();
880 
881                         if (src1 == src2) {
882                             return 0;
883                         } else if (src1 == PkgCategorySource.UNKNOWN_SOURCE) {
884                             return -1;
885                         } else if (src2 == PkgCategorySource.UNKNOWN_SOURCE) {
886                             return 1;
887                         }
888                         assert src1 != null; // true because LOCAL_SOURCE==null
889                         assert src2 != null;
890                         return src1.toString().compareTo(src2.toString());
891                     }
892                 });
893             }
894         }
895 
896         @Override
postCategoryItemsChanged()897         public void postCategoryItemsChanged() {
898             // Sort the items
899             for (PkgCategory cat : mCategories) {
900                 Collections.sort(cat.getItems());
901             }
902         }
903     }
904 }
905