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