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