/*
* 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.sdk.wizard;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.SdkManager;
import com.android.sdklib.internal.repository.updater.SdkUpdaterNoWindow;
import com.android.sdklib.repository.descriptors.IPkgDesc;
import com.android.sdklib.repository.descriptors.PkgType;
import com.android.tools.idea.sdk.SdkState;
import com.android.tools.idea.wizard.DynamicWizardStepWithHeaderAndDescription;
import com.android.utils.ILogger;
import com.android.utils.IReaderLogger;
import com.google.common.collect.Lists;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.PerformInBackgroundOption;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.android.sdk.AndroidSdkData;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.android.tools.idea.wizard.WizardConstants.NEWLY_INSTALLED_API_KEY;
import static com.android.tools.idea.wizard.WizardConstants.INSTALL_REQUESTS_KEY;
public class SmwOldApiDirectInstall extends DynamicWizardStepWithHeaderAndDescription {
private Logger LOG = Logger.getInstance(SmwOldApiDirectInstall.class);
private JBLabel myLabelSdkPath;
private JTextArea myTextArea1;
private JBLabel myLabelProgress1;
private JProgressBar myProgressBar;
private JBLabel myLabelProgress2;
private JLabel myErrorLabel;
private JPanel myContentPanel;
private boolean myInstallFinished;
private Boolean myBackgroundSuccess = null;
private boolean myBeforeInstall = true;
public SmwOldApiDirectInstall(@NotNull Disposable disposable) {
super("Installing Requested Components", "", null, disposable);
setBodyComponent(myContentPanel);
}
@Override
public void onEnterStep() {
super.onEnterStep();
myTextArea1.setText("");
startSdkInstallUsingNonSwtOldApi();
}
@Override
public boolean isStepVisible() {
return myState.listSize(INSTALL_REQUESTS_KEY) > 0;
}
@Override
public boolean validate() {
return myInstallFinished;
}
@Override
public boolean canGoPrevious() {
return myInstallFinished;
}
//-----------
private void startSdkInstallUsingNonSwtOldApi() {
// Get the SDK instance.
final AndroidSdkData sdkData = AndroidSdkUtils.tryToChooseAndroidSdk();
SdkState sdkState = sdkData == null ? null : SdkState.getInstance(sdkData);
if (sdkState == null) {
myErrorLabel.setText("Error: can't get SDK instance.");
return;
}
myLabelSdkPath.setText(sdkData.getLocation().getPath());
final CustomLogger logger = new CustomLogger();
Runnable onSdkAvailable = new Runnable() {
@Override
public void run() {
// TODO: since the local SDK has been parsed, this is now a good time
// to filter requestedPackages to remove current installed packages.
// That's because on Windows trying to update some of the packages in-place
// *will* fail (e.g. typically the android platform or the tools) as the
// folder is most likely locked.
// As mentioned in InstallTask below, the shortcut we're taking here will
// install all the requested packages, even if already installed, which is
// useless so that's another incentive to remove already installed packages
// from the requested list.
final ArrayList<String> requestedPackages = Lists.newArrayList();
List requestedChanges = myState.get(INSTALL_REQUESTS_KEY);
if (requestedChanges == null) {
// This should never occur
myInstallFinished = true;
invokeUpdate(null);
return;
}
for (Object object : requestedChanges) {
try {
IPkgDesc packageDesc = (IPkgDesc)object;
if (packageDesc != null) {
// TODO use localSdk to filter list and remove already installed items
requestedPackages.add(packageDesc.getInstallId());
}
} catch (ClassCastException e) {
LOG.error(e);
}
}
InstallTask task = new InstallTask(sdkData, requestedPackages, logger);
BackgroundableProcessIndicator indicator = new BackgroundableProcessIndicator(task);
logger.setIndicator(indicator);
ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator);
}
};
// loadAsync checks if the timeout expired and/or loads the SDK if it's not loaded yet.
// If needed, it does a backgroundable Task to load the SDK and then calls onSdkAvailable.
// Otherwise it returns false, in which case we call onSdkAvailable ourselves.
logger.info("Loading SDK information...\n");
if (!sdkState.loadAsync(1000 * 3600 * 24, // 24 hour timeout since last check
false, // canBeCancelled
onSdkAvailable, // onSuccess
null)) { // onError -- TODO display something?
onSdkAvailable.run();
}
}
@NotNull
@Override
public String getStepName() {
return "InstallingSDKComponentsStep";
}
@Override
public JComponent getPreferredFocusedComponent() {
return null;
}
private class InstallTask extends Task.Backgroundable {
@NotNull private final AndroidSdkData mySdkData;
@NotNull private final ArrayList<String> myRequestedPackages;
@NotNull private final ILogger myLogger;
private InstallTask(@NotNull AndroidSdkData sdkData,
@NotNull ArrayList<String> requestedPackages,
@NotNull ILogger logger) {
super(null /*project*/,
"Installing Android SDK",
false /*canBeCancelled*/,
PerformInBackgroundOption.ALWAYS_BACKGROUND);
mySdkData = sdkData;
myRequestedPackages = requestedPackages;
myLogger = logger;
}
@Override
public void run(@NotNull ProgressIndicator indicator) {
// This runs in a background task and isn't interrupted.
// Perform the install by using the command-line interface and dumping the output into the logger.
// The command-line API is a bit archaic and has some drastic limitations, one of them being that
// it blindly re-install stuff even if already present IIRC.
myBeforeInstall = false;
String osSdkFolder = mySdkData.getLocation().getPath();
SdkManager sdkManager = SdkManager.createManager(osSdkFolder, myLogger);
SdkUpdaterNoWindow upd = new SdkUpdaterNoWindow(
osSdkFolder,
sdkManager,
myLogger,
false, // force -- The reply to any question asked by the update process.
// Currently this will be yes/no for ability to replace modified samples, restart ADB, restart on locked win folder.
false, // useHttp -- True to force using HTTP instead of HTTPS for downloads.
null, // proxyPort -- An optional HTTP/HTTPS proxy port. Can be null. -- Can we get it from Studio?
null); // proxyHost -- An optional HTTP/HTTPS proxy host. Can be null. -- Can we get it from Studio?
upd.updateAll(myRequestedPackages,
true, // all
false, // dryMode
null); // acceptLicense
while (myBackgroundSuccess == null) {
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
// Pass
}
}
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
myProgressBar.setValue(100);
myLabelProgress1.setText("");
if (!myBackgroundSuccess) {
myLabelProgress2.setText("<html>Install Failed. Please check your network connection and try again. " +
"You may continue with creating your project, but it will not compile correctly " +
"without the missing components.</html>");
myLabelProgress2.setForeground(JBColor.RED);
myProgressBar.setEnabled(false);
} else {
myLabelProgress2.setText("Done");
List requestedChanges = myState.get(INSTALL_REQUESTS_KEY);
checkForUpgrades(requestedChanges);
myState.remove(INSTALL_REQUESTS_KEY);
}
myInstallFinished = true;
invokeUpdate(null);
}
});
}
}
/**
* Look through the list of completed changes, and set a key if any new platforms
* were installed.
*/
private void checkForUpgrades(@Nullable List completedChanges) {
if (completedChanges == null) {
return;
}
int highestNewApiLevel = 0;
for (Object o : completedChanges) {
if (! (o instanceof IPkgDesc)) {
continue;
}
IPkgDesc pkgDesc = (IPkgDesc)o;
if (pkgDesc.getType().equals(PkgType.PKG_PLATFORM)) {
AndroidVersion version = pkgDesc.getAndroidVersion();
if (version != null && version.getApiLevel() > highestNewApiLevel) {
highestNewApiLevel = version.getApiLevel();
}
}
}
if (highestNewApiLevel > 0) {
myState.put(NEWLY_INSTALLED_API_KEY, highestNewApiLevel);
}
}
// Groups: 1=license-id
private static Pattern sLicenceText = Pattern.compile("^\\s*License id:\\s*([a-z0-9-]+).*\\s*");
// Groups: 1=progress values (%, ETA), 2=% int, 3=progress text
private static Pattern sProgress1Text = Pattern.compile("^\\s+\\((([0-9]+)%,\\s*[^)]*)\\)(.*)\\s*");
// Groups: 1=progress text, 2=progress values, 3=% int
private static Pattern sProgress2Text = Pattern.compile("^\\s+([^(]+)\\s+\\((([0-9]+)%)\\)\\s*");
private class CustomLogger implements IReaderLogger {
private BackgroundableProcessIndicator myIndicator;
private String myCurrLicense;
private String myLastLine;
public CustomLogger() {
}
public void setIndicator(BackgroundableProcessIndicator indicator) {
myIndicator = indicator;
}
/**
* Used by UpdaterData.acceptLicense() to prompt for license acceptance
* when updating the SDK from the command-line.
* <p/>
* {@inheritDoc}
*/
@Override
public int readLine(@NotNull byte[] inputBuffer) throws IOException {
if (myLastLine != null && myLastLine.contains("Do you accept the license")) {
// Let's take a simple shortcut and simply reply 'y' for yes.
inputBuffer[0] = 'y';
inputBuffer[1] = 0;
return 1;
}
inputBuffer[0] = 'n';
inputBuffer[1] = 0;
return 1;
}
@Override
public void error(@Nullable Throwable t,
@Nullable String msgFormat,
Object... args) {
if (msgFormat == null && t != null) {
if (myIndicator != null) myIndicator.setText2(t.toString());
outputLine(t.toString());
} else if (msgFormat != null) {
if (myIndicator != null) myIndicator.setText2(String.format(msgFormat, args));
outputLine(String.format(msgFormat, args));
}
}
@Override
public void warning(@NotNull String msgFormat, Object... args) {
if (myIndicator != null) myIndicator.setText2(String.format(msgFormat, args));
outputLine(String.format(msgFormat, args));
}
@Override
public void info(@NotNull String msgFormat, Object... args) {
if (myIndicator != null) myIndicator.setText2(String.format(msgFormat, args));
outputLine(String.format(msgFormat, args));
}
@Override
public void verbose(@NotNull String msgFormat, Object... args) {
// Don't log verbose stuff in the background indicator.
outputLine(String.format(msgFormat, args));
}
/**
* This method takes the console output from the command-line updater.
* It filters it to remove some verbose output that is not desirable here.
* It also detects progress-bar like text and updates the dialog's progress
* bar accordingly.
*/
private void outputLine(@NotNull String line) {
myLastLine = line;
try {
// skip some of the verbose output such as license text & refreshing http sources
Matcher m = sLicenceText.matcher(line);
if (m.matches()) {
myCurrLicense = m.group(1);
return;
}
else if (myCurrLicense != null) {
if (line.contains("Do you accept the license") && line.contains(myCurrLicense)) {
myCurrLicense = null;
}
return;
}
else if (line.contains("Fetching http") ||
line.contains("Fetching URL:") ||
line.contains("Validate XML") ||
line.contains("Parse XML") ||
line.contains("---------")) {
return;
}
int progInt = -1;
String progText2 = null;
String progText1 = null;
m = sProgress1Text.matcher(line);
if (m.matches()) {
// Groups: 1=progress values (%, ETA), 2=% int, 3=progress text
try {
progInt = Integer.parseInt(m.group(2));
}
catch (NumberFormatException ignore) {
progInt = 0;
}
progText1 = m.group(3);
progText2 = m.group(1);
line = null;
} else {
m = sProgress2Text.matcher(line);
if (m.matches()) {
// Groups: 1=progress text, 2=progress values, 3=% int
try {
progInt = Integer.parseInt(m.group(3));
}
catch (NumberFormatException ignore) {
progInt = 0;
}
progText1 = m.group(1);
progText2 = m.group(2);
line = null;
}
}
final int fProgInt = progInt;
final String fProgText2 = progText2;
final String fProgText1 = progText1;
final String fAddLine = line;
// This is invoked from a backgroundable task, only update text on the ui thread.
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
if (fAddLine != null) {
String current = myTextArea1.getText();
if (current == null) {
current = "";
}
myTextArea1.setText(current + fAddLine);
if (fAddLine.contains("Nothing was installed")) {
myBackgroundSuccess = false;
} else if (fAddLine.contains("Failed")) {
myBackgroundSuccess = false;
} else if (fAddLine.contains("Done") && !fAddLine.contains("othing")) {
myBackgroundSuccess = Boolean.TRUE;
}
}
if (fProgText1 != null) {
myLabelProgress1.setText(fProgText1);
}
if (fProgText2 != null) {
myLabelProgress2.setText(fProgText2);
}
if (fProgInt >= 0) {
myProgressBar.setValue(fProgInt);
}
}
});
} catch (Exception ignore) {}
}
}
}