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