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