2 * Copyright (C) 2011 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.sdkuilib.internal.repository;
19 import com.android.sdklib.internal.repository.Archive;
20 import com.android.sdklib.internal.repository.IPackageVersion;
21 import com.android.sdklib.internal.repository.ITask;
22 import com.android.sdklib.internal.repository.ITaskMonitor;
23 import com.android.sdklib.internal.repository.LocalSdkParser;
24 import com.android.sdklib.internal.repository.Package;
25 import com.android.sdklib.internal.repository.SdkSource;
26 import com.android.sdklib.internal.repository.Package.UpdateInfo;
28 import org.eclipse.swt.widgets.Display;
29 import org.eclipse.swt.widgets.Shell;
32 import java.util.ArrayList;
33 import java.util.List;
36 * Loads packages fetched from the remote SDK Repository and keeps track
37 * of their state compared with the current local SDK installation.
41 private final UpdaterData mUpdaterData;
44 * Interface for the callback called by
45 * {@link PackageLoader#loadPackages(ISourceLoadedCallback)}.
47 * After processing each source, the package loader calls {@link #onSourceLoaded(List)}
48 * with the list of package items found in that source. The client should process that
49 * list as it want, typically by accumulating the package items in a list of its own.
50 * By returning true from {@link #onSourceLoaded(List)}, the client tells the loader to
51 * continue and process the next source. By returning false, it tells to stop loading.
53 * The {@link #onLoadCompleted()} method is guaranteed to be called at the end, no
54 * matter how the loader stopped, so that the client can clean up or perform any
57 public interface ISourceLoadedCallback {
59 * After processing each source, the package loader calls this method with the
60 * list of package items found in that source. The client should process that
61 * list as it want, typically by accumulating the package items in a list of its own.
62 * By returning true from {@link #onSourceLoaded(List)}, the client tells the loader to
63 * continue and process the next source. By returning false, it tells to stop loading.
65 * <em>Important</em>: This method is called from a sub-thread, so clients who try
66 * to access any UI widgets must wrap their calls into {@link Display#syncExec(Runnable)}
67 * or {@link Display#asyncExec(Runnable)}.
69 * @param pkgItems All the package items loaded from the last processed source.
70 * This is a copy and the client can hold to this list or modify it in any way.
71 * @return True if the load operation should continue, false if it should stop.
73 public boolean onSourceLoaded(List<PkgItem> pkgItems);
76 * This method is guaranteed to be called at the end, no matter how the
77 * loader stopped, so that the client can clean up or perform any final action.
79 public void onLoadCompleted();
83 * Interface describing the task of installing a specific package.
84 * For details on the operation,
85 * see {@link PackageLoader#loadPackagesWithInstallTask(IAutoInstallTask)}.
87 * @see PackageLoader#loadPackagesWithInstallTask(IAutoInstallTask)
89 public interface IAutoInstallTask {
91 * Called by the install task for every package available (new ones, updates as well
92 * as existing ones that don't have a potential update.)
93 * The method should return true if this is the package that should be installed,
94 * at which point the packager loader will stop processing the next packages and sources.
96 * <em>Important</em>: This method is called from a sub-thread, so clients who try
97 * to access any UI widgets must wrap their calls into {@link Display#syncExec(Runnable)}
98 * or {@link Display#asyncExec(Runnable)}.
100 public boolean acceptPackage(Package pkg);
103 * Called when the accepted package has been installed, successfully or not.
104 * If an already installed (aka existing) package has been accepted, this will
105 * be called with a 'true' success and the actual install path.
107 * <em>Important</em>: This method is called from a sub-thread, so clients who try
108 * to access any UI widgets must wrap their calls into {@link Display#syncExec(Runnable)}
109 * or {@link Display#asyncExec(Runnable)}.
111 public void setResult(Package pkg, boolean success, File installPath);
114 * Called when the task is done iterating and completed.
116 public void taskCompleted();
120 * Creates a new PackageManager associated with the given {@link UpdaterData}.
122 * @param updaterData The {@link UpdaterData}. Must not be null.
124 public PackageLoader(UpdaterData updaterData) {
125 mUpdaterData = updaterData;
129 * Loads all packages from the remote repository.
130 * This runs in an {@link ITask}. The call is blocking.
132 * The callback is called with each set of {@link PkgItem} found in each source.
133 * The caller is responsible to accumulate the packages given to the callback
134 * after each source is finished loaded. In return the callback tells the loader
135 * whether to continue loading sources.
137 public void loadPackages(final ISourceLoadedCallback sourceLoadedCallback) {
139 if (mUpdaterData == null) {
143 // get local packages and offer them to the callback
144 List<PkgItem> localPkgItems = loadLocalPackages();
145 if (!localPkgItems.isEmpty()) {
146 if (!sourceLoadedCallback.onSourceLoaded(localPkgItems)) {
151 final int[] numPackages = { localPkgItems.size() };
153 // get remote packages
154 final boolean forceHttp = mUpdaterData.getSettingsController().getForceHttp();
155 mUpdaterData.loadRemoteAddonsList();
156 mUpdaterData.getTaskFactory().start("Loading Sources", new ITask() {
157 public void run(ITaskMonitor monitor) {
158 SdkSource[] sources = mUpdaterData.getSources().getAllSources();
160 for (SdkSource source : sources) {
161 Package[] pkgs = source.getPackages();
163 source.load(monitor, forceHttp);
164 pkgs = source.getPackages();
170 List<PkgItem> sourcePkgItems = new ArrayList<PkgItem>();
171 for(Package pkg : pkgs) {
172 PkgItem pi = new PkgItem(pkg, PkgState.NEW);
173 sourcePkgItems.add(pi);
176 numPackages[0] += sourcePkgItems.size();
178 // Notify the callback a new source has finished loading.
179 // If the callback requests so, stop right away.
180 if (!sourceLoadedCallback.onSourceLoaded(sourcePkgItems)) {
184 } catch(Exception e) {
185 monitor.logError("Loading source failed: %1$s", e.toString());
187 monitor.setDescription("Done loading %1$d packages from %2$d sources",
194 sourceLoadedCallback.onLoadCompleted();
199 * Internal method that returns all installed packages from the {@link LocalSdkParser}
200 * associated with the {@link UpdaterData}.
202 * Note that the {@link LocalSdkParser} maintains a cache, so callers need to clear
203 * it if they know they changed the local installation.
205 * @return A new list of {@link PkgItem}. May be empty but never null.
207 private List<PkgItem> loadLocalPackages() {
208 List<PkgItem> pkgItems = new ArrayList<PkgItem>();
210 for (Package pkg : mUpdaterData.getInstalledPackages()) {
211 PkgItem pi = new PkgItem(pkg, PkgState.INSTALLED);
219 * Load packages, source by source using {@link #loadPackages(ISourceLoadedCallback)},
220 * and executes the given {@link IAutoInstallTask} on the current package list.
221 * That is for each package known, the install task is queried to find if
222 * the package is the one to be installed or updated.
224 * - If an already installed package is accepted by the task, it is returned. <br/>
225 * - If a new package (remotely available but not installed locally) is accepted,
226 * the user will be <em>prompted</em> for permission to install it. <br/>
227 * - If an existing package has updates, the install task will be accept if it
228 * accepts one of the updating packages, and if yes the the user will be
229 * <em>prompted</em> for permission to install it. <br/>
231 * Only one package can be accepted, after which the task is completed.
232 * There is no direct return value, {@link IAutoInstallTask#setResult} is called on the
233 * result of the accepted package.
234 * When the task is completed, {@link IAutoInstallTask#taskCompleted()} is called.
236 * <em>Important</em>: Since some UI will be displayed to install the selected package,
237 * the {@link UpdaterData} must have a window {@link Shell} associated using
238 * {@link UpdaterData#setWindowShell(Shell)}.
240 * The call is blocking. Although the name says "Task", this is not an {@link ITask}
241 * running in its own thread but merely a synchronous call.
243 * @param installTask The task to perform.
245 public void loadPackagesWithInstallTask(final IAutoInstallTask installTask) {
247 loadPackages(new ISourceLoadedCallback() {
248 public boolean onSourceLoaded(List<PkgItem> pkgItems) {
249 for (PkgItem item : pkgItems) {
250 Package acceptedPkg = null;
251 switch(item.getState()) {
253 if (installTask.acceptPackage(item.getMainPackage())) {
254 acceptedPkg = item.getMainPackage();
256 if (item.hasUpdatePkg() && installTask.acceptPackage(item.getUpdatePkg())) {
257 acceptedPkg = item.getUpdatePkg();
261 if (installTask.acceptPackage(item.getMainPackage())) {
262 // If the caller is accepting an installed package,
263 // return a success and give the package's install path
264 acceptedPkg = item.getMainPackage();
265 Archive[] a = acceptedPkg.getArchives();
266 // an installed package should have one local compatible archive
267 if (a.length == 1 && a[0].isCompatible()) {
268 installTask.setResult(
271 new File(a[0].getLocalOsPath()));
273 // return false to tell loadPackages() that we don't
274 // need to continue processing any more sources.
280 if (acceptedPkg != null) {
281 // Try to install this package if it has one compatible archive.
282 Archive archiveToInstall = null;
284 for (Archive a2 : acceptedPkg.getArchives()) {
285 if (a2.isCompatible()) {
286 archiveToInstall = a2;
291 if (archiveToInstall != null) {
292 installArchive(archiveToInstall);
295 // return false to tell loadPackages() that we don't
296 // need to continue processing any more sources.
301 // Tell loadPackages() to process the next source.
306 * Shows the UI of the install selector.
307 * If the package is then actually installed, refresh the local list and
308 * notify the install task of the installation path.
310 * @param archiveToInstall The archive to install.
312 private void installArchive(Archive archiveToInstall) {
313 // What we want to install
314 final ArrayList<Archive> archivesToInstall = new ArrayList<Archive>();
315 archivesToInstall.add(archiveToInstall);
317 Package packageToInstall = archiveToInstall.getParentPackage();
319 // What we'll end up installing
320 final Archive[] installedArchive = new Archive[] { null };
322 // Actually install the new archive that we just found.
323 // This will display some UI so we need a shell's sync exec.
325 mUpdaterData.getWindowShell().getDisplay().syncExec(new Runnable() {
327 List<Archive> archives =
328 mUpdaterData.updateOrInstallAll_WithGUI(
330 true /* includeObsoletes */);
332 if (archives != null && !archives.isEmpty()) {
333 // We expect that at most one archive has been installed.
334 assert archives.size() == 1;
335 installedArchive[0] = archives.get(0);
340 // If the desired package has been installed...
341 if (installedArchive[0] == archiveToInstall) {
343 // The local package list has changed, make sure to refresh it
344 mUpdaterData.getLocalSdkParser().clearPackages();
345 final List<PkgItem> localPkgItems = loadLocalPackages();
347 // Try to locate the installed package in the new package list
348 for (PkgItem localItem : localPkgItems) {
349 Package localPkg = localItem.getMainPackage();
350 if (localPkg.canBeUpdatedBy(packageToInstall) == UpdateInfo.NOT_UPDATE) {
351 Archive[] localArchive = localPkg.getArchives();
352 if (localArchive.length == 1 && localArchive[0].isCompatible()) {
353 installTask.setResult(
356 new File(localArchive[0].getLocalOsPath()));
363 // We failed to install the package.
364 installTask.setResult(packageToInstall, false /*success*/, null);
367 public void onLoadCompleted() {
368 installTask.taskCompleted();
375 * The state of the a given {@link PkgItem}, that is the relationship between
376 * a given remote package and the local repository.
378 public enum PkgState {
380 * Package is locally installed and may or may not have an update.
385 * There's a new package available on the remote site that isn't installed locally.
391 * A {@link PkgItem} represents one main {@link Package} combined with its state
392 * and an optional update package.
394 * The main package is final and cannot change since it's what "defines" this PkgItem.
395 * The state or update package can change later.
397 public static class PkgItem implements Comparable<PkgItem> {
398 private PkgState mState;
399 private final Package mMainPkg;
400 private Package mUpdatePkg;
403 * Create a new {@link PkgItem} for this main package.
404 * The main package is final and cannot change since it's what "defines" this PkgItem.
405 * The state or update package can change later.
407 public PkgItem(Package mainPkg, PkgState state) {
410 assert mMainPkg != null;
413 public boolean isObsolete() {
414 return mMainPkg.isObsolete();
417 public Package getUpdatePkg() {
421 public boolean hasUpdatePkg() {
422 return mState == PkgState.INSTALLED && mUpdatePkg != null;
425 public String getName() {
426 return mMainPkg.getListDescription();
429 public int getRevision() {
430 return mMainPkg.getRevision();
433 public String getDescription() {
434 return mMainPkg.getDescription();
437 public Package getMainPackage() {
441 public PkgState getState() {
445 public SdkSource getSource() {
446 return mMainPkg.getParentSource();
449 public int getApi() {
450 return mMainPkg instanceof IPackageVersion ?
451 ((IPackageVersion) mMainPkg).getVersion().getApiLevel() :
455 public Archive[] getArchives() {
456 return mMainPkg.getArchives();
459 public int compareTo(PkgItem pkg) {
460 return getMainPackage().compareTo(pkg.getMainPackage());
464 * Returns true if this package or its updating packages contains
465 * the exact given archive.
466 * Important: This compares object references, not object equality.
468 public boolean hasArchive(Archive archive) {
469 if (mMainPkg.hasArchive(archive)) {
472 if (mUpdatePkg != null && mUpdatePkg.hasArchive(archive)) {
479 * Checks whether the main packages are of the same type and are
480 * not an update of each other.
482 public boolean isSameMainPackageAs(Package pkg) {
483 if (mMainPkg.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE) {
484 // package revision numbers must match
485 return mMainPkg.getRevision() == pkg.getRevision();
491 * Checks whether too {@link PkgItem} are the same.
492 * This checks both items have the same state, both main package are similar
493 * and that they have the same updating packages.
495 public boolean isSameItemAs(PkgItem item) {
499 boolean same = this.mState == item.mState;
501 same = isSameMainPackageAs(item.getMainPackage());
505 // check updating packages are the same
506 Package p1 = this.mUpdatePkg;
507 Package p2 = item.getUpdatePkg();
508 same = (p1 == p2) || (p1 == null && p2 == null) || (p1 != null && p2 != null);
510 if (same && p1 != null) {
511 same = p1.canBeUpdatedBy(p2) == UpdateInfo.NOT_UPDATE;
519 * Equality is defined as {@link #isSameItemAs(PkgItem)}: state, main package
520 * and update package must be the similar.
523 public boolean equals(Object obj) {
524 return (obj instanceof PkgItem) && this.isSameItemAs((PkgItem) obj);
528 public int hashCode() {
529 final int prime = 31;
531 result = prime * result + ((mState == null) ? 0 : mState.hashCode());
532 result = prime * result + ((mMainPkg == null) ? 0 : mMainPkg.hashCode());
533 result = prime * result + ((mUpdatePkg == null) ? 0 : mUpdatePkg.hashCode());
538 * Check whether the 'pkg' argument is an update for this package.
539 * If it is, record it as an updating package.
540 * If there's already an updating package, only keep the most recent update.
541 * Returns true if it is update (even if there was already an update and this
542 * ended up not being the most recent), false if incompatible or not an update.
544 * This should only be used for installed packages.
546 public boolean mergeUpdate(Package pkg) {
547 if (mUpdatePkg == pkg) {
550 if (mMainPkg.canBeUpdatedBy(pkg) == UpdateInfo.UPDATE) {
551 if (mUpdatePkg == null) {
553 } else if (mUpdatePkg.canBeUpdatedBy(pkg) == UpdateInfo.UPDATE) {
554 // If we have more than one, keep only the most recent update
563 public void removeUpdate() {
567 /** Returns a string representation of this item, useful when debugging. */
569 public String toString() {
570 StringBuilder sb = new StringBuilder();
572 sb.append(mState.toString());
574 if (mMainPkg != null) {
575 sb.append(", pkg:"); //$NON-NLS-1$
576 sb.append(mMainPkg.toString());
579 if (mUpdatePkg != null) {
580 sb.append(", updated by:"); //$NON-NLS-1$
581 sb.append(mUpdatePkg.toString());
585 return sb.toString();