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.prefs.AndroidLocation.AndroidLocationException; 20 import com.android.sdklib.ISdkLog; 21 import com.android.sdklib.SdkConstants; 22 import com.android.sdklib.SdkManager; 23 import com.android.sdklib.internal.avd.AvdManager; 24 import com.android.sdklib.internal.repository.AddonPackage; 25 import com.android.sdklib.internal.repository.Archive; 26 import com.android.sdklib.internal.repository.ITask; 27 import com.android.sdklib.internal.repository.ITaskFactory; 28 import com.android.sdklib.internal.repository.ITaskMonitor; 29 import com.android.sdklib.internal.repository.LocalSdkParser; 30 import com.android.sdklib.internal.repository.Package; 31 import com.android.sdklib.internal.repository.RepoSource; 32 import com.android.sdklib.internal.repository.RepoSources; 33 import com.android.sdklib.internal.repository.ToolPackage; 34 import com.android.sdkuilib.internal.repository.icons.ImageFactory; 35 import com.android.sdkuilib.repository.UpdaterWindow.ISdkListener; 36 37 import org.eclipse.jface.dialogs.MessageDialog; 38 import org.eclipse.swt.widgets.Display; 39 import org.eclipse.swt.widgets.Shell; 40 41 import java.io.ByteArrayOutputStream; 42 import java.io.PrintStream; 43 import java.util.ArrayList; 44 import java.util.Collection; 45 import java.util.HashSet; 46 47 /** 48 * Data shared between {@link UpdaterWindowImpl} and its pages. 49 */ 50 class UpdaterData { 51 private String mOsSdkRoot; 52 53 private final ISdkLog mSdkLog; 54 private ITaskFactory mTaskFactory; 55 private boolean mUserCanChangeSdkRoot; 56 57 private SdkManager mSdkManager; 58 private AvdManager mAvdManager; 59 60 private final LocalSdkParser mLocalSdkParser = new LocalSdkParser(); 61 private final RepoSources mSources = new RepoSources(); 62 63 private final LocalSdkAdapter mLocalSdkAdapter = new LocalSdkAdapter(this); 64 private final RepoSourcesAdapter mSourcesAdapter = new RepoSourcesAdapter(this); 65 66 private ImageFactory mImageFactory; 67 68 private final SettingsController mSettingsController; 69 70 private final ArrayList<ISdkListener> mListeners = new ArrayList<ISdkListener>(); 71 72 private Shell mWindowShell; 73 74 private AndroidLocationException mAvdManagerInitError; 75 76 /** 77 * Creates a new updater data. 78 * 79 * @param sdkLog Logger. Cannot be null. 80 * @param osSdkRoot The OS path to the SDK root. 81 */ 82 public UpdaterData(String osSdkRoot, ISdkLog sdkLog) { 83 mOsSdkRoot = osSdkRoot; 84 mSdkLog = sdkLog; 85 86 mSettingsController = new SettingsController(this); 87 88 initSdk(); 89 } 90 91 // ----- getters, setters ---- 92 93 public String getOsSdkRoot() { 94 return mOsSdkRoot; 95 } 96 97 public void setTaskFactory(ITaskFactory taskFactory) { 98 mTaskFactory = taskFactory; 99 } 100 101 public ITaskFactory getTaskFactory() { 102 return mTaskFactory; 103 } 104 105 public void setUserCanChangeSdkRoot(boolean userCanChangeSdkRoot) { 106 mUserCanChangeSdkRoot = userCanChangeSdkRoot; 107 } 108 109 public boolean canUserChangeSdkRoot() { 110 return mUserCanChangeSdkRoot; 111 } 112 113 public RepoSources getSources() { 114 return mSources; 115 } 116 117 public RepoSourcesAdapter getSourcesAdapter() { 118 return mSourcesAdapter; 119 } 120 121 public LocalSdkParser getLocalSdkParser() { 122 return mLocalSdkParser; 123 } 124 125 public LocalSdkAdapter getLocalSdkAdapter() { 126 return mLocalSdkAdapter; 127 } 128 129 public ISdkLog getSdkLog() { 130 return mSdkLog; 131 } 132 133 public void setImageFactory(ImageFactory imageFactory) { 134 mImageFactory = imageFactory; 135 } 136 137 public ImageFactory getImageFactory() { 138 return mImageFactory; 139 } 140 141 public SdkManager getSdkManager() { 142 return mSdkManager; 143 } 144 145 public AvdManager getAvdManager() { 146 return mAvdManager; 147 } 148 149 public SettingsController getSettingsController() { 150 return mSettingsController; 151 } 152 153 /** Adds a listener ({@link ISdkListener}) that is notified when the SDK is reloaded. */ 154 public void addListeners(ISdkListener listener) { 155 if (mListeners.contains(listener) == false) { 156 mListeners.add(listener); 157 } 158 } 159 160 /** Removes a listener ({@link ISdkListener}) that is notified when the SDK is reloaded. */ 161 public void removeListener(ISdkListener listener) { 162 mListeners.remove(listener); 163 } 164 165 public void setWindowShell(Shell windowShell) { 166 mWindowShell = windowShell; 167 } 168 169 public Shell getWindowShell() { 170 return mWindowShell; 171 } 172 173 /** 174 * Check if any error occurred during initialization. 175 * If it did, display an error message. 176 * 177 * @return True if an error occurred, false if we should continue. 178 */ 179 public boolean checkIfInitFailed() { 180 if (mAvdManagerInitError != null) { 181 String example; 182 if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { 183 example = "%USERPROFILE%"; //$NON-NLS-1$ 184 } else { 185 example = "~"; //$NON-NLS-1$ 186 } 187 188 MessageDialog.openError(mWindowShell, 189 "Android Virtual Devices Manager", 190 String.format( 191 "The AVD manager normally uses the user's profile directory to store " + 192 "AVD files. However it failed to find the default profile directory. " + 193 "\n" + 194 "To fix this, please set the environment variable ANDROID_SDK_HOME to " + 195 "a valid path such as \"%s\".", 196 example)); 197 198 return true; 199 } 200 return false; 201 } 202 203 // ----- 204 205 /** 206 * Initializes the {@link SdkManager} and the {@link AvdManager}. 207 */ 208 private void initSdk() { 209 mSdkManager = SdkManager.createManager(mOsSdkRoot, mSdkLog); 210 try { 211 mAvdManager = null; // remove the old one if needed. 212 mAvdManager = new AvdManager(mSdkManager, mSdkLog); 213 } catch (AndroidLocationException e) { 214 mSdkLog.error(e, "Unable to read AVDs: " + e.getMessage()); //$NON-NLS-1$ 215 216 // Note: we used to continue here, but the thing is that 217 // mAvdManager==null so nothing is really going to work as 218 // expected. Let's just display an error later in checkIfInitFailed() 219 // and abort right there. This step is just too early in the SWT 220 // setup process to display a message box yet. 221 222 mAvdManagerInitError = e; 223 } 224 225 // notify adapters/parsers 226 // TODO 227 228 // notify listeners. 229 notifyListeners(false /*init*/); 230 } 231 232 /** 233 * Reloads the SDK content (targets). 234 * <p/> This also reloads the AVDs in case their status changed. 235 * <p/>This does not notify the listeners ({@link ISdkListener}). 236 */ 237 public void reloadSdk() { 238 // reload SDK 239 mSdkManager.reloadSdk(mSdkLog); 240 241 // reload AVDs 242 if (mAvdManager != null) { 243 try { 244 mAvdManager.reloadAvds(mSdkLog); 245 } catch (AndroidLocationException e) { 246 // FIXME 247 } 248 } 249 250 // notify adapters? 251 mLocalSdkParser.clearPackages(); 252 // TODO 253 254 // notify listeners 255 notifyListeners(false /*init*/); 256 } 257 258 /** 259 * Reloads the AVDs. 260 * <p/>This does not notify the listeners. 261 */ 262 public void reloadAvds() { 263 // reload AVDs 264 if (mAvdManager != null) { 265 try { 266 mAvdManager.reloadAvds(mSdkLog); 267 } catch (AndroidLocationException e) { 268 mSdkLog.error(e, null); 269 } 270 } 271 } 272 273 /** 274 * Returns the list of installed packages, parsing them if this has not yet been done. 275 */ 276 public Package[] getInstalledPackage() { 277 LocalSdkParser parser = getLocalSdkParser(); 278 279 Package[] packages = parser.getPackages(); 280 281 if (packages == null) { 282 // load on demand the first time 283 packages = parser.parseSdk(getOsSdkRoot(), getSdkManager(), getSdkLog()); 284 } 285 286 return packages; 287 } 288 289 /** 290 * Notify the listeners ({@link ISdkListener}) that the SDK was reloaded. 291 * <p/>This can be called from any thread. 292 * @param init whether the SDK loaded for the first time. 293 */ 294 public void notifyListeners(final boolean init) { 295 if (mWindowShell != null && mListeners.size() > 0) { 296 mWindowShell.getDisplay().syncExec(new Runnable() { 297 public void run() { 298 for (ISdkListener listener : mListeners) { 299 try { 300 listener.onSdkChange(init); 301 } catch (Throwable t) { 302 mSdkLog.error(t, null); 303 } 304 } 305 } 306 }); 307 } 308 } 309 310 /** 311 * Install the list of given {@link Archive}s. This is invoked by the user selecting some 312 * packages in the remote page and then clicking "install selected". 313 * 314 * @param result The archives to install. Incompatible ones will be skipped. 315 */ 316 public void installArchives(final ArrayList<ArchiveInfo> result) { 317 if (mTaskFactory == null) { 318 throw new IllegalArgumentException("Task Factory is null"); 319 } 320 321 final boolean forceHttp = getSettingsController().getForceHttp(); 322 323 mTaskFactory.start("Installing Archives", new ITask() { 324 public void run(ITaskMonitor monitor) { 325 326 final int progressPerArchive = 2 * Archive.NUM_MONITOR_INC; 327 monitor.setProgressMax(result.size() * progressPerArchive); 328 monitor.setDescription("Preparing to install archives"); 329 330 boolean installedAddon = false; 331 boolean installedTools = false; 332 333 // Mark all current local archives as already installed. 334 HashSet<Archive> installedArchives = new HashSet<Archive>(); 335 for (Package p : getInstalledPackage()) { 336 for (Archive a : p.getArchives()) { 337 installedArchives.add(a); 338 } 339 } 340 341 int numInstalled = 0; 342 nextArchive: for (ArchiveInfo ai : result) { 343 Archive archive = ai.getNewArchive(); 344 if (archive == null) { 345 // This is not supposed to happen. 346 continue nextArchive; 347 } 348 349 int nextProgress = monitor.getProgress() + progressPerArchive; 350 try { 351 if (monitor.isCancelRequested()) { 352 break; 353 } 354 355 ArchiveInfo[] adeps = ai.getDependsOn(); 356 if (adeps != null) { 357 for (ArchiveInfo adep : adeps) { 358 Archive na = adep.getNewArchive(); 359 if (na == null) { 360 // This archive depends on a missing archive. 361 // We shouldn't get here. 362 // Skip it. 363 monitor.setResult("Skipping '%1$s'; it depends on a missing package.", 364 archive.getParentPackage().getShortDescription()); 365 continue nextArchive; 366 } else if (!installedArchives.contains(na)) { 367 // This archive depends on another one that was not installed. 368 // We shouldn't get here. 369 // Skip it. 370 monitor.setResult("Skipping '%1$s'; it depends on '%2$s' which was not installed.", 371 archive.getParentPackage().getShortDescription(), 372 adep.getShortDescription()); 373 continue nextArchive; 374 } 375 } 376 } 377 378 if (archive.install(mOsSdkRoot, forceHttp, mSdkManager, monitor)) { 379 // We installed this archive. 380 installedArchives.add(archive); 381 numInstalled++; 382 383 // If this package was replacing an existing one, the old one 384 // is no longer installed. 385 installedArchives.remove(ai.getReplaced()); 386 387 // Check if we successfully installed a tool or add-on package. 388 if (archive.getParentPackage() instanceof AddonPackage) { 389 installedAddon = true; 390 } else if (archive.getParentPackage() instanceof ToolPackage) { 391 installedTools = true; 392 } 393 } 394 395 } catch (Throwable t) { 396 // Display anything unexpected in the monitor. 397 String msg = t.getMessage(); 398 if (msg != null) { 399 monitor.setResult("Unexpected Error installing '%1$s': %2$s", 400 archive.getParentPackage().getShortDescription(), msg); 401 } else { 402 // no error info? get the stack call to display it 403 // At least that'll give us a better bug report. 404 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 405 t.printStackTrace(new PrintStream(baos)); 406 407 // and display it 408 monitor.setResult("Unexpected Error installing '%1$s'\n%2$s", 409 archive.getParentPackage().getShortDescription(), 410 baos.toString()); 411 } 412 } finally { 413 414 // Always move the progress bar to the desired position. 415 // This allows internal methods to not have to care in case 416 // they abort early 417 monitor.incProgress(nextProgress - monitor.getProgress()); 418 } 419 } 420 421 if (installedAddon) { 422 // Update the USB vendor ids for adb 423 try { 424 mSdkManager.updateAdb(); 425 monitor.setResult("Updated ADB to support the USB devices declared in the SDK add-ons."); 426 } catch (Exception e) { 427 mSdkLog.error(e, "Update ADB failed"); 428 monitor.setResult("failed to update adb to support the USB devices declared in the SDK add-ons."); 429 } 430 } 431 432 if (installedAddon || installedTools) { 433 // We need to restart ADB. Actually since we don't know if it's even 434 // running, maybe we should just kill it and not start it. 435 // Note: it turns out even under Windows we don't need to kill adb 436 // before updating the tools folder, as adb.exe is (surprisingly) not 437 // locked. 438 439 askForAdbRestart(monitor); 440 } 441 442 if (installedTools) { 443 notifyToolsNeedsToBeRestarted(); 444 } 445 446 if (numInstalled == 0) { 447 monitor.setDescription("Done. Nothing was installed."); 448 } else { 449 monitor.setDescription("Done. %1$d %2$s installed.", 450 numInstalled, 451 numInstalled == 1 ? "package" : "packages"); 452 453 //notify listeners something was installed, so that they can refresh 454 reloadSdk(); 455 } 456 } 457 }); 458 } 459 460 /** 461 * Attemps to restart ADB. 462 * 463 * If the "ask before restart" setting is set (the default), prompt the user whether 464 * now is a good time to restart ADB. 465 * @param monitor 466 */ 467 private void askForAdbRestart(ITaskMonitor monitor) { 468 final boolean[] canRestart = new boolean[] { true }; 469 470 if (getSettingsController().getAskBeforeAdbRestart()) { 471 // need to ask for permission first 472 Display display = mWindowShell.getDisplay(); 473 474 display.syncExec(new Runnable() { 475 public void run() { 476 canRestart[0] = MessageDialog.openQuestion(mWindowShell, 477 "ADB Restart", 478 "A package that depends on ADB has been updated. It is recommended " + 479 "to restart ADB. Is it OK to do it now? If not, you can restart it " + 480 "manually later."); 481 } 482 }); 483 } 484 485 if (canRestart[0]) { 486 AdbWrapper adb = new AdbWrapper(getOsSdkRoot(), monitor); 487 adb.stopAdb(); 488 adb.startAdb(); 489 } 490 } 491 492 private void notifyToolsNeedsToBeRestarted() { 493 Display display = mWindowShell.getDisplay(); 494 495 display.syncExec(new Runnable() { 496 public void run() { 497 MessageDialog.openInformation(mWindowShell, 498 "Android Tools Updated", 499 "The Android SDK and AVD Manager that you are currently using has been updated. " + 500 "It is recommended that you now close the manager window and re-open it. " + 501 "If you started this window from Eclipse, please check if the Android " + 502 "plug-in needs to be updated."); 503 } 504 }); 505 } 506 507 508 /** 509 * Tries to update all the *existing* local packages. 510 * <p/> 511 * There are two modes of operation: 512 * <ul> 513 * <li>If selectedArchives is null, refreshes all sources, compares the available remote 514 * packages with the current local ones and suggest updates to be done to the user (including 515 * new platforms that the users doesn't have yet). 516 * <li>If selectedArchives is not null, this represents a list of archives/packages that 517 * the user wants to install or update, so just process these. 518 * </ul> 519 * 520 * @param selectedArchives The list of remote archive to consider for the update. 521 * This can be null, in which case a list of remote archive is fetched from all 522 * available sources. 523 */ 524 public void updateOrInstallAll(Collection<Archive> selectedArchives) { 525 if (selectedArchives == null) { 526 refreshSources(true); 527 } 528 529 UpdaterLogic ul = new UpdaterLogic(); 530 ArrayList<ArchiveInfo> archives = ul.computeUpdates( 531 selectedArchives, 532 getSources(), 533 getLocalSdkParser().getPackages()); 534 535 if (selectedArchives == null) { 536 ul.addNewPlatforms(archives, getSources(), getLocalSdkParser().getPackages()); 537 } 538 539 // TODO if selectedArchives is null and archives.len==0, find if there's 540 // any new platform we can suggest to install instead. 541 542 UpdateChooserDialog dialog = new UpdateChooserDialog(getWindowShell(), this, archives); 543 dialog.open(); 544 545 ArrayList<ArchiveInfo> result = dialog.getResult(); 546 if (result != null && result.size() > 0) { 547 installArchives(result); 548 } 549 } 550 /** 551 * Refresh all sources. This is invoked either internally (reusing an existing monitor) 552 * or as a UI callback on the remote page "Refresh" button (in which case the monitor is 553 * null and a new task should be created.) 554 * 555 * @param forceFetching When true, load sources that haven't been loaded yet. 556 * When false, only refresh sources that have been loaded yet. 557 */ 558 public void refreshSources(final boolean forceFetching) { 559 assert mTaskFactory != null; 560 561 final boolean forceHttp = getSettingsController().getForceHttp(); 562 563 mTaskFactory.start("Refresh Sources",new ITask() { 564 public void run(ITaskMonitor monitor) { 565 RepoSource[] sources = mSources.getSources(); 566 monitor.setProgressMax(sources.length); 567 for (RepoSource source : sources) { 568 if (forceFetching || 569 source.getPackages() != null || 570 source.getFetchError() != null) { 571 source.load(monitor.createSubMonitor(1), forceHttp); 572 } 573 monitor.incProgress(1); 574 } 575 } 576 }); 577 } 578 } 579