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