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