2 * Copyright (C) 2009 The Android Open Source Project
\r
4 * Licensed under the Apache License, Version 2.0 (the "License");
\r
5 * you may not use this file except in compliance with the License.
\r
6 * You may obtain a copy of the License at
\r
8 * http://www.apache.org/licenses/LICENSE-2.0
\r
10 * Unless required by applicable law or agreed to in writing, software
\r
11 * distributed under the License is distributed on an "AS IS" BASIS,
\r
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\r
13 * See the License for the specific language governing permissions and
\r
14 * limitations under the License.
\r
17 package com.android.sdkuilib.internal.repository;
\r
19 import com.android.sdklib.AndroidVersion;
\r
20 import com.android.sdklib.internal.repository.AddonPackage;
\r
21 import com.android.sdklib.internal.repository.Archive;
\r
22 import com.android.sdklib.internal.repository.DocPackage;
\r
23 import com.android.sdklib.internal.repository.ExtraPackage;
\r
24 import com.android.sdklib.internal.repository.IPackageVersion;
\r
25 import com.android.sdklib.internal.repository.MinToolsPackage;
\r
26 import com.android.sdklib.internal.repository.Package;
\r
27 import com.android.sdklib.internal.repository.PlatformPackage;
\r
28 import com.android.sdklib.internal.repository.RepoSource;
\r
29 import com.android.sdklib.internal.repository.RepoSources;
\r
30 import com.android.sdklib.internal.repository.ToolPackage;
\r
31 import com.android.sdklib.internal.repository.Package.UpdateInfo;
\r
33 import java.util.ArrayList;
\r
34 import java.util.Collection;
\r
35 import java.util.HashMap;
\r
38 * The logic to compute which packages to install, based on the choices
\r
39 * made by the user. This adds dependent packages as needed.
\r
41 * When the user doesn't provide a selection, looks at local package to find
\r
42 * those that can be updated and compute dependencies too.
\r
44 class UpdaterLogic {
\r
47 * Compute which packages to install by taking the user selection
\r
48 * and adding dependent packages as needed.
\r
50 * When the user doesn't provide a selection, looks at local packages to find
\r
51 * those that can be updated and compute dependencies too.
\r
53 public ArrayList<ArchiveInfo> computeUpdates(
\r
54 Collection<Archive> selectedArchives,
\r
55 RepoSources sources,
\r
56 Package[] localPkgs) {
\r
58 ArrayList<ArchiveInfo> archives = new ArrayList<ArchiveInfo>();
\r
59 ArrayList<Package> remotePkgs = new ArrayList<Package>();
\r
60 RepoSource[] remoteSources = sources.getSources();
\r
62 // Create ArchiveInfos out of local (installed) packages.
\r
63 ArchiveInfo[] localArchives = createLocalArchives(localPkgs);
\r
65 if (selectedArchives == null) {
\r
66 selectedArchives = findUpdates(localArchives, remotePkgs, remoteSources);
\r
69 for (Archive a : selectedArchives) {
\r
76 false /*automated*/);
\r
83 * Finds new platforms that the user does not have in his/her local SDK
\r
84 * and adds them to the list of archives to install.
\r
86 public void addNewPlatforms(ArrayList<ArchiveInfo> archives,
\r
87 RepoSources sources,
\r
88 Package[] localPkgs) {
\r
90 // Create ArchiveInfos out of local (installed) packages.
\r
91 ArchiveInfo[] localArchives = createLocalArchives(localPkgs);
\r
93 // Find the highest platform installed
\r
94 float currentPlatformScore = 0;
\r
95 float currentAddonScore = 0;
\r
96 float currentDocScore = 0;
\r
97 HashMap<String, Float> currentExtraScore = new HashMap<String, Float>();
\r
98 for (Package p : localPkgs) {
\r
99 int rev = p.getRevision();
\r
101 boolean isPreview = false;
\r
102 if (p instanceof IPackageVersion) {
\r
103 AndroidVersion vers = ((IPackageVersion) p).getVersion();
\r
104 api = vers.getApiLevel();
\r
105 isPreview = vers.isPreview();
\r
108 // The score is 10*api + (1 if preview) + rev/100
\r
109 // This allows previews to rank above a non-preview and
\r
110 // allows revisions to rank appropriately.
\r
111 float score = api * 10 + (isPreview ? 1 : 0) + rev/100.f;
\r
113 if (p instanceof PlatformPackage) {
\r
114 currentPlatformScore = Math.max(currentPlatformScore, score);
\r
115 } else if (p instanceof AddonPackage) {
\r
116 currentAddonScore = Math.max(currentAddonScore, score);
\r
117 } else if (p instanceof ExtraPackage) {
\r
118 currentExtraScore.put(((ExtraPackage) p).getPath(), score);
\r
119 } else if (p instanceof DocPackage) {
\r
120 currentDocScore = Math.max(currentDocScore, score);
\r
124 RepoSource[] remoteSources = sources.getSources();
\r
125 ArrayList<Package> remotePkgs = new ArrayList<Package>();
\r
126 fetchRemotePackages(remotePkgs, remoteSources);
\r
128 Package suggestedDoc = null;
\r
130 for (Package p : remotePkgs) {
\r
131 int rev = p.getRevision();
\r
133 boolean isPreview = false;
\r
134 if (p instanceof IPackageVersion) {
\r
135 AndroidVersion vers = ((IPackageVersion) p).getVersion();
\r
136 api = vers.getApiLevel();
\r
137 isPreview = vers.isPreview();
\r
140 float score = api * 10 + (isPreview ? 1 : 0) + rev/100.f;
\r
142 boolean shouldAdd = false;
\r
143 if (p instanceof PlatformPackage) {
\r
144 shouldAdd = score > currentPlatformScore;
\r
145 } else if (p instanceof AddonPackage) {
\r
146 shouldAdd = score > currentAddonScore;
\r
147 } else if (p instanceof ExtraPackage) {
\r
148 String key = ((ExtraPackage) p).getPath();
\r
149 shouldAdd = !currentExtraScore.containsKey(key) ||
\r
150 score > currentExtraScore.get(key).floatValue();
\r
151 } else if (p instanceof DocPackage) {
\r
152 // We don't want all the doc, only the most recent one
\r
153 if (score > currentDocScore) {
\r
155 currentDocScore = score;
\r
160 // We should suggest this package for installation.
\r
161 for (Archive a : p.getArchives()) {
\r
162 if (a.isCompatible()) {
\r
165 null /*selectedArchives*/,
\r
169 true /*automated*/);
\r
175 if (suggestedDoc != null) {
\r
176 // We should suggest this package for installation.
\r
177 for (Archive a : suggestedDoc.getArchives()) {
\r
178 if (a.isCompatible()) {
\r
181 null /*selectedArchives*/,
\r
185 true /*automated*/);
\r
193 * Create a array of {@link ArchiveInfo} based on all local (already installed)
\r
194 * packages. The array is always non-null but may be empty.
\r
196 * The local {@link ArchiveInfo} are guaranteed to have one non-null archive
\r
197 * that you can retrieve using {@link ArchiveInfo#getNewArchive()}.
\r
199 protected ArchiveInfo[] createLocalArchives(Package[] localPkgs) {
\r
201 if (localPkgs != null) {
\r
202 ArrayList<ArchiveInfo> list = new ArrayList<ArchiveInfo>();
\r
203 for (Package p : localPkgs) {
\r
204 // Only accept packages that have one compatible archive.
\r
205 // Local package should have 1 and only 1 compatible archive anyway.
\r
206 for (Archive a : p.getArchives()) {
\r
207 if (a != null && a.isCompatible()) {
\r
208 // We create an "installed" archive info to wrap the local package.
\r
209 // Note that dependencies are not computed since right now we don't
\r
210 // deal with more than one level of dependencies and installed archives
\r
211 // are deemed implicitly accepted anyway.
\r
212 list.add(new LocalArchiveInfo(a));
\r
217 return list.toArray(new ArchiveInfo[list.size()]);
\r
220 return new ArchiveInfo[0];
\r
224 * Find suitable updates to all current local packages.
\r
226 private Collection<Archive> findUpdates(ArchiveInfo[] localArchives,
\r
227 ArrayList<Package> remotePkgs,
\r
228 RepoSource[] remoteSources) {
\r
229 ArrayList<Archive> updates = new ArrayList<Archive>();
\r
231 fetchRemotePackages(remotePkgs, remoteSources);
\r
233 for (ArchiveInfo ai : localArchives) {
\r
234 Archive na = ai.getNewArchive();
\r
238 Package localPkg = na.getParentPackage();
\r
240 for (Package remotePkg : remotePkgs) {
\r
241 if (localPkg.canBeUpdatedBy(remotePkg) == UpdateInfo.UPDATE) {
\r
242 // Found a suitable update. Only accept the remote package
\r
243 // if it provides at least one compatible archive.
\r
245 for (Archive a : remotePkg.getArchives()) {
\r
246 if (a.isCompatible()) {
\r
258 private ArchiveInfo insertArchive(Archive archive,
\r
259 ArrayList<ArchiveInfo> outArchives,
\r
260 Collection<Archive> selectedArchives,
\r
261 ArrayList<Package> remotePkgs,
\r
262 RepoSource[] remoteSources,
\r
263 ArchiveInfo[] localArchives,
\r
264 boolean automated) {
\r
265 Package p = archive.getParentPackage();
\r
267 // Is this an update?
\r
268 Archive updatedArchive = null;
\r
269 for (ArchiveInfo ai : localArchives) {
\r
270 Archive a = ai.getNewArchive();
\r
272 Package lp = a.getParentPackage();
\r
274 if (lp.canBeUpdatedBy(p) == UpdateInfo.UPDATE) {
\r
275 updatedArchive = a;
\r
280 // Find dependencies
\r
281 ArchiveInfo[] deps = findDependency(p,
\r
288 // Make sure it's not a dup
\r
289 ArchiveInfo ai = null;
\r
291 for (ArchiveInfo ai2 : outArchives) {
\r
292 Archive a2 = ai2.getNewArchive();
\r
293 if (a2 != null && a2.getParentPackage().sameItemAs(archive.getParentPackage())) {
\r
300 ai = new ArchiveInfo(
\r
301 archive, //newArchive
\r
302 updatedArchive, //replaced
\r
305 outArchives.add(ai);
\r
308 if (deps != null) {
\r
309 for (ArchiveInfo d : deps) {
\r
310 d.addDependencyFor(ai);
\r
317 private ArchiveInfo[] findDependency(Package pkg,
\r
318 ArrayList<ArchiveInfo> outArchives,
\r
319 Collection<Archive> selectedArchives,
\r
320 ArrayList<Package> remotePkgs,
\r
321 RepoSource[] remoteSources,
\r
322 ArchiveInfo[] localArchives) {
\r
324 // Current dependencies can be:
\r
325 // - addon: *always* depends on platform of same API level
\r
326 // - platform: *might* depends on tools of rev >= min-tools-rev
\r
327 // - extra: *might* depends on platform with api >= min-api-level
\r
329 if (pkg instanceof AddonPackage) {
\r
330 AddonPackage addon = (AddonPackage) pkg;
\r
332 ArchiveInfo ai = findPlatformDependency(
\r
341 return new ArchiveInfo[] { ai };
\r
344 } else if (pkg instanceof MinToolsPackage) {
\r
345 MinToolsPackage platformOrExtra = (MinToolsPackage) pkg;
\r
348 ArchiveInfo ai1 = findToolsDependency(
\r
356 n += ai1 == null ? 0 : 1;
\r
358 ArchiveInfo ai2 = null;
\r
359 if (pkg instanceof ExtraPackage) {
\r
360 ai2 = findExtraPlatformDependency(
\r
361 (ExtraPackage) pkg,
\r
369 n += ai2 == null ? 0 : 1;
\r
372 ArchiveInfo[] ais = new ArchiveInfo[n];
\r
373 ais[0] = ai1 != null ? ai1 : ai2;
\r
374 if (n > 1) ais[1] = ai2;
\r
383 * Resolves dependencies on tools.
\r
385 * A platform or an extra package can both have a min-tools-rev, in which case it
\r
386 * depends on having a tools package of the requested revision.
\r
387 * Finds the tools dependency. If found, add it to the list of things to install.
\r
388 * Returns the archive info dependency, if any.
\r
390 protected ArchiveInfo findToolsDependency(MinToolsPackage platformOrExtra,
\r
391 ArrayList<ArchiveInfo> outArchives,
\r
392 Collection<Archive> selectedArchives,
\r
393 ArrayList<Package> remotePkgs,
\r
394 RepoSource[] remoteSources,
\r
395 ArchiveInfo[] localArchives) {
\r
396 // This is the requirement to match.
\r
397 int rev = platformOrExtra.getMinToolsRevision();
\r
399 if (rev == MinToolsPackage.MIN_TOOLS_REV_NOT_SPECIFIED) {
\r
400 // Well actually there's no requirement.
\r
404 // First look in locally installed packages.
\r
405 for (ArchiveInfo ai : localArchives) {
\r
406 Archive a = ai.getNewArchive();
\r
408 Package p = a.getParentPackage();
\r
409 if (p instanceof ToolPackage) {
\r
410 if (((ToolPackage) p).getRevision() >= rev) {
\r
411 // We found one already installed.
\r
418 // Look in archives already scheduled for install
\r
419 for (ArchiveInfo ai : outArchives) {
\r
420 Archive a = ai.getNewArchive();
\r
422 Package p = a.getParentPackage();
\r
423 if (p instanceof ToolPackage) {
\r
424 if (((ToolPackage) p).getRevision() >= rev) {
\r
425 // The dependency is already scheduled for install, nothing else to do.
\r
432 // Otherwise look in the selected archives.
\r
433 if (selectedArchives != null) {
\r
434 for (Archive a : selectedArchives) {
\r
435 Package p = a.getParentPackage();
\r
436 if (p instanceof ToolPackage) {
\r
437 if (((ToolPackage) p).getRevision() >= rev) {
\r
438 // It's not already in the list of things to install, so add it now
\r
439 return insertArchive(a,
\r
445 true /*automated*/);
\r
451 // Finally nothing matched, so let's look at all available remote packages
\r
452 fetchRemotePackages(remotePkgs, remoteSources);
\r
453 for (Package p : remotePkgs) {
\r
454 if (p instanceof ToolPackage) {
\r
455 if (((ToolPackage) p).getRevision() >= rev) {
\r
456 // It's not already in the list of things to install, so add the
\r
457 // first compatible archive we can find.
\r
458 for (Archive a : p.getArchives()) {
\r
459 if (a.isCompatible()) {
\r
460 return insertArchive(a,
\r
466 true /*automated*/);
\r
473 // We end up here if nothing matches. We don't have a good platform to match.
\r
474 // We need to indicate this extra depends on a missing platform archive
\r
475 // so that it can be impossible to install later on.
\r
476 return new MissingToolArchiveInfo(rev);
\r
480 * Resolves dependencies on platform for an addon.
\r
482 * An addon depends on having a platform with the same API level.
\r
484 * Finds the platform dependency. If found, add it to the list of things to install.
\r
485 * Returns the archive info dependency, if any.
\r
487 protected ArchiveInfo findPlatformDependency(
\r
488 AddonPackage addon,
\r
489 ArrayList<ArchiveInfo> outArchives,
\r
490 Collection<Archive> selectedArchives,
\r
491 ArrayList<Package> remotePkgs,
\r
492 RepoSource[] remoteSources,
\r
493 ArchiveInfo[] localArchives) {
\r
494 // This is the requirement to match.
\r
495 AndroidVersion v = addon.getVersion();
\r
497 // Find a platform that would satisfy the requirement.
\r
499 // First look in locally installed packages.
\r
500 for (ArchiveInfo ai : localArchives) {
\r
501 Archive a = ai.getNewArchive();
\r
503 Package p = a.getParentPackage();
\r
504 if (p instanceof PlatformPackage) {
\r
505 if (v.equals(((PlatformPackage) p).getVersion())) {
\r
506 // We found one already installed.
\r
513 // Look in archives already scheduled for install
\r
514 for (ArchiveInfo ai : outArchives) {
\r
515 Archive a = ai.getNewArchive();
\r
517 Package p = a.getParentPackage();
\r
518 if (p instanceof PlatformPackage) {
\r
519 if (v.equals(((PlatformPackage) p).getVersion())) {
\r
520 // The dependency is already scheduled for install, nothing else to do.
\r
527 // Otherwise look in the selected archives.
\r
528 if (selectedArchives != null) {
\r
529 for (Archive a : selectedArchives) {
\r
530 Package p = a.getParentPackage();
\r
531 if (p instanceof PlatformPackage) {
\r
532 if (v.equals(((PlatformPackage) p).getVersion())) {
\r
533 // It's not already in the list of things to install, so add it now
\r
534 return insertArchive(a,
\r
540 true /*automated*/);
\r
546 // Finally nothing matched, so let's look at all available remote packages
\r
547 fetchRemotePackages(remotePkgs, remoteSources);
\r
548 for (Package p : remotePkgs) {
\r
549 if (p instanceof PlatformPackage) {
\r
550 if (v.equals(((PlatformPackage) p).getVersion())) {
\r
551 // It's not already in the list of things to install, so add the
\r
552 // first compatible archive we can find.
\r
553 for (Archive a : p.getArchives()) {
\r
554 if (a.isCompatible()) {
\r
555 return insertArchive(a,
\r
561 true /*automated*/);
\r
568 // We end up here if nothing matches. We don't have a good platform to match.
\r
569 // We need to indicate this addon depends on a missing platform archive
\r
570 // so that it can be impossible to install later on.
\r
571 return new MissingPlatformArchiveInfo(addon.getVersion());
\r
575 * Resolves platform dependencies for extras.
\r
576 * An extra depends on having a platform with a minimun API level.
\r
578 * We try to return the highest API level available above the specified minimum.
\r
579 * Note that installed packages have priority so if one installed platform satisfies
\r
580 * the dependency, we'll use it even if there's a higher API platform available but
\r
581 * not installed yet.
\r
583 * Finds the platform dependency. If found, add it to the list of things to install.
\r
584 * Returns the archive info dependency, if any.
\r
586 protected ArchiveInfo findExtraPlatformDependency(
\r
587 ExtraPackage extra,
\r
588 ArrayList<ArchiveInfo> outArchives,
\r
589 Collection<Archive> selectedArchives,
\r
590 ArrayList<Package> remotePkgs,
\r
591 RepoSource[] remoteSources,
\r
592 ArchiveInfo[] localArchives) {
\r
594 int api = extra.getMinApiLevel();
\r
596 if (api == ExtraPackage.MIN_API_LEVEL_NOT_SPECIFIED) {
\r
600 // Find a platform that would satisfy the requirement.
\r
602 // First look in locally installed packages.
\r
603 for (ArchiveInfo ai : localArchives) {
\r
604 Archive a = ai.getNewArchive();
\r
606 Package p = a.getParentPackage();
\r
607 if (p instanceof PlatformPackage) {
\r
608 if (((PlatformPackage) p).getVersion().isGreaterOrEqualThan(api)) {
\r
609 // We found one already installed.
\r
616 // Look in archives already scheduled for install
\r
618 ArchiveInfo foundAi = null;
\r
620 for (ArchiveInfo ai : outArchives) {
\r
621 Archive a = ai.getNewArchive();
\r
623 Package p = a.getParentPackage();
\r
624 if (p instanceof PlatformPackage) {
\r
625 if (((PlatformPackage) p).getVersion().isGreaterOrEqualThan(api)) {
\r
626 if (api > foundApi) {
\r
635 if (foundAi != null) {
\r
636 // The dependency is already scheduled for install, nothing else to do.
\r
640 // Otherwise look in the selected archives *or* available remote packages
\r
641 // and takes the best out of the two sets.
\r
643 Archive foundArchive = null;
\r
644 if (selectedArchives != null) {
\r
645 for (Archive a : selectedArchives) {
\r
646 Package p = a.getParentPackage();
\r
647 if (p instanceof PlatformPackage) {
\r
648 if (((PlatformPackage) p).getVersion().isGreaterOrEqualThan(api)) {
\r
649 if (api > foundApi) {
\r
658 // Finally nothing matched, so let's look at all available remote packages
\r
659 fetchRemotePackages(remotePkgs, remoteSources);
\r
660 for (Package p : remotePkgs) {
\r
661 if (p instanceof PlatformPackage) {
\r
662 if (((PlatformPackage) p).getVersion().isGreaterOrEqualThan(api)) {
\r
663 if (api > foundApi) {
\r
664 // It's not already in the list of things to install, so add the
\r
665 // first compatible archive we can find.
\r
666 for (Archive a : p.getArchives()) {
\r
667 if (a.isCompatible()) {
\r
677 if (foundArchive != null) {
\r
678 // It's not already in the list of things to install, so add it now
\r
679 return insertArchive(foundArchive,
\r
685 true /*automated*/);
\r
688 // We end up here if nothing matches. We don't have a good platform to match.
\r
689 // We need to indicate this extra depends on a missing platform archive
\r
690 // so that it can be impossible to install later on.
\r
691 return new MissingPlatformArchiveInfo(new AndroidVersion(api, null /*codename*/));
\r
694 /** Fetch all remote packages only if really needed. */
\r
695 protected void fetchRemotePackages(ArrayList<Package> remotePkgs, RepoSource[] remoteSources) {
\r
696 if (remotePkgs.size() > 0) {
\r
700 for (RepoSource remoteSrc : remoteSources) {
\r
701 Package[] pkgs = remoteSrc.getPackages();
\r
702 if (pkgs != null) {
\r
703 nextPackage: for (Package pkg : pkgs) {
\r
704 for (Archive a : pkg.getArchives()) {
\r
705 // Only add a package if it contains at least one compatible archive
\r
706 if (a.isCompatible()) {
\r
707 remotePkgs.add(pkg);
\r
708 continue nextPackage;
\r
718 * A {@link LocalArchiveInfo} is an {@link ArchiveInfo} that wraps an already installed
\r
719 * "local" package/archive.
\r
721 * In this case, the "new Archive" is still expected to be non null and the
\r
722 * "replaced Archive" isnull. Installed archives are always accepted and never
\r
725 * Dependencies are not set.
\r
727 private static class LocalArchiveInfo extends ArchiveInfo {
\r
729 public LocalArchiveInfo(Archive localArchive) {
\r
730 super(localArchive, null /*replaced*/, null /*dependsOn*/);
\r
733 /** Installed archives are always accepted. */
\r
735 public boolean isAccepted() {
\r
739 /** Installed archives are never rejected. */
\r
741 public boolean isRejected() {
\r
747 * A {@link MissingPlatformArchiveInfo} is an {@link ArchiveInfo} that represents a
\r
748 * package/archive that we <em>really</em> need as a dependency but that we don't have.
\r
750 * This is currently used for addons and extras in case we can't find a matching base platform.
\r
752 * This kind of archive has specific properties: the new archive to install is null,
\r
753 * there are no dependencies and no archive is being replaced. The info can never be
\r
754 * accepted and is always rejected.
\r
756 private static class MissingPlatformArchiveInfo extends ArchiveInfo {
\r
758 private final AndroidVersion mVersion;
\r
761 * Constructs a {@link MissingPlatformArchiveInfo} that will indicate the
\r
762 * given platform version is missing.
\r
764 public MissingPlatformArchiveInfo(AndroidVersion version) {
\r
765 super(null /*newArchive*/, null /*replaced*/, null /*dependsOn*/);
\r
766 mVersion = version;
\r
769 /** Missing archives are never accepted. */
\r
771 public boolean isAccepted() {
\r
775 /** Missing archives are always rejected. */
\r
777 public boolean isRejected() {
\r
782 public String getShortDescription() {
\r
783 return String.format("Missing SDK Platform Android%1$s, API %2$d",
\r
784 mVersion.isPreview() ? " Preview" : "",
\r
785 mVersion.getApiLevel());
\r
790 * A {@link MissingToolArchiveInfo} is an {@link ArchiveInfo} that represents a
\r
791 * package/archive that we <em>really</em> need as a dependency but that we don't have.
\r
793 * This is currently used for extras in case we can't find a matching tool revision.
\r
795 * This kind of archive has specific properties: the new archive to install is null,
\r
796 * there are no dependencies and no archive is being replaced. The info can never be
\r
797 * accepted and is always rejected.
\r
799 private static class MissingToolArchiveInfo extends ArchiveInfo {
\r
801 private final int mRevision;
\r
804 * Constructs a {@link MissingPlatformArchiveInfo} that will indicate the
\r
805 * given platform version is missing.
\r
807 public MissingToolArchiveInfo(int revision) {
\r
808 super(null /*newArchive*/, null /*replaced*/, null /*dependsOn*/);
\r
809 mRevision = revision;
\r
812 /** Missing archives are never accepted. */
\r
814 public boolean isAccepted() {
\r
818 /** Missing archives are always rejected. */
\r
820 public boolean isRejected() {
\r
825 public String getShortDescription() {
\r
826 return String.format("Missing Android SDK Tools, revision %1$d", mRevision);
\r