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