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