1 /* 2 * Copyright (C) 2009 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; 18 19 import com.android.SdkConstants; 20 import com.android.annotations.VisibleForTesting; 21 import com.android.annotations.VisibleForTesting.Visibility; 22 import com.android.prefs.AndroidLocation.AndroidLocationException; 23 import com.android.sdklib.SdkManager; 24 import com.android.sdklib.internal.avd.AvdManager; 25 import com.android.sdklib.internal.repository.AdbWrapper; 26 import com.android.sdklib.internal.repository.DownloadCache; 27 import com.android.sdklib.internal.repository.ITask; 28 import com.android.sdklib.internal.repository.ITaskFactory; 29 import com.android.sdklib.internal.repository.ITaskMonitor; 30 import com.android.sdklib.internal.repository.LocalSdkParser; 31 import com.android.sdklib.internal.repository.NullTaskMonitor; 32 import com.android.sdklib.internal.repository.archives.Archive; 33 import com.android.sdklib.internal.repository.archives.ArchiveInstaller; 34 import com.android.sdklib.internal.repository.packages.AddonPackage; 35 import com.android.sdklib.internal.repository.packages.Package; 36 import com.android.sdklib.internal.repository.packages.PlatformToolPackage; 37 import com.android.sdklib.internal.repository.packages.ToolPackage; 38 import com.android.sdklib.internal.repository.sources.SdkRepoSource; 39 import com.android.sdklib.internal.repository.sources.SdkSource; 40 import com.android.sdklib.internal.repository.sources.SdkSourceCategory; 41 import com.android.sdklib.internal.repository.sources.SdkSources; 42 import com.android.sdklib.repository.SdkAddonConstants; 43 import com.android.sdklib.repository.SdkRepoConstants; 44 import com.android.sdklib.util.LineUtil; 45 import com.android.sdklib.util.SparseIntArray; 46 import com.android.sdkuilib.internal.repository.SettingsController.OnChangedListener; 47 import com.android.sdkuilib.internal.repository.core.PackageLoader; 48 import com.android.sdkuilib.internal.repository.icons.ImageFactory; 49 import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2; 50 import com.android.sdkuilib.repository.ISdkChangeListener; 51 import com.android.utils.ILogger; 52 53 import org.eclipse.jface.dialogs.MessageDialog; 54 import org.eclipse.swt.widgets.Shell; 55 56 import java.io.ByteArrayOutputStream; 57 import java.io.PrintStream; 58 import java.util.ArrayList; 59 import java.util.Collection; 60 import java.util.Collections; 61 import java.util.Comparator; 62 import java.util.HashMap; 63 import java.util.HashSet; 64 import java.util.Iterator; 65 import java.util.List; 66 import java.util.Map; 67 import java.util.Set; 68 69 /** 70 * Data shared between {@link SdkUpdaterWindowImpl2} and its pages. 71 */ 72 public class UpdaterData implements IUpdaterData { 73 74 public static final int NO_TOOLS_MSG = 0; 75 public static final int TOOLS_MSG_UPDATED_FROM_ADT = 1; 76 public static final int TOOLS_MSG_UPDATED_FROM_SDKMAN = 2; 77 78 private String mOsSdkRoot; 79 80 private final LocalSdkParser mLocalSdkParser = new LocalSdkParser(); 81 /** Holds all sources. Do not use this directly. 82 * Instead use {@link #getSources()} so that unit tests can override this as needed. */ 83 private final SdkSources mSources = new SdkSources(); 84 /** Holds settings. Do not use this directly. 85 * Instead use {@link #getSettingsController()} so that unit tests can override this. */ 86 private final SettingsController mSettingsController; 87 private final ArrayList<ISdkChangeListener> mListeners = new ArrayList<ISdkChangeListener>(); 88 private final ILogger mSdkLog; 89 private ITaskFactory mTaskFactory; 90 private Shell mWindowShell; 91 private SdkManager mSdkManager; 92 private AvdManager mAvdManager; 93 /** 94 * The current {@link PackageLoader} to use. 95 * Lazily created in {@link #getPackageLoader()}. 96 */ 97 private PackageLoader mPackageLoader; 98 /** 99 * The current {@link DownloadCache} to use. 100 * Lazily created in {@link #getDownloadCache()}. 101 */ 102 private DownloadCache mDownloadCache; 103 /** 104 * The current {@link ImageFactory}. 105 * Set via {@link #setImageFactory(ImageFactory)} by the window implementation. 106 * It is null when invoked using the command-line interface. 107 */ 108 private ImageFactory mImageFactory; 109 private AndroidLocationException mAvdManagerInitError; 110 111 /** 112 * Creates a new updater data. 113 * 114 * @param sdkLog Logger. Cannot be null. 115 * @param osSdkRoot The OS path to the SDK root. 116 */ 117 public UpdaterData(String osSdkRoot, ILogger sdkLog) { 118 mOsSdkRoot = osSdkRoot; 119 mSdkLog = sdkLog; 120 121 122 mSettingsController = initSettingsController(); 123 initSdk(); 124 } 125 126 // ----- getters, setters ---- 127 128 public String getOsSdkRoot() { 129 return mOsSdkRoot; 130 } 131 132 @Override 133 public DownloadCache getDownloadCache() { 134 if (mDownloadCache == null) { 135 mDownloadCache = new DownloadCache( 136 getSettingsController().getSettings().getUseDownloadCache() ? 137 DownloadCache.Strategy.FRESH_CACHE : 138 DownloadCache.Strategy.DIRECT); 139 } 140 return mDownloadCache; 141 } 142 143 public void setTaskFactory(ITaskFactory taskFactory) { 144 mTaskFactory = taskFactory; 145 } 146 147 @Override 148 public ITaskFactory getTaskFactory() { 149 return mTaskFactory; 150 } 151 152 public SdkSources getSources() { 153 return mSources; 154 } 155 156 public LocalSdkParser getLocalSdkParser() { 157 return mLocalSdkParser; 158 } 159 160 @Override 161 public ILogger getSdkLog() { 162 return mSdkLog; 163 } 164 165 public void setImageFactory(ImageFactory imageFactory) { 166 mImageFactory = imageFactory; 167 } 168 169 @Override 170 public ImageFactory getImageFactory() { 171 return mImageFactory; 172 } 173 174 @Override 175 public SdkManager getSdkManager() { 176 return mSdkManager; 177 } 178 179 @Override 180 public AvdManager getAvdManager() { 181 return mAvdManager; 182 } 183 184 @Override 185 public SettingsController getSettingsController() { 186 return mSettingsController; 187 } 188 189 /** Adds a listener ({@link ISdkChangeListener}) that is notified when the SDK is reloaded. */ 190 public void addListeners(ISdkChangeListener listener) { 191 if (mListeners.contains(listener) == false) { 192 mListeners.add(listener); 193 } 194 } 195 196 /** Removes a listener ({@link ISdkChangeListener}) that is notified when the SDK is reloaded. */ 197 public void removeListener(ISdkChangeListener listener) { 198 mListeners.remove(listener); 199 } 200 201 public void setWindowShell(Shell windowShell) { 202 mWindowShell = windowShell; 203 } 204 205 @Override 206 public Shell getWindowShell() { 207 return mWindowShell; 208 } 209 210 public PackageLoader getPackageLoader() { 211 // The package loader is lazily initialized here. 212 if (mPackageLoader == null) { 213 mPackageLoader = new PackageLoader(this); 214 } 215 return mPackageLoader; 216 } 217 218 /** 219 * Check if any error occurred during initialization. 220 * If it did, display an error message. 221 * 222 * @return True if an error occurred, false if we should continue. 223 */ 224 public boolean checkIfInitFailed() { 225 if (mAvdManagerInitError != null) { 226 String example; 227 if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { 228 example = "%USERPROFILE%"; //$NON-NLS-1$ 229 } else { 230 example = "~"; //$NON-NLS-1$ 231 } 232 233 String error = String.format( 234 "The AVD manager normally uses the user's profile directory to store " + 235 "AVD files. However it failed to find the default profile directory. " + 236 "\n" + 237 "To fix this, please set the environment variable ANDROID_SDK_HOME to " + 238 "a valid path such as \"%s\".", 239 example); 240 241 // We may not have any UI. Only display a dialog if there's a window shell available. 242 if (mWindowShell != null && !mWindowShell.isDisposed()) { 243 MessageDialog.openError(mWindowShell, 244 "Android Virtual Devices Manager", 245 error); 246 } else { 247 mSdkLog.error(null /* Throwable */, "%s", error); //$NON-NLS-1$ 248 } 249 250 return true; 251 } 252 return false; 253 } 254 255 // ----- 256 257 /** 258 * Initializes the {@link SdkManager} and the {@link AvdManager}. 259 * Extracted so that we can override this in unit tests. 260 */ 261 @VisibleForTesting(visibility=Visibility.PRIVATE) 262 protected void initSdk() { 263 setSdkManager(SdkManager.createManager(mOsSdkRoot, mSdkLog)); 264 try { 265 mAvdManager = null; 266 mAvdManager = AvdManager.getInstance(mSdkManager, mSdkLog); 267 } catch (AndroidLocationException e) { 268 mSdkLog.error(e, "Unable to read AVDs: " + e.getMessage()); //$NON-NLS-1$ 269 270 // Note: we used to continue here, but the thing is that 271 // mAvdManager==null so nothing is really going to work as 272 // expected. Let's just display an error later in checkIfInitFailed() 273 // and abort right there. This step is just too early in the SWT 274 // setup process to display a message box yet. 275 276 mAvdManagerInitError = e; 277 } 278 279 // notify listeners. 280 broadcastOnSdkReload(); 281 } 282 283 /** 284 * Initializes the {@link SettingsController} 285 * Extracted so that we can override this in unit tests. 286 */ 287 @VisibleForTesting(visibility=Visibility.PRIVATE) 288 protected SettingsController initSettingsController() { 289 SettingsController settingsController = new SettingsController(mSdkLog); 290 settingsController.registerOnChangedListener(new OnChangedListener() { 291 @Override 292 public void onSettingsChanged( 293 SettingsController controller, 294 SettingsController.Settings oldSettings) { 295 296 // Reset the download cache if it doesn't match the right strategy. 297 // The cache instance gets lazily recreated later in getDownloadCache(). 298 if (mDownloadCache != null) { 299 if (controller.getSettings().getUseDownloadCache() && 300 mDownloadCache.getStrategy() != DownloadCache.Strategy.FRESH_CACHE) { 301 mDownloadCache = null; 302 } else if (!controller.getSettings().getUseDownloadCache() && 303 mDownloadCache.getStrategy() != DownloadCache.Strategy.DIRECT) { 304 mDownloadCache = null; 305 } 306 } 307 } 308 }); 309 return settingsController; 310 } 311 312 @VisibleForTesting(visibility=Visibility.PRIVATE) 313 protected void setSdkManager(SdkManager sdkManager) { 314 mSdkManager = sdkManager; 315 } 316 317 /** 318 * Reloads the SDK content (targets). 319 * <p/> 320 * This also reloads the AVDs in case their status changed. 321 * <p/> 322 * This does not notify the listeners ({@link ISdkChangeListener}). 323 */ 324 public void reloadSdk() { 325 // reload SDK 326 mSdkManager.reloadSdk(mSdkLog); 327 328 // reload AVDs 329 if (mAvdManager != null) { 330 try { 331 mAvdManager.reloadAvds(mSdkLog); 332 } catch (AndroidLocationException e) { 333 // FIXME 334 } 335 } 336 337 mLocalSdkParser.clearPackages(); 338 339 // notify listeners 340 broadcastOnSdkReload(); 341 } 342 343 /** 344 * Reloads the AVDs. 345 * <p/> 346 * This does not notify the listeners. 347 */ 348 public void reloadAvds() { 349 // reload AVDs 350 if (mAvdManager != null) { 351 try { 352 mAvdManager.reloadAvds(mSdkLog); 353 } catch (AndroidLocationException e) { 354 mSdkLog.error(e, null); 355 } 356 } 357 } 358 359 /** 360 * Sets up the default sources: <br/> 361 * - the default google SDK repository, <br/> 362 * - the user sources from prefs <br/> 363 * - the extra repo URLs from the environment, <br/> 364 * - and finally the extra user repo URLs from the environment. 365 */ 366 public void setupDefaultSources() { 367 SdkSources sources = getSources(); 368 369 // Load the conventional sources. 370 // For testing, the env var can be set to replace the default root download URL. 371 // It must end with a / and its the location where the updater will look for 372 // the repository.xml, addons_list.xml and such files. 373 374 String baseUrl = System.getenv("SDK_TEST_BASE_URL"); //$NON-NLS-1$ 375 if (baseUrl == null || baseUrl.length() <= 0 || !baseUrl.endsWith("/")) { //$NON-NLS-1$ 376 baseUrl = SdkRepoConstants.URL_GOOGLE_SDK_SITE; 377 } 378 379 sources.add(SdkSourceCategory.ANDROID_REPO, 380 new SdkRepoSource(baseUrl, 381 SdkSourceCategory.ANDROID_REPO.getUiName())); 382 383 // Load user sources (this will also notify change listeners but this operation is 384 // done early enough that there shouldn't be any anyway.) 385 sources.loadUserAddons(getSdkLog()); 386 } 387 388 /** 389 * Returns the list of installed packages, parsing them if this has not yet been done. 390 * <p/> 391 * The package list is cached in the {@link LocalSdkParser} and will be reset when 392 * {@link #reloadSdk()} is invoked. 393 */ 394 public Package[] getInstalledPackages(ITaskMonitor monitor) { 395 LocalSdkParser parser = getLocalSdkParser(); 396 397 Package[] packages = parser.getPackages(); 398 399 if (packages == null) { 400 // load on demand the first time 401 packages = parser.parseSdk(getOsSdkRoot(), getSdkManager(), monitor); 402 } 403 404 return packages; 405 } 406 /** 407 * Install the list of given {@link Archive}s. This is invoked by the user selecting some 408 * packages in the remote page and then clicking "install selected". 409 * 410 * @param archives The archives to install. Incompatible ones will be skipped. 411 * @param flags Optional flags for the installer, such as {@link #NO_TOOLS_MSG}. 412 * @return A list of archives that have been installed. Can be empty but not null. 413 */ 414 @VisibleForTesting(visibility=Visibility.PRIVATE) 415 protected List<Archive> installArchives(final List<ArchiveInfo> archives, final int flags) { 416 if (mTaskFactory == null) { 417 throw new IllegalArgumentException("Task Factory is null"); 418 } 419 420 // this will accumulate all the packages installed. 421 final List<Archive> newlyInstalledArchives = new ArrayList<Archive>(); 422 423 final boolean forceHttp = getSettingsController().getSettings().getForceHttp(); 424 425 // sort all archives based on their dependency level. 426 Collections.sort(archives, new InstallOrderComparator()); 427 428 mTaskFactory.start("Installing Archives", new ITask() { 429 @Override 430 public void run(ITaskMonitor monitor) { 431 432 final int progressPerArchive = 2 * ArchiveInstaller.NUM_MONITOR_INC; 433 monitor.setProgressMax(1 + archives.size() * progressPerArchive); 434 monitor.setDescription("Preparing to install archives"); 435 436 boolean installedAddon = false; 437 boolean installedTools = false; 438 boolean installedPlatformTools = false; 439 boolean preInstallHookInvoked = false; 440 441 // Mark all current local archives as already installed. 442 HashSet<Archive> installedArchives = new HashSet<Archive>(); 443 for (Package p : getInstalledPackages(monitor.createSubMonitor(1))) { 444 for (Archive a : p.getArchives()) { 445 installedArchives.add(a); 446 } 447 } 448 449 int numInstalled = 0; 450 nextArchive: for (ArchiveInfo ai : archives) { 451 Archive archive = ai.getNewArchive(); 452 if (archive == null) { 453 // This is not supposed to happen. 454 continue nextArchive; 455 } 456 457 int nextProgress = monitor.getProgress() + progressPerArchive; 458 try { 459 if (monitor.isCancelRequested()) { 460 break; 461 } 462 463 ArchiveInfo[] adeps = ai.getDependsOn(); 464 if (adeps != null) { 465 for (ArchiveInfo adep : adeps) { 466 Archive na = adep.getNewArchive(); 467 if (na == null) { 468 // This archive depends on a missing archive. 469 // We shouldn't get here. 470 // Skip it. 471 monitor.log("Skipping '%1$s'; it depends on a missing package.", 472 archive.getParentPackage().getShortDescription()); 473 continue nextArchive; 474 } else if (!installedArchives.contains(na)) { 475 // This archive depends on another one that was not installed. 476 // We shouldn't get here. 477 // Skip it. 478 monitor.logError("Skipping '%1$s'; it depends on '%2$s' which was not installed.", 479 archive.getParentPackage().getShortDescription(), 480 adep.getShortDescription()); 481 continue nextArchive; 482 } 483 } 484 } 485 486 if (!preInstallHookInvoked) { 487 preInstallHookInvoked = true; 488 broadcastPreInstallHook(); 489 } 490 491 ArchiveInstaller installer = createArchiveInstaler(); 492 if (installer.install(ai, 493 mOsSdkRoot, 494 forceHttp, 495 mSdkManager, 496 getDownloadCache(), 497 monitor)) { 498 // We installed this archive. 499 newlyInstalledArchives.add(archive); 500 installedArchives.add(archive); 501 numInstalled++; 502 503 // If this package was replacing an existing one, the old one 504 // is no longer installed. 505 installedArchives.remove(ai.getReplaced()); 506 507 // Check if we successfully installed a platform-tool or add-on package. 508 if (archive.getParentPackage() instanceof AddonPackage) { 509 installedAddon = true; 510 } else if (archive.getParentPackage() instanceof ToolPackage) { 511 installedTools = true; 512 } else if (archive.getParentPackage() instanceof PlatformToolPackage) { 513 installedPlatformTools = true; 514 } 515 } 516 517 } catch (Throwable t) { 518 // Display anything unexpected in the monitor. 519 String msg = t.getMessage(); 520 if (msg != null) { 521 msg = String.format("Unexpected Error installing '%1$s': %2$s: %3$s", 522 archive.getParentPackage().getShortDescription(), 523 t.getClass().getCanonicalName(), msg); 524 } else { 525 // no error info? get the stack call to display it 526 // At least that'll give us a better bug report. 527 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 528 t.printStackTrace(new PrintStream(baos)); 529 530 msg = String.format("Unexpected Error installing '%1$s'\n%2$s", 531 archive.getParentPackage().getShortDescription(), 532 baos.toString()); 533 } 534 535 monitor.log( "%1$s", msg); //$NON-NLS-1$ 536 mSdkLog.error(t, "%1$s", msg); //$NON-NLS-1$ 537 } finally { 538 539 // Always move the progress bar to the desired position. 540 // This allows internal methods to not have to care in case 541 // they abort early 542 monitor.incProgress(nextProgress - monitor.getProgress()); 543 } 544 } 545 546 if (installedAddon) { 547 // Update the USB vendor ids for adb 548 try { 549 mSdkManager.updateAdb(); 550 monitor.log("Updated ADB to support the USB devices declared in the SDK add-ons."); 551 } catch (Exception e) { 552 mSdkLog.error(e, "Update ADB failed"); 553 monitor.logError("failed to update adb to support the USB devices declared in the SDK add-ons."); 554 } 555 } 556 557 if (preInstallHookInvoked) { 558 broadcastPostInstallHook(); 559 } 560 561 if (installedAddon || installedPlatformTools) { 562 // We need to restart ADB. Actually since we don't know if it's even 563 // running, maybe we should just kill it and not start it. 564 // Note: it turns out even under Windows we don't need to kill adb 565 // before updating the tools folder, as adb.exe is (surprisingly) not 566 // locked. 567 568 askForAdbRestart(monitor); 569 } 570 571 if (installedTools) { 572 notifyToolsNeedsToBeRestarted(flags); 573 } 574 575 if (numInstalled == 0) { 576 monitor.setDescription("Done. Nothing was installed."); 577 } else { 578 monitor.setDescription("Done. %1$d %2$s installed.", 579 numInstalled, 580 numInstalled == 1 ? "package" : "packages"); 581 582 //notify listeners something was installed, so that they can refresh 583 reloadSdk(); 584 } 585 } 586 }); 587 588 return newlyInstalledArchives; 589 } 590 591 /** 592 * A comparator to sort all the {@link ArchiveInfo} based on their 593 * dependency level. This forces the installer to install first all packages 594 * with no dependency, then those with one level of dependency, etc. 595 */ 596 private static class InstallOrderComparator implements Comparator<ArchiveInfo> { 597 598 private final Map<ArchiveInfo, Integer> mOrders = new HashMap<ArchiveInfo, Integer>(); 599 600 @Override 601 public int compare(ArchiveInfo o1, ArchiveInfo o2) { 602 int n1 = getDependencyOrder(o1); 603 int n2 = getDependencyOrder(o2); 604 605 return n1 - n2; 606 } 607 608 private int getDependencyOrder(ArchiveInfo ai) { 609 if (ai == null) { 610 return 0; 611 } 612 613 // reuse cached value, if any 614 Integer cached = mOrders.get(ai); 615 if (cached != null) { 616 return cached.intValue(); 617 } 618 619 ArchiveInfo[] deps = ai.getDependsOn(); 620 if (deps == null) { 621 return 0; 622 } 623 624 // compute dependencies, recursively 625 int n = deps.length; 626 627 for (ArchiveInfo dep : deps) { 628 n += getDependencyOrder(dep); 629 } 630 631 // cache it 632 mOrders.put(ai, Integer.valueOf(n)); 633 634 return n; 635 } 636 637 } 638 639 /** 640 * Attempts to restart ADB. 641 * <p/> 642 * If the "ask before restart" setting is set (the default), prompt the user whether 643 * now is a good time to restart ADB. 644 * 645 * @param monitor 646 */ 647 private void askForAdbRestart(ITaskMonitor monitor) { 648 final boolean[] canRestart = new boolean[] { true }; 649 650 if (getWindowShell() != null && 651 getSettingsController().getSettings().getAskBeforeAdbRestart()) { 652 // need to ask for permission first 653 final Shell shell = getWindowShell(); 654 if (shell != null && !shell.isDisposed()) { 655 shell.getDisplay().syncExec(new Runnable() { 656 @Override 657 public void run() { 658 if (!shell.isDisposed()) { 659 canRestart[0] = MessageDialog.openQuestion(shell, 660 "ADB Restart", 661 "A package that depends on ADB has been updated. \n" + 662 "Do you want to restart ADB now?"); 663 } 664 } 665 }); 666 } 667 } 668 669 if (canRestart[0]) { 670 AdbWrapper adb = new AdbWrapper(getOsSdkRoot(), monitor); 671 adb.stopAdb(); 672 adb.startAdb(); 673 } 674 } 675 676 private void notifyToolsNeedsToBeRestarted(int flags) { 677 678 String msg = null; 679 if ((flags & TOOLS_MSG_UPDATED_FROM_ADT) != 0) { 680 msg = 681 "The Android SDK and AVD Manager that you are currently using has been updated. " + 682 "Please also run Eclipse > Help > Check for Updates to see if the Android " + 683 "plug-in needs to be updated."; 684 685 } else if ((flags & TOOLS_MSG_UPDATED_FROM_SDKMAN) != 0) { 686 msg = 687 "The Android SDK and AVD Manager that you are currently using has been updated. " + 688 "It is recommended that you now close the manager window and re-open it. " + 689 "If you use Eclipse, please run Help > Check for Updates to see if the Android " + 690 "plug-in needs to be updated."; 691 } 692 693 final String msg2 = msg; 694 695 final Shell shell = getWindowShell(); 696 if (msg2 != null && shell != null && !shell.isDisposed()) { 697 shell.getDisplay().syncExec(new Runnable() { 698 @Override 699 public void run() { 700 if (!shell.isDisposed()) { 701 MessageDialog.openInformation(shell, 702 "Android Tools Updated", 703 msg2); 704 } 705 } 706 }); 707 } 708 } 709 710 711 /** 712 * Tries to update all the *existing* local packages. 713 * This version *requires* to be run with a GUI. 714 * <p/> 715 * There are two modes of operation: 716 * <ul> 717 * <li>If selectedArchives is null, refreshes all sources, compares the available remote 718 * packages with the current local ones and suggest updates to be done to the user (including 719 * new platforms that the users doesn't have yet). 720 * <li>If selectedArchives is not null, this represents a list of archives/packages that 721 * the user wants to install or update, so just process these. 722 * </ul> 723 * 724 * @param selectedArchives The list of remote archives to consider for the update. 725 * This can be null, in which case a list of remote archive is fetched from all 726 * available sources. 727 * @param includeObsoletes True if obsolete packages should be used when resolving what 728 * to update. 729 * @param flags Optional flags for the installer, such as {@link #NO_TOOLS_MSG}. 730 * @return A list of archives that have been installed. Can be null if nothing was done. 731 */ 732 public List<Archive> updateOrInstallAll_WithGUI( 733 Collection<Archive> selectedArchives, 734 boolean includeObsoletes, 735 int flags) { 736 737 // Note: we no longer call refreshSources(true) here. This will be done 738 // automatically by computeUpdates() iif it needs to access sources to 739 // resolve missing dependencies. 740 741 SdkUpdaterLogic ul = new SdkUpdaterLogic(this); 742 List<ArchiveInfo> archives = ul.computeUpdates( 743 selectedArchives, 744 getSources(), 745 getLocalSdkParser().getPackages(), 746 includeObsoletes); 747 748 if (selectedArchives == null) { 749 getPackageLoader().loadRemoteAddonsList(new NullTaskMonitor(getSdkLog())); 750 ul.addNewPlatforms( 751 archives, 752 getSources(), 753 getLocalSdkParser().getPackages(), 754 includeObsoletes); 755 } 756 757 // TODO if selectedArchives is null and archives.len==0, find if there are 758 // any new platform we can suggest to install instead. 759 760 Collections.sort(archives); 761 762 SdkUpdaterChooserDialog dialog = 763 new SdkUpdaterChooserDialog(getWindowShell(), this, archives); 764 dialog.open(); 765 766 ArrayList<ArchiveInfo> result = dialog.getResult(); 767 if (result != null && result.size() > 0) { 768 return installArchives(result, flags); 769 } 770 return null; 771 } 772 773 /** 774 * Fetches all archives available on the known remote sources. 775 * 776 * Used by {@link UpdaterData#listRemotePackages_NoGUI} and 777 * {@link UpdaterData#updateOrInstallAll_NoGUI}. 778 * 779 * @param includeAll True to list and install all packages, including obsolete ones. 780 * @return A list of potential {@link ArchiveInfo} to install. 781 */ 782 private List<ArchiveInfo> getRemoteArchives_NoGUI(boolean includeAll) { 783 refreshSources(true); 784 getPackageLoader().loadRemoteAddonsList(new NullTaskMonitor(getSdkLog())); 785 786 List<ArchiveInfo> archives; 787 SdkUpdaterLogic ul = new SdkUpdaterLogic(this); 788 789 if (includeAll) { 790 archives = ul.getAllRemoteArchives( 791 getSources(), 792 getLocalSdkParser().getPackages(), 793 includeAll); 794 795 } else { 796 archives = ul.computeUpdates( 797 null /*selectedArchives*/, 798 getSources(), 799 getLocalSdkParser().getPackages(), 800 includeAll); 801 802 ul.addNewPlatforms( 803 archives, 804 getSources(), 805 getLocalSdkParser().getPackages(), 806 includeAll); 807 } 808 809 Collections.sort(archives); 810 return archives; 811 } 812 813 /** 814 * Lists remote packages available for install using 815 * {@link UpdaterData#updateOrInstallAll_NoGUI}. 816 * 817 * @param includeAll True to list and install all packages, including obsolete ones. 818 * @param extendedOutput True to display more details on each package. 819 */ 820 public void listRemotePackages_NoGUI(boolean includeAll, boolean extendedOutput) { 821 822 List<ArchiveInfo> archives = getRemoteArchives_NoGUI(includeAll); 823 824 mSdkLog.info("Packages available for installation or update: %1$d\n", archives.size()); 825 826 int index = 1; 827 for (ArchiveInfo ai : archives) { 828 Archive a = ai.getNewArchive(); 829 if (a != null) { 830 Package p = a.getParentPackage(); 831 if (p != null) { 832 if (extendedOutput) { 833 mSdkLog.info("----------\n"); 834 mSdkLog.info("id: %1$d or \"%2$s\"\n", index, p.installId()); 835 mSdkLog.info(" Type: %1$s\n", 836 p.getClass().getSimpleName().replaceAll("Package", "")); //$NON-NLS-1$ //$NON-NLS-2$ 837 String desc = LineUtil.reformatLine(" Desc: %s\n", 838 p.getLongDescription()); 839 mSdkLog.info("%s", desc); //$NON-NLS-1$ 840 } else { 841 mSdkLog.info("%1$ 4d- %2$s\n", 842 index, 843 p.getShortDescription()); 844 } 845 index++; 846 } 847 } 848 } 849 } 850 851 /** 852 * Tries to update all the *existing* local packages. 853 * This version is intended to run without a GUI and 854 * only outputs to the current {@link ILogger}. 855 * 856 * @param pkgFilter A list of {@link SdkRepoConstants#NODES} or {@link Package#installId()} 857 * or package indexes to limit the packages we can update or install. 858 * A null or empty list means to update everything possible. 859 * @param includeAll True to list and install all packages, including obsolete ones. 860 * @param dryMode True to check what would be updated/installed but do not actually 861 * download or install anything. 862 * @return A list of archives that have been installed. Can be null if nothing was done. 863 */ 864 public List<Archive> updateOrInstallAll_NoGUI( 865 Collection<String> pkgFilter, 866 boolean includeAll, 867 boolean dryMode) { 868 869 List<ArchiveInfo> archives = getRemoteArchives_NoGUI(includeAll); 870 871 // Filter the selected archives to only keep the ones matching the filter 872 if (pkgFilter != null && pkgFilter.size() > 0 && archives != null && archives.size() > 0) { 873 // Map filter types to an SdkRepository Package type, 874 // e.g. create a map "platform" => PlatformPackage.class 875 HashMap<String, Class<? extends Package>> pkgMap = 876 new HashMap<String, Class<? extends Package>>(); 877 878 mapFilterToPackageClass(pkgMap, SdkRepoConstants.NODES); 879 mapFilterToPackageClass(pkgMap, SdkAddonConstants.NODES); 880 881 // Prepare a map install-id => package instance 882 HashMap<String, Package> installIdMap = new HashMap<String, Package>(); 883 for (ArchiveInfo ai : archives) { 884 Archive a = ai.getNewArchive(); 885 if (a != null) { 886 Package p = a.getParentPackage(); 887 if (p != null) { 888 String id = p.installId(); 889 if (id != null && id.length() > 0 && !installIdMap.containsKey(id)) { 890 installIdMap.put(id, p); 891 } 892 } 893 } 894 } 895 896 // Now intersect this with the pkgFilter requested by the user, in order to 897 // only keep the classes that the user wants to install. 898 // We also create a set with the package indices requested by the user 899 // and a set of install-ids requested by the user. 900 901 HashSet<Class<? extends Package>> userFilteredClasses = 902 new HashSet<Class<? extends Package>>(); 903 SparseIntArray userFilteredIndices = new SparseIntArray(); 904 Set<String> userFilteredInstallIds = new HashSet<String>(); 905 906 for (String type : pkgFilter) { 907 if (installIdMap.containsKey(type)) { 908 userFilteredInstallIds.add(type); 909 910 } else if (type.replaceAll("[0-9]+", "").length() == 0) {//$NON-NLS-1$ //$NON-NLS-2$ 911 // An all-digit number is a package index requested by the user. 912 int index = Integer.parseInt(type); 913 userFilteredIndices.put(index, index); 914 915 } else if (pkgMap.containsKey(type)) { 916 userFilteredClasses.add(pkgMap.get(type)); 917 918 } else { 919 // This should not happen unless there's a mismatch in the package map. 920 mSdkLog.error(null, "Ignoring unknown package filter '%1$s'", type); 921 } 922 } 923 924 // we don't need the maps anymore 925 pkgMap = null; 926 installIdMap = null; 927 928 // Now filter the remote archives list to keep: 929 // - any package which class matches userFilteredClasses 930 // - any package index which matches userFilteredIndices 931 // - any package install id which matches userFilteredInstallIds 932 933 int index = 1; 934 for (Iterator<ArchiveInfo> it = archives.iterator(); it.hasNext(); ) { 935 boolean keep = false; 936 ArchiveInfo ai = it.next(); 937 Archive a = ai.getNewArchive(); 938 if (a != null) { 939 Package p = a.getParentPackage(); 940 if (p != null) { 941 if (userFilteredInstallIds.contains(p.installId()) || 942 userFilteredClasses.contains(p.getClass()) || 943 userFilteredIndices.get(index) > 0) { 944 keep = true; 945 } 946 947 index++; 948 } 949 } 950 951 if (!keep) { 952 it.remove(); 953 } 954 } 955 956 if (archives.size() == 0) { 957 mSdkLog.info(LineUtil.reflowLine( 958 "Warning: The package filter removed all packages. There is nothing to install.\nPlease consider trying to update again without a package filter.\n")); 959 return null; 960 } 961 } 962 963 if (archives != null && archives.size() > 0) { 964 if (dryMode) { 965 mSdkLog.info("Packages selected for install:\n"); 966 for (ArchiveInfo ai : archives) { 967 Archive a = ai.getNewArchive(); 968 if (a != null) { 969 Package p = a.getParentPackage(); 970 if (p != null) { 971 mSdkLog.info("- %1$s\n", p.getShortDescription()); 972 } 973 } 974 } 975 mSdkLog.info("\nDry mode is on so nothing is actually being installed.\n"); 976 } else { 977 return installArchives(archives, NO_TOOLS_MSG); 978 } 979 } else { 980 mSdkLog.info("There is nothing to install or update.\n"); 981 } 982 983 return null; 984 } 985 986 @SuppressWarnings("unchecked") 987 private void mapFilterToPackageClass( 988 HashMap<String, Class<? extends Package>> inOutPkgMap, 989 String[] nodes) { 990 991 // Automatically find the classes matching the node names 992 ClassLoader classLoader = getClass().getClassLoader(); 993 String basePackage = Package.class.getPackage().getName(); 994 995 for (String node : nodes) { 996 // Capitalize the name 997 String name = node.substring(0, 1).toUpperCase() + node.substring(1); 998 999 // We can have one dash at most in a name. If it's present, we'll try 1000 // with the dash or with the next letter capitalized. 1001 int dash = name.indexOf('-'); 1002 if (dash > 0) { 1003 name = name.replaceFirst("-", ""); 1004 } 1005 1006 for (int alternatives = 0; alternatives < 2; alternatives++) { 1007 1008 String fqcn = basePackage + '.' + name + "Package"; //$NON-NLS-1$ 1009 try { 1010 Class<? extends Package> clazz = 1011 (Class<? extends Package>) classLoader.loadClass(fqcn); 1012 if (clazz != null) { 1013 inOutPkgMap.put(node, clazz); 1014 continue; 1015 } 1016 } catch (Throwable ignore) { 1017 } 1018 1019 if (alternatives == 0 && dash > 0) { 1020 // Try an alternative where the next letter after the dash 1021 // is converted to an upper case. 1022 name = name.substring(0, dash) + 1023 name.substring(dash, dash + 1).toUpperCase() + 1024 name.substring(dash + 1); 1025 } else { 1026 break; 1027 } 1028 } 1029 } 1030 } 1031 1032 /** 1033 * Refresh all sources. This is invoked either internally (reusing an existing monitor) 1034 * or as a UI callback on the remote page "Refresh" button (in which case the monitor is 1035 * null and a new task should be created.) 1036 * 1037 * @param forceFetching When true, load sources that haven't been loaded yet. 1038 * When false, only refresh sources that have been loaded yet. 1039 */ 1040 public void refreshSources(final boolean forceFetching) { 1041 assert mTaskFactory != null; 1042 1043 final boolean forceHttp = getSettingsController().getSettings().getForceHttp(); 1044 1045 mTaskFactory.start("Refresh Sources", new ITask() { 1046 @Override 1047 public void run(ITaskMonitor monitor) { 1048 1049 getPackageLoader().loadRemoteAddonsList(monitor); 1050 1051 SdkSource[] sources = getSources().getAllSources(); 1052 monitor.setDescription("Refresh Sources"); 1053 monitor.setProgressMax(monitor.getProgress() + sources.length); 1054 for (SdkSource source : sources) { 1055 if (forceFetching || 1056 source.getPackages() != null || 1057 source.getFetchError() != null) { 1058 source.load(getDownloadCache(), monitor.createSubMonitor(1), forceHttp); 1059 } 1060 monitor.incProgress(1); 1061 } 1062 } 1063 }); 1064 } 1065 1066 /** 1067 * Safely invoke all the registered {@link ISdkChangeListener#onSdkLoaded()}. 1068 * This can be called from any thread. 1069 */ 1070 public void broadcastOnSdkLoaded() { 1071 if (mWindowShell != null && !mWindowShell.isDisposed() && mListeners.size() > 0) { 1072 mWindowShell.getDisplay().syncExec(new Runnable() { 1073 @Override 1074 public void run() { 1075 for (ISdkChangeListener listener : mListeners) { 1076 try { 1077 listener.onSdkLoaded(); 1078 } catch (Throwable t) { 1079 mSdkLog.error(t, null); 1080 } 1081 } 1082 } 1083 }); 1084 } 1085 } 1086 1087 /** 1088 * Safely invoke all the registered {@link ISdkChangeListener#onSdkReload()}. 1089 * This can be called from any thread. 1090 */ 1091 private void broadcastOnSdkReload() { 1092 if (mWindowShell != null && !mWindowShell.isDisposed() && mListeners.size() > 0) { 1093 mWindowShell.getDisplay().syncExec(new Runnable() { 1094 @Override 1095 public void run() { 1096 for (ISdkChangeListener listener : mListeners) { 1097 try { 1098 listener.onSdkReload(); 1099 } catch (Throwable t) { 1100 mSdkLog.error(t, null); 1101 } 1102 } 1103 } 1104 }); 1105 } 1106 } 1107 1108 /** 1109 * Safely invoke all the registered {@link ISdkChangeListener#preInstallHook()}. 1110 * This can be called from any thread. 1111 */ 1112 private void broadcastPreInstallHook() { 1113 if (mWindowShell != null && !mWindowShell.isDisposed() && mListeners.size() > 0) { 1114 mWindowShell.getDisplay().syncExec(new Runnable() { 1115 @Override 1116 public void run() { 1117 for (ISdkChangeListener listener : mListeners) { 1118 try { 1119 listener.preInstallHook(); 1120 } catch (Throwable t) { 1121 mSdkLog.error(t, null); 1122 } 1123 } 1124 } 1125 }); 1126 } 1127 } 1128 1129 /** 1130 * Safely invoke all the registered {@link ISdkChangeListener#postInstallHook()}. 1131 * This can be called from any thread. 1132 */ 1133 private void broadcastPostInstallHook() { 1134 if (mWindowShell != null && !mWindowShell.isDisposed() && mListeners.size() > 0) { 1135 mWindowShell.getDisplay().syncExec(new Runnable() { 1136 @Override 1137 public void run() { 1138 for (ISdkChangeListener listener : mListeners) { 1139 try { 1140 listener.postInstallHook(); 1141 } catch (Throwable t) { 1142 mSdkLog.error(t, null); 1143 } 1144 } 1145 } 1146 }); 1147 } 1148 } 1149 1150 /** 1151 * Internal helper to return a new {@link ArchiveInstaller}. 1152 * This allows us to override the installer for unit-testing. 1153 */ 1154 @VisibleForTesting(visibility=Visibility.PRIVATE) 1155 protected ArchiveInstaller createArchiveInstaler() { 1156 return new ArchiveInstaller(); 1157 } 1158 1159 } 1160