blob: dbe844e77093557ffebeb97990234edc76a79e34 [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.welcome.wizard;
import com.android.SdkConstants;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.SdkManager;
import com.android.sdklib.repository.descriptors.IPkgDesc;
import com.android.sdklib.repository.descriptors.PkgType;
import com.android.tools.idea.sdk.IdeSdks;
import com.android.tools.idea.sdk.SdkMerger;
import com.android.tools.idea.sdk.remote.RemotePkgInfo;
import com.android.tools.idea.welcome.config.AndroidFirstRunPersistentData;
import com.android.tools.idea.welcome.config.FirstRunWizardMode;
import com.android.tools.idea.welcome.install.*;
import com.android.tools.idea.wizard.dynamic.DynamicWizardPath;
import com.android.tools.idea.wizard.dynamic.DynamicWizardStep;
import com.android.tools.idea.wizard.dynamic.ScopedStateStore;
import com.android.tools.idea.wizard.WizardConstants;
import com.android.utils.NullLogger;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.*;
import com.google.common.base.Supplier;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.util.io.FileUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.*;
/**
* Wizard path that manages component installation flow. It will prompt the user
* for the components to install and for install parameters. On wizard
* completion it will download and unzip component archives and will
* perform component setup.
*/
public class InstallComponentsPath extends DynamicWizardPath implements LongRunningOperationPath {
public static final AndroidVersion LATEST_ANDROID_VERSION = new AndroidVersion(22, null);
private static final ScopedStateStore.Key<String> KEY_SDK_INSTALL_LOCATION =
ScopedStateStore.createKey("download.sdk.location", ScopedStateStore.Scope.PATH, String.class);
private final ProgressStep myProgressStep;
@NotNull private final FirstRunWizardMode myMode;
@NotNull private final ComponentInstaller myComponentInstaller;
@NotNull private final File mySdkLocation;
private SdkComponentsStep mySdkComponentsStep;
private ComponentTreeNode myComponentTree;
@Nullable private final Multimap<PkgType, RemotePkgInfo> myRemotePackages;
public InstallComponentsPath(@NotNull ProgressStep progressStep,
@NotNull FirstRunWizardMode mode,
@NotNull File sdkLocation,
@Nullable Multimap<PkgType, RemotePkgInfo> remotePackages) {
myProgressStep = progressStep;
myMode = mode;
mySdkLocation = sdkLocation;
myRemotePackages = remotePackages;
myComponentInstaller = new ComponentInstaller(remotePackages);
}
private ComponentTreeNode createComponentTree(@NotNull FirstRunWizardMode reason, @NotNull ScopedStateStore stateStore, boolean createAvd) {
List<ComponentTreeNode> components = Lists.newArrayList();
components.add(new AndroidSdk(stateStore));
ComponentTreeNode platforms = Platform.createSubtree(stateStore, myRemotePackages);
if (platforms != null) {
components.add(platforms);
}
if (Haxm.canRun() && reason == FirstRunWizardMode.NEW_INSTALL) {
components.add(new Haxm(stateStore, FirstRunWizard.KEY_CUSTOM_INSTALL));
}
if (createAvd) {
components.add(new AndroidVirtualDevice(stateStore, myRemotePackages));
}
return new ComponentCategory("Root", "Root node that is not supposed to appear in the UI", components);
}
private static File createTempDir() throws WizardException {
File tempDirectory;
try {
tempDirectory = FileUtil.createTempDirectory("AndroidStudio", "FirstRun", true);
}
catch (IOException e) {
throw new WizardException("Unable to create temporary folder: " + e.getMessage(), e);
}
return tempDirectory;
}
private static boolean hasPlatformsDir(@Nullable File[] files) {
if (files == null) {
return false;
}
for (File file : files) {
if (isPlatformsDir(file)) {
return true;
}
}
return false;
}
private static boolean isPlatformsDir(File file) {
return file.isDirectory() && file.getName().equalsIgnoreCase(SdkConstants.FD_PLATFORMS);
}
/**
* This is an attempt to isolate from SDK packaging peculiarities.
*/
@NotNull
private static File getSdkRoot(@NotNull File expandedLocation) {
File[] files = expandedLocation.listFiles();
// Breadth-first scan - to lower chance of false positive
if (hasPlatformsDir(files)) {
return expandedLocation;
}
// Only scan one level down (no recursion) - avoid false positives
if (files != null) {
for (File file : files) {
if (hasPlatformsDir(file.listFiles())) {
return file;
}
}
}
return expandedLocation;
}
/**
* @return null if the user cancels from the UI
*/
@NotNull
@VisibleForTesting
static InstallOperation<File, File> downloadAndUnzipSdkSeed(@NotNull InstallContext context,
@NotNull final File destination,
double progressShare) {
final double DOWNLOAD_OPERATION_PROGRESS_SHARE = progressShare * 0.8;
final double UNZIP_OPERATION_PROGRESS_SHARE = progressShare * 0.15;
final double MOVE_OPERATION_PROGRESS_SHARE = progressShare - DOWNLOAD_OPERATION_PROGRESS_SHARE - UNZIP_OPERATION_PROGRESS_SHARE;
DownloadOperation download =
new DownloadOperation(context, FirstRunWizardDefaults.getSdkDownloadUrl(), DOWNLOAD_OPERATION_PROGRESS_SHARE);
UnpackOperation unpack = new UnpackOperation(context, UNZIP_OPERATION_PROGRESS_SHARE);
MoveSdkOperation move = new MoveSdkOperation(context, destination, MOVE_OPERATION_PROGRESS_SHARE);
return download.then(unpack).then(move);
}
private static boolean existsAndIsVisible(DynamicWizardStep step) {
return step != null && step.isStepVisible();
}
@Nullable
private File getHandoffAndroidSdkSource() {
File androidSrc = myMode.getAndroidSrc();
if (androidSrc != null) {
File[] files = androidSrc.listFiles();
if (androidSrc.isDirectory() && files != null && files.length > 0) {
return androidSrc;
}
}
return null;
}
/**
* <p>Creates an operation that will prepare SDK so the components can be installed.</p>
* <p>Supported scenarios:</p>
* <ol>
* <li>Install wizard leaves SDK repository to merge - merge will happen whether destination exists or not.</li>
* <li>Valid SDK at destination - do nothing, the wizard will update components later</li>
* <li>No handoff, no valid SDK at destination - SDK "seed" will be downloaded and unpacked</li>
* </ol>
*
* @return install operation object that will perform the setup
*/
private InstallOperation<File, File> createInitSdkOperation(InstallContext installContext, File destination, double progressRatio) {
File handoffSource = getHandoffAndroidSdkSource();
if (handoffSource != null) {
return new MergeOperation(handoffSource, installContext, progressRatio);
}
if (isNonEmptyDirectory(destination)) {
SdkManager manager = SdkManager.createManager(destination.getAbsolutePath(), new NullLogger());
if (manager != null) {
// We have SDK, first operation simply passes path through
return InstallOperation.wrap(installContext, new ReturnValue(), 0);
}
}
return downloadAndUnzipSdkSeed(installContext, destination, progressRatio);
}
private static boolean isNonEmptyDirectory(File file) {
String[] contents = !file.isDirectory() ? null : file.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return !(name.equalsIgnoreCase(".DS_Store") || name.equalsIgnoreCase("thumbs.db") || name.equalsIgnoreCase("desktop.ini"));
}
});
return contents != null && contents.length > 0;
}
@Override
protected void init() {
boolean createAvd = myMode.shouldCreateAvd();
String pathString = mySdkLocation.getAbsolutePath();
myState.put(KEY_SDK_INSTALL_LOCATION, pathString);
myComponentTree = createComponentTree(myMode, myState, createAvd);
myComponentTree.init(myProgressStep);
mySdkComponentsStep = new SdkComponentsStep(myComponentTree, FirstRunWizard.KEY_CUSTOM_INSTALL, KEY_SDK_INSTALL_LOCATION, myMode);
addStep(mySdkComponentsStep);
SdkManager manager = SdkManager.createManager(pathString, new NullLogger());
myComponentTree.init(myProgressStep);
myComponentTree.updateState(manager);
for (DynamicWizardStep step : myComponentTree.createSteps()) {
addStep(step);
}
if (myMode != FirstRunWizardMode.INSTALL_HANDOFF) {
addStep(new InstallSummaryStep(FirstRunWizard.KEY_CUSTOM_INSTALL, KEY_SDK_INSTALL_LOCATION, new Supplier<Collection<RemotePkgInfo>>() {
@Override
public Collection<RemotePkgInfo> get() {
return myComponentInstaller.getPackagesToInstallInfos(myState.get(KEY_SDK_INSTALL_LOCATION), myComponentTree.getChildrenToInstall());
}
}));
}
}
@Override
public void deriveValues(Set<ScopedStateStore.Key> modified) {
super.deriveValues(modified);
if (modified.contains(KEY_SDK_INSTALL_LOCATION)) {
String sdkPath = myState.get(KEY_SDK_INSTALL_LOCATION);
SdkManager manager = null;
if (sdkPath != null) {
manager = SdkManager.createManager(sdkPath, new NullLogger());
}
myComponentTree.updateState(manager);
}
if (modified.contains(FirstRunWizard.KEY_CUSTOM_INSTALL) || modified.contains(KEY_SDK_INSTALL_LOCATION) ||
myComponentTree.componentStateChanged(modified)) {
myState.put(WizardConstants.INSTALL_REQUESTS_KEY, getPackageDescriptions());
}
}
private List<IPkgDesc> getPackageDescriptions() {
Collection<RemotePkgInfo> installIds =
myComponentInstaller.getPackagesToInstallInfos(myState.get(KEY_SDK_INSTALL_LOCATION), myComponentTree.getChildrenToInstall());
ImmutableList.Builder<IPkgDesc> packages = ImmutableList.builder();
for (RemotePkgInfo remotePackage : installIds) {
packages.add(remotePackage.getPkgDesc());
}
return packages.build();
}
@NotNull
@Override
public String getPathName() {
return "Setup Android Studio Components";
}
@Override
public void runLongOperation() throws WizardException {
final double INIT_SDK_OPERATION_PROGRESS_SHARE = 0.3;
final double INSTALL_COMPONENTS_OPERATION_PROGRESS_SHARE = 1.0 - INIT_SDK_OPERATION_PROGRESS_SHARE;
final InstallContext installContext = new InstallContext(createTempDir(), myProgressStep);
final File destination = getDestination();
final InstallOperation<File, File> initialize = createInitSdkOperation(installContext, destination, INIT_SDK_OPERATION_PROGRESS_SHARE);
final Collection<? extends InstallableComponent> selectedComponents = myComponentTree.getChildrenToInstall();
CheckSdkOperation checkSdk = new CheckSdkOperation(installContext);
InstallComponentsOperation install =
new InstallComponentsOperation(installContext, selectedComponents, myComponentInstaller, INSTALL_COMPONENTS_OPERATION_PROGRESS_SHARE);
SetPreference setPreference = new SetPreference(myMode.getInstallerTimestamp());
try {
initialize.then(checkSdk).then(install).then(setPreference).then(new ConfigureComponents(installContext, selectedComponents))
.execute(destination);
}
catch (InstallationCancelledException e) {
installContext.print("Android Studio setup was canceled", ConsoleViewContentType.ERROR_OUTPUT);
}
}
public static RemotePkgInfo findLatestPlatform(Multimap<PkgType, RemotePkgInfo> remotePackages, boolean preview) {
List<RemotePkgInfo> packages = Lists.newArrayList(remotePackages.get(PkgType.PKG_PLATFORM));
Collections.sort(packages);
Collections.reverse(packages);
RemotePkgInfo latest = null;
for (RemotePkgInfo pkg : packages) {
boolean isPreview = pkg.getPkgDesc().getAndroidVersion().isPreview();
if (preview) {
if (isPreview) {
latest = pkg;
}
// if it's not a preview, there isn't a preview more recent than the latest non-preview. return null.
break;
}
else if (!isPreview) {
latest = pkg;
break;
}
}
return latest;
}
@NotNull
private File getDestination() throws WizardException {
String destinationPath = myState.get(KEY_SDK_INSTALL_LOCATION);
assert destinationPath != null;
final File destination = new File(destinationPath);
if (destination.isFile()) {
throw new WizardException(String.format("Path %s does not point to a directory", destination));
}
return destination;
}
@Override
public boolean performFinishingActions() {
// Everything happens after wizard completion
return true;
}
@Override
public boolean isPathVisible() {
return true;
}
public boolean showsStep() {
return isPathVisible() && (existsAndIsVisible(mySdkComponentsStep) || myMode == FirstRunWizardMode.NEW_INSTALL);
}
private static class MergeOperation extends InstallOperation<File, File> {
private final File myRepo;
private final InstallContext myContext;
private boolean myRepoWasMerged = false;
public MergeOperation(File repo, InstallContext context, double progressRatio) {
super(context, progressRatio);
myRepo = repo;
myContext = context;
}
@NotNull
@Override
protected File perform(@NotNull ProgressIndicator indicator, @NotNull File destination) throws WizardException {
indicator.setText("Installing Android SDK");
try {
FileUtil.ensureExists(destination);
if (!FileUtil.filesEqual(destination.getCanonicalFile(), myRepo.getCanonicalFile())) {
SdkMerger.mergeSdks(myRepo, destination, indicator);
myRepoWasMerged = true;
}
myContext.print(String.format("Android SDK was installed to %1$s\n", destination), ConsoleViewContentType.SYSTEM_OUTPUT);
return destination;
}
catch (IOException e) {
throw new WizardException(e.getMessage(), e);
}
finally {
indicator.stop();
}
}
@Override
public void cleanup(@NotNull File result) {
if (myRepoWasMerged && myRepo.exists()) {
FileUtil.delete(myRepo);
}
}
}
private static class MoveSdkOperation extends InstallOperation<File, File> {
@NotNull private final File myDestination;
public MoveSdkOperation(@NotNull InstallContext context, @NotNull File destination, double progressShare) {
super(context, progressShare);
myDestination = destination;
}
@NotNull
@Override
protected File perform(@NotNull ProgressIndicator indicator, @NotNull File file) throws WizardException {
indicator.setText("Moving downloaded SDK");
indicator.start();
try {
File root = getSdkRoot(file);
if (!root.renameTo(myDestination)) {
FileUtil.copyDir(root, myDestination);
FileUtil.delete(root); // Failure to delete it is not critical, the source is in temp folder.
// No need to abort installation.
}
return myDestination;
}
catch (IOException e) {
throw new WizardException("Unable to move Android SDK", e);
}
finally {
indicator.setFraction(1.0);
indicator.stop();
}
}
@Override
public void cleanup(@NotNull File result) {
// Do nothing
}
}
private static class ReturnValue implements Function<File, File> {
@Override
public File apply(@Nullable File input) {
assert input != null;
return input;
}
}
private static class SetPreference implements Function<File, File> {
@Nullable private final String myInstallerTimestamp;
public SetPreference(@Nullable String installerTimestamp) {
myInstallerTimestamp = installerTimestamp;
}
@Override
public File apply(@Nullable final File input) {
assert input != null;
final Application application = ApplicationManager.getApplication();
// SDK can only be set from write action, write action can only be started from UI thread
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override
public void run() {
application.runWriteAction(new Runnable() {
@Override
public void run() {
IdeSdks.setAndroidSdkPath(input, null);
AndroidFirstRunPersistentData.getInstance().markSdkUpToDate(myInstallerTimestamp);
}
});
}
}, application.getAnyModalityState());
return input;
}
}
private static class ConfigureComponents implements Function<File, File> {
private final InstallContext myInstallContext;
private final Collection<? extends InstallableComponent> mySelectedComponents;
public ConfigureComponents(InstallContext installContext, Collection<? extends InstallableComponent> selectedComponents) {
myInstallContext = installContext;
mySelectedComponents = selectedComponents;
}
@Override
public File apply(@Nullable File input) {
assert input != null;
for (InstallableComponent component : mySelectedComponents) {
component.configure(myInstallContext, input);
}
return input;
}
}
}