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