blob: 4f739cb95a72e1149bdd95e4139bfaed440a971f [file] [log] [blame]
/*
* Copyright 2000-2010 JetBrains s.r.o.
*
* 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 org.jetbrains.android.run;
import com.android.annotations.concurrency.GuardedBy;
import com.android.ddmlib.*;
import com.android.prefs.AndroidLocation;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.internal.avd.AvdInfo;
import com.android.sdklib.internal.avd.AvdManager;
import com.android.tools.idea.ddms.DevicePanel;
import com.android.tools.idea.ddms.adb.AdbService;
import com.android.tools.idea.gradle.project.AndroidGradleNotification;
import com.android.tools.idea.gradle.service.notification.hyperlink.SyncProjectHyperlink;
import com.android.tools.idea.gradle.util.GradleUtil;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.monitor.AndroidToolWindowFactory;
import com.android.tools.idea.run.*;
import com.android.tools.idea.stats.UsageTracker;
import com.google.common.base.Charsets;
import com.google.common.collect.Sets;
import com.google.common.hash.Hashing;
import com.intellij.CommonBundle;
import com.intellij.execution.DefaultExecutionResult;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.ExecutionResult;
import com.intellij.execution.Executor;
import com.intellij.execution.configurations.RunProfileState;
import com.intellij.execution.executors.DefaultDebugExecutor;
import com.intellij.execution.filters.HyperlinkInfo;
import com.intellij.execution.filters.TextConsoleBuilder;
import com.intellij.execution.filters.TextConsoleBuilderFactory;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ExecutionUtil;
import com.intellij.execution.runners.ProgramRunner;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.ide.DataManager;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.content.Content;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.Consumer;
import com.intellij.util.ThreeState;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import com.intellij.xdebugger.DefaultDebugProcessHandler;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.facet.AvdsNotSupportedException;
import org.jetbrains.android.logcat.AndroidLogcatView;
import org.jetbrains.android.run.testing.AndroidTestRunConfiguration;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.android.sdk.AvdManagerLog;
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.android.util.AndroidOutputReceiver;
import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.android.tools.idea.run.CloudConfiguration.Kind.MATRIX;
import static com.android.tools.idea.run.CloudConfiguration.Kind.SINGLE_DEVICE;
import static com.intellij.execution.process.ProcessOutputTypes.STDERR;
import static com.intellij.execution.process.ProcessOutputTypes.STDOUT;
/**
* @author coyote
*/
public class AndroidRunningState implements RunProfileState, AndroidDebugBridge.IClientChangeListener, AndroidExecutionState {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.run.AndroidRunningState");
@NonNls private static final String ANDROID_TARGET_DEVICES_PROPERTY = "AndroidTargetDevices";
private static final IDevice[] EMPTY_DEVICE_ARRAY = new IDevice[0];
public static final int WAITING_TIME_SECS = 20;
private static final Pattern FAILURE = Pattern.compile("Failure\\s+\\[(.*)\\]");
private static final Pattern TYPED_ERROR = Pattern.compile("Error\\s+[Tt]ype\\s+(\\d+).*");
private static final String ERROR_PREFIX = "Error";
public static final int NO_ERROR = -2;
public static final int UNTYPED_ERROR = -1;
private final ApkProvider myApkProvider;
private String myTargetPackageName;
private final AndroidFacet myFacet;
private final String myCommandLine;
private final AndroidApplicationLauncher myApplicationLauncher;
private final AndroidRunConfigurationBase myConfiguration;
private final Object myDebugLock = new Object();
@NotNull
private volatile IDevice[] myTargetDevices = EMPTY_DEVICE_ARRAY;
private volatile String myAvdName;
private volatile boolean myDebugMode;
private volatile boolean myOpenLogcatAutomatically;
private volatile boolean myFilterLogcatAutomatically;
private volatile DebugLauncher myDebugLauncher;
private final ExecutionEnvironment myEnv;
private volatile boolean myStopped;
private volatile ProcessHandler myProcessHandler;
private final Object myLock = new Object();
private volatile boolean myDeploy = true;
private volatile boolean myApplicationDeployed = false;
private ConsoleView myConsole;
private TargetChooser myTargetChooser;
private final boolean mySupportMultipleDevices;
private final boolean myClearLogcatBeforeStart;
private final List<AndroidRunningStateListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private final boolean myNonDebuggableOnDevice;
public void setDebugMode(boolean debugMode) {
myDebugMode = debugMode;
}
public void setDebugLauncher(@NotNull DebugLauncher debugLauncher) {
myDebugLauncher = debugLauncher;
}
public boolean isDebugMode() {
return myDebugMode;
}
private static void runInDispatchedThread(@NotNull Runnable r, boolean blocking) {
Application application = ApplicationManager.getApplication();
if (application.isDispatchThread()) {
r.run();
}
else if (blocking) {
application.invokeAndWait(r, ModalityState.defaultModalityState());
}
else {
application.invokeLater(r);
}
}
@Override
public ExecutionResult execute(@NotNull Executor executor, @NotNull ProgramRunner runner) throws ExecutionException {
Project project = myFacet.getModule().getProject();
myProcessHandler = new DefaultDebugProcessHandler();
AndroidProcessText.attach(myProcessHandler);
ConsoleView console = null;
if (isDebugMode()) {
final TextConsoleBuilder builder = TextConsoleBuilderFactory.getInstance().createBuilder(project);
console = builder.getConsole();
if (console != null) {
console.attachToProcess(myProcessHandler);
}
}
final CloudConfigurationProvider provider = CloudConfigurationProvider.getCloudConfigurationProvider();
boolean debugMatrixOnCloud = myTargetChooser instanceof CloudTargetChooser &&
((CloudTargetChooser)myTargetChooser).getConfigurationKind() == MATRIX &&
executor instanceof DefaultDebugExecutor;
// Show the device chooser if either the config specifies it, or if the request is to debug a matrix of devices on cloud
// (which does not make sense).
if (myTargetChooser instanceof ManualTargetChooser || debugMatrixOnCloud) {
if (myConfiguration.USE_LAST_SELECTED_DEVICE) {
DeviceStateAtLaunch lastLaunchState = myConfiguration.getDevicesUsedInLastLaunch();
if (lastLaunchState != null) {
Set<IDevice> onlineDevices = getOnlineDevices();
if (lastLaunchState.matchesCurrentAvailableDevices(onlineDevices)) {
Collection<IDevice> usedDevices = lastLaunchState.filterByUsed(onlineDevices);
myTargetDevices = usedDevices.toArray(new IDevice[usedDevices.size()]);
}
}
if (myTargetDevices.length > 1 && !mySupportMultipleDevices) {
myTargetDevices = EMPTY_DEVICE_ARRAY;
}
}
if (myTargetDevices.length == 0) {
AndroidPlatform platform = myFacet.getConfiguration().getAndroidPlatform();
if (platform == null) {
LOG.error("Android platform not set for module: " + myFacet.getModule().getName());
return null;
}
boolean showCloudTarget = getConfiguration() instanceof AndroidTestRunConfiguration && !(executor instanceof DefaultDebugExecutor);
final ExtendedDeviceChooserDialog chooser =
new ExtendedDeviceChooserDialog(myFacet, platform.getTarget(), mySupportMultipleDevices, true,
myConfiguration.USE_LAST_SELECTED_DEVICE, showCloudTarget, myCommandLine);
chooser.show();
if (chooser.getExitCode() != DialogWrapper.OK_EXIT_CODE) {
return null;
}
if (chooser.isToLaunchEmulator()) {
final String selectedAvd = chooser.getSelectedAvd();
if (selectedAvd == null) {
return null;
}
myTargetChooser = new EmulatorTargetChooser(selectedAvd);
myAvdName = selectedAvd;
}
else if (chooser.isCloudTestOptionSelected()) {
return provider
.executeCloudMatrixTests(chooser.getSelectedMatrixConfigurationId(), chooser.getChosenCloudProjectId(), this, executor);
}
else {
final IDevice[] selectedDevices = chooser.getSelectedDevices();
if (selectedDevices.length == 0) {
return null;
}
myTargetDevices = selectedDevices;
if (chooser.useSameDevicesAgain()) {
myConfiguration.USE_LAST_SELECTED_DEVICE = true;
myConfiguration.setDevicesUsedInLaunch(Sets.newHashSet(selectedDevices), getOnlineDevices());
} else {
myConfiguration.USE_LAST_SELECTED_DEVICE = false;
myConfiguration.setDevicesUsedInLaunch(Collections.<IDevice>emptySet(), Collections.<IDevice>emptySet());
}
}
}
}
else if (myTargetChooser instanceof CloudTargetChooser) {
assert provider != null;
final CloudTargetChooser cloudTargetChooser = (CloudTargetChooser)myTargetChooser;
if (cloudTargetChooser.getConfigurationKind() == MATRIX) {
return provider
.executeCloudMatrixTests(cloudTargetChooser.getCloudConfigurationId(), cloudTargetChooser.getCloudProjectId(), this, executor);
}
else {
assert cloudTargetChooser.getConfigurationKind() == SINGLE_DEVICE;
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
provider.launchCloudDevice(cloudTargetChooser.getCloudConfigurationId(), cloudTargetChooser.getCloudProjectId(), myFacet);
}
});
return new DefaultExecutionResult(console, myProcessHandler);
}
}
else if (myTargetChooser instanceof CloudDebuggingTargetChooser) {
assert provider != null;
myTargetDevices = EMPTY_DEVICE_ARRAY;
String cloudDeviceSerialNumber = ((CloudDebuggingTargetChooser)myTargetChooser).getCloudDeviceSerialNumber();
for (IDevice device : AndroidDebugBridge.getBridge().getDevices()) {
if (device.getSerialNumber().equals(cloudDeviceSerialNumber)) {
myTargetDevices = new IDevice[] {device};
break;
}
}
if (myTargetDevices.length == 0) { // No matching cloud device available.
return null;
}
}
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
start(true);
}
});
if (console == null) { //Will not be null in debug mode or if additional option was chosen.
console = myConfiguration.attachConsole(this, executor);
}
myConsole = console;
return new DefaultExecutionResult(console, myProcessHandler);
}
private Set<IDevice> getOnlineDevices() {
AndroidDebugBridge debugBridge = AndroidSdkUtils.getDebugBridge(myFacet.getModule().getProject());
if (debugBridge == null) {
return Collections.emptySet();
}
return Sets.newHashSet(debugBridge.getDevices());
}
@Override
@NotNull
public AndroidRunConfigurationBase getConfiguration() {
return myConfiguration;
}
public ExecutionEnvironment getEnvironment() {
return myEnv;
}
public boolean isStopped() {
return myStopped;
}
public Object getRunningLock() {
return myLock;
}
public String getPackageName() {
try {
return myApkProvider.getPackageName();
} catch (ApkProvisionException e) {
return null;
}
}
public String getTestPackageName() {
try {
return myApkProvider.getTestPackageName();
} catch (ApkProvisionException e) {
return null;
}
}
public Module getModule() {
return myFacet.getModule();
}
@NotNull
public AndroidFacet getFacet() {
return myFacet;
}
@Override
public IDevice[] getDevices() {
return myTargetDevices;
}
@Nullable
@Override
public ConsoleView getConsoleView() {
return myConsole;
}
public class MyReceiver extends AndroidOutputReceiver {
private int errorType = NO_ERROR;
private String failureMessage = null;
private final StringBuilder output = new StringBuilder();
@Override
protected void processNewLine(String line) {
if (line.length() > 0) {
Matcher failureMatcher = FAILURE.matcher(line);
if (failureMatcher.matches()) {
failureMessage = failureMatcher.group(1);
}
Matcher errorMatcher = TYPED_ERROR.matcher(line);
if (errorMatcher.matches()) {
errorType = Integer.parseInt(errorMatcher.group(1));
}
else if (line.startsWith(ERROR_PREFIX) && errorType == NO_ERROR) {
errorType = UNTYPED_ERROR;
}
}
output.append(line).append('\n');
}
public int getErrorType() {
return errorType;
}
@Override
public boolean isCancelled() {
return myStopped;
}
public StringBuilder getOutput() {
return output;
}
}
public AndroidRunningState(@NotNull ExecutionEnvironment environment,
@NotNull AndroidFacet facet,
@NotNull ApkProvider apkProvider,
@Nullable TargetChooser targetChooser,
@NotNull String commandLine,
AndroidApplicationLauncher applicationLauncher,
boolean supportMultipleDevices,
boolean clearLogcatBeforeStart,
@NotNull AndroidRunConfigurationBase configuration,
boolean nonDebuggableOnDevice) {
myFacet = facet;
myApkProvider = apkProvider;
myCommandLine = commandLine;
myConfiguration = configuration;
myTargetChooser = targetChooser;
mySupportMultipleDevices = supportMultipleDevices;
myAvdName = targetChooser instanceof EmulatorTargetChooser
? ((EmulatorTargetChooser)targetChooser).getAvd()
: null;
myEnv = environment;
myApplicationLauncher = applicationLauncher;
myClearLogcatBeforeStart = clearLogcatBeforeStart;
myNonDebuggableOnDevice = nonDebuggableOnDevice;
}
public void setDeploy(boolean deploy) {
myDeploy = deploy;
}
public void setTargetPackageName(String targetPackageName) {
synchronized (myDebugLock) {
myTargetPackageName = targetPackageName;
}
}
@Nullable
private IDevice[] chooseDevicesAutomatically() {
final List<IDevice> compatibleDevices = getAllCompatibleDevices();
if (compatibleDevices.size() == 0) {
return EMPTY_DEVICE_ARRAY;
}
else if (compatibleDevices.size() == 1) {
return new IDevice[] {compatibleDevices.get(0)};
}
else {
final IDevice[][] devicesWrapper = {null};
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override
public void run() {
devicesWrapper[0] = chooseDevicesManually(new Condition<IDevice>() {
@Override
public boolean value(IDevice device) {
return isCompatibleDevice(device) != Boolean.FALSE;
}
});
}
}, ModalityState.defaultModalityState());
return devicesWrapper[0].length > 0 ? devicesWrapper[0] : null;
}
}
@NotNull
List<IDevice> getAllCompatibleDevices() {
final List<IDevice> compatibleDevices = new ArrayList<IDevice>();
final AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
if (bridge != null) {
IDevice[] devices = bridge.getDevices();
for (IDevice device : devices) {
if (isCompatibleDevice(device) != Boolean.FALSE) {
compatibleDevices.add(device);
}
}
}
return compatibleDevices;
}
private void chooseAvd() {
IAndroidTarget buildTarget = myFacet.getConfiguration().getAndroidTarget();
assert buildTarget != null;
AvdInfo[] avds = myFacet.getValidCompatibleAvds();
if (avds.length > 0) {
myAvdName = avds[0].getName();
}
else {
final Project project = myFacet.getModule().getProject();
AvdManager manager = null;
try {
manager = myFacet.getAvdManager(new AvdManagerLog() {
@Override
public void error(Throwable t, String errorFormat, Object... args) {
super.error(t, errorFormat, args);
if (errorFormat != null) {
final String msg = String.format(errorFormat, args);
message(msg, STDERR);
}
}
});
}
catch (AvdsNotSupportedException e) {
// can't be
LOG.error(e);
}
catch (final AndroidLocation.AndroidLocationException e) {
LOG.info(e);
runInDispatchedThread(new Runnable() {
@Override
public void run() {
Messages.showErrorDialog(project, e.getMessage(), CommonBundle.getErrorTitle());
}
}, false);
return;
}
final AvdManager finalManager = manager;
assert finalManager != null;
runInDispatchedThread(new Runnable() {
@Override
public void run() {
CreateAvdDialog dialog = new CreateAvdDialog(project, myFacet, finalManager, true, true);
dialog.show();
if (dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) {
AvdInfo createdAvd = dialog.getCreatedAvd();
if (createdAvd != null) {
myAvdName = createdAvd.getName();
}
}
}
}, true);
}
}
void start(boolean chooseTargetDevice) {
try {
setTargetPackageName(myApkProvider.getPackageName());
} catch (ApkProvisionException e) {
message(e.getMessage(), STDERR);
LOG.error(e);
getProcessHandler().destroyProcess();
return;
}
if (chooseTargetDevice) {
//TODO: Why this message sometimes does not show up when a not yet booted device is picked?
message("Waiting for device.", STDOUT);
if (myTargetDevices.length == 0 && !chooseOrLaunchDevice()) {
getProcessHandler().destroyProcess();
fireExecutionFailed();
return;
}
}
doStart();
}
private void doStart() {
if (myDebugMode) {
AndroidDebugBridge.addClientChangeListener(this);
}
final MyDeviceChangeListener[] deviceListener = {null};
getProcessHandler().addProcessListener(new ProcessAdapter() {
@Override
public void processWillTerminate(ProcessEvent event, boolean willBeDestroyed) {
if (myDebugMode) {
AndroidDebugBridge.removeClientChangeListener(AndroidRunningState.this);
}
if (deviceListener[0] != null) {
Disposer.dispose(deviceListener[0]);
AndroidDebugBridge.removeDeviceChangeListener(deviceListener[0]);
}
myStopped = true;
synchronized (myLock) {
myLock.notifyAll();
}
}
});
deviceListener[0] = prepareAndStartAppWhenDeviceIsOnline();
}
private boolean chooseOrLaunchDevice() {
IDevice[] targetDevices = chooseDevicesAutomatically();
if (targetDevices == null) {
message("Canceled", STDERR);
return false;
}
if (targetDevices.length > 0) {
myTargetDevices = targetDevices;
}
else if (myTargetChooser instanceof EmulatorTargetChooser) {
if (myAvdName == null) {
chooseAvd();
}
if (myAvdName != null) {
myFacet.launchEmulator(myAvdName, myCommandLine);
}
else if (getProcessHandler().isStartNotified()) {
message("Canceled", STDERR);
return false;
}
}
else {
message("USB device not found", STDERR);
return false;
}
return true;
}
@NotNull
private IDevice[] chooseDevicesManually(@Nullable Condition<IDevice> filter) {
final Project project = myFacet.getModule().getProject();
String value = PropertiesComponent.getInstance(project).getValue(ANDROID_TARGET_DEVICES_PROPERTY);
String[] selectedSerials = value != null ? fromString(value) : null;
AndroidPlatform platform = myFacet.getConfiguration().getAndroidPlatform();
if (platform == null) {
LOG.error("Android platform not set for module: " + myFacet.getModule().getName());
return DeviceChooser.EMPTY_DEVICE_ARRAY;
}
DeviceChooserDialog chooser = new DeviceChooserDialog(myFacet, platform.getTarget(), mySupportMultipleDevices, selectedSerials, filter);
chooser.show();
IDevice[] devices = chooser.getSelectedDevices();
if (chooser.getExitCode() != DialogWrapper.OK_EXIT_CODE || devices.length == 0) {
return DeviceChooser.EMPTY_DEVICE_ARRAY;
}
PropertiesComponent.getInstance(project).setValue(ANDROID_TARGET_DEVICES_PROPERTY, toString(devices));
return devices;
}
@NotNull
public static String toString(@NotNull IDevice[] devices) {
StringBuilder builder = new StringBuilder();
for (int i = 0, n = devices.length; i < n; i++) {
builder.append(devices[i].getSerialNumber());
if (i < n - 1) {
builder.append(' ');
}
}
return builder.toString();
}
@NotNull
private static String[] fromString(@NotNull String s) {
return s.split(" ");
}
public void message(@NotNull String message, @NotNull Key outputKey) {
getProcessHandler().notifyTextAvailable(message + '\n', outputKey);
}
@Override
public void clientChanged(Client client, int changeMask) {
synchronized (myDebugLock) {
if (myDebugLauncher == null) {
return;
}
if (myDeploy && !myApplicationDeployed) {
return;
}
IDevice device = client.getDevice();
if (isMyDevice(device) && device.isOnline()) {
if (myTargetDevices.length == 0) {
myTargetDevices = new IDevice[]{device};
}
ClientData data = client.getClientData();
if (myDebugLauncher != null && isToLaunchDebug(data)) {
launchDebug(client);
}
}
}
}
private boolean isToLaunchDebug(@NotNull ClientData data) {
if (data.getDebuggerConnectionStatus() == ClientData.DebuggerStatus.WAITING) {
// early exit without checking package name in case the debug package doesn't match
// our target package name. This happens for instance when debugging a test that doesn't launch an application
return true;
}
String description = data.getClientDescription();
if (description == null) {
return false;
}
return description.equals(myTargetPackageName) && myApplicationLauncher.isReadyForDebugging(data, getProcessHandler());
}
private void launchDebug(@NotNull Client client) {
myDebugLauncher.launchDebug(client);
myDebugLauncher = null;
}
@Nullable
Boolean isCompatibleDevice(@NotNull IDevice device) {
if (myTargetChooser instanceof EmulatorTargetChooser) {
if (device.isEmulator()) {
String avdName = device.getAvdName();
if (myAvdName != null) {
return myAvdName.equals(avdName);
}
AndroidPlatform androidPlatform = myFacet.getConfiguration().getAndroidPlatform();
if (androidPlatform == null) {
LOG.error("Target Android platform not set for module: " + myFacet.getModule().getName());
return false;
} else {
LaunchCompatibility compatibility = LaunchCompatibility.canRunOnDevice(AndroidModuleInfo.get(myFacet).getRuntimeMinSdkVersion(),
androidPlatform.getTarget(),
EnumSet.noneOf(IDevice.HardwareFeature.class),
device,
null);
return compatibility.isCompatible() != ThreeState.NO;
}
}
}
else if (myTargetChooser instanceof UsbDeviceTargetChooser) {
return !device.isEmulator();
}
else if (myTargetChooser instanceof ManualTargetChooser && myConfiguration.USE_LAST_SELECTED_DEVICE) {
DeviceStateAtLaunch lastLaunchState = myConfiguration.getDevicesUsedInLastLaunch();
return lastLaunchState != null && lastLaunchState.usedDevice(device);
}
return false;
}
private boolean isMyDevice(@NotNull IDevice device) {
if (myTargetDevices.length > 0) {
return ArrayUtilRt.find(myTargetDevices, device) >= 0;
}
Boolean compatible = isCompatibleDevice(device);
return compatible == null || compatible.booleanValue();
}
public void setTargetDevices(@NotNull IDevice[] targetDevices) {
myTargetDevices = targetDevices;
}
public void setConsole(@NotNull ConsoleView console) {
myConsole = console;
}
@Nullable
private MyDeviceChangeListener prepareAndStartAppWhenDeviceIsOnline() {
if (myTargetDevices.length > 0) {
boolean allDevicesOnline = true;
for (IDevice targetDevice : myTargetDevices) {
if (targetDevice.isOnline()) {
if (!prepareAndStartApp(targetDevice) && !myStopped) {
// todo: check: it may be we don't need to assign it directly
// TODO: Why stop completely for a problem potentially affecting only a single device?
myStopped = true;
getProcessHandler().destroyProcess();
break;
}
}
else {
allDevicesOnline = false;
}
}
// If all target devices are online, we are done.
if (allDevicesOnline) {
if (!myDebugMode && !myStopped) {
getProcessHandler().destroyProcess();
}
return null;
}
}
final MyDeviceChangeListener deviceListener = new MyDeviceChangeListener();
AndroidDebugBridge.addDeviceChangeListener(deviceListener);
return deviceListener;
}
public synchronized void setProcessHandler(ProcessHandler processHandler) {
myProcessHandler = processHandler;
}
public synchronized ProcessHandler getProcessHandler() {
return myProcessHandler;
}
private boolean prepareAndStartApp(IDevice device) {
if (myDebugMode && myNonDebuggableOnDevice && !device.isEmulator()) {
message(AndroidBundle.message("android.cannot.debug.noDebugPermissions", getPackageName(), device.getName()), STDERR);
fireExecutionFailed();
return false;
}
if (!doPrepareAndStart(device)) {
fireExecutionFailed();
return false;
}
return true;
}
private void fireExecutionFailed() {
for (AndroidRunningStateListener listener : myListeners) {
listener.executionFailed();
}
}
public void setOpenLogcatAutomatically(boolean openLogcatAutomatically) {
myOpenLogcatAutomatically = openLogcatAutomatically;
}
public void setFilterLogcatAutomatically(boolean filterLogcatAutomatically) {
myFilterLogcatAutomatically = filterLogcatAutomatically;
}
private boolean doPrepareAndStart(@NotNull final IDevice device) {
if (myClearLogcatBeforeStart) {
clearLogcatAndConsole(getModule().getProject(), device);
}
message("Target device: " + device.getName(), STDOUT);
try {
if (myDeploy) {
Collection<ApkInfo> apks;
try {
apks = myApkProvider.getApks(device);
} catch (ApkProvisionException e) {
message(e.getMessage(), STDERR);
LOG.error(e);
return false;
}
for (ApkInfo apk : apks) {
if (!uploadAndInstallApk(device, apk.getApplicationId(), apk.getFile())) {
return false;
}
}
trackInstallation(device);
myApplicationDeployed = true;
}
final AndroidApplicationLauncher.LaunchResult launchResult =
myApplicationLauncher.launch(this, device);
if (launchResult == AndroidApplicationLauncher.LaunchResult.STOP) {
return false;
}
else if (launchResult == AndroidApplicationLauncher.LaunchResult.SUCCESS) {
checkDdms();
}
final Client client;
synchronized (myDebugLock) {
client = device.getClient(myTargetPackageName);
if (myDebugLauncher != null) {
if (client != null &&
myApplicationLauncher.isReadyForDebugging(client.getClientData(), getProcessHandler())) {
launchDebug(client);
}
else {
message("Waiting for process: " + myTargetPackageName, STDOUT);
}
}
}
if (!myDebugMode && myOpenLogcatAutomatically) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
final ToolWindow androidToolWindow = ToolWindowManager.getInstance(myEnv.getProject()).
getToolWindow(AndroidToolWindowFactory.TOOL_WINDOW_ID);
// Activate the tool window, and once activated, make sure the right device is selected
androidToolWindow.activate(new Runnable() {
@Override
public void run() {
int count = androidToolWindow.getContentManager().getContentCount();
for (int i = 0; i < count; i++) {
Content content = androidToolWindow.getContentManager().getContent(i);
DevicePanel devicePanel = content == null ? null : content.getUserData(AndroidToolWindowFactory.DEVICES_PANEL_KEY);
if (devicePanel != null) {
devicePanel.selectDevice(device);
devicePanel.selectClient(client);
break;
}
}
}
}, false);
}
});
}
return true;
}
catch (TimeoutException e) {
LOG.info(e);
message("Error: Connection to ADB failed with a timeout", STDERR);
return false;
}
catch (AdbCommandRejectedException e) {
LOG.info(e);
message("Error: Adb refused a command", STDERR);
return false;
}
catch (IOException e) {
LOG.info(e);
String message = e.getMessage();
message("I/O Error" + (message != null ? ": " + message : ""), STDERR);
return false;
}
}
private static int ourInstallationCount = 0;
private static void trackInstallation(@NotNull IDevice device) {
if (!UsageTracker.getInstance().canTrack()) {
return;
}
// only track every 10th installation (just to reduce the load on the server)
ourInstallationCount = (ourInstallationCount + 1) % 10;
if (ourInstallationCount != 0) {
return;
}
UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_DEPLOYMENT, UsageTracker.ACTION_DEPLOYMENT_APK, null, null);
UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_DEVICE_INFO, UsageTracker.DEVICE_INFO_SERIAL_HASH,
Hashing.md5().hashString(device.getSerialNumber(), Charsets.UTF_8).toString(), null);
UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_DEVICE_INFO, UsageTracker.DEVICE_INFO_BUILD_TAGS,
device.getProperty(IDevice.PROP_BUILD_TAGS), null);
UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_DEVICE_INFO, UsageTracker.DEVICE_INFO_BUILD_TYPE,
device.getProperty(IDevice.PROP_BUILD_TYPE), null);
UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_DEVICE_INFO, UsageTracker.DEVICE_INFO_BUILD_VERSION_RELEASE,
device.getProperty(IDevice.PROP_BUILD_VERSION), null);
UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_DEVICE_INFO, UsageTracker.DEVICE_INFO_BUILD_API_LEVEL,
device.getProperty(IDevice.PROP_BUILD_API_LEVEL), null);
UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_DEVICE_INFO, UsageTracker.DEVICE_INFO_MANUFACTURER,
device.getProperty(IDevice.PROP_DEVICE_MANUFACTURER), null);
UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_DEVICE_INFO, UsageTracker.DEVICE_INFO_MODEL,
device.getProperty(IDevice.PROP_DEVICE_MODEL), null);
UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_DEVICE_INFO, UsageTracker.DEVICE_INFO_CPU_ABI,
device.getProperty(IDevice.PROP_DEVICE_CPU_ABI), null);
}
protected static void clearLogcatAndConsole(@NotNull final Project project, @NotNull final IDevice device) {
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override
public void run() {
final ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow(AndroidToolWindowFactory.TOOL_WINDOW_ID);
if (toolWindow == null) {
return;
}
for (Content content : toolWindow.getContentManager().getContents()) {
final AndroidLogcatView view = content.getUserData(AndroidLogcatView.ANDROID_LOGCAT_VIEW_KEY);
if (view != null) {
view.clearLogcat(device);
}
}
}
}, ModalityState.defaultModalityState());
}
private boolean checkDdms() {
AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
if (myDebugMode && bridge != null && AdbService.canDdmsBeCorrupted(bridge)) {
message(AndroidBundle.message("ddms.corrupted.error"), STDERR);
JComponent component = myConsole == null ? null : myConsole.getComponent();
if (component != null) {
final ExecutionEnvironment environment = LangDataKeys.EXECUTION_ENVIRONMENT.getData(DataManager.getInstance().getDataContext(component));
if (environment == null) {
return false;
}
myConsole.printHyperlink(AndroidBundle.message("restart.adb.fix.text"), new HyperlinkInfo() {
@Override
public void navigate(Project project) {
AdbService.getInstance().restartDdmlib(project);
final ProcessHandler processHandler = getProcessHandler();
if (!processHandler.isProcessTerminated()) {
processHandler.destroyProcess();
}
ExecutionUtil.restart(environment);
}
});
myConsole.print("\n", ConsoleViewContentType.NORMAL_OUTPUT);
}
return false;
}
return true;
}
/**
* Installs the given apk on the device.
* @return whether the installation was successful
*/
private boolean uploadAndInstallApk(@NotNull IDevice device, @NotNull String packageName, @NotNull File localFile)
throws IOException, AdbCommandRejectedException, TimeoutException {
if (myStopped) return false;
String remotePath = "/data/local/tmp/" + packageName;
String exceptionMessage;
String errorMessage;
message("Uploading file\n\tlocal path: " + localFile + "\n\tremote path: " + remotePath, STDOUT);
try {
InstalledApks installedApks = ServiceManager.getService(InstalledApks.class);
if (installedApks.isInstalled(device, localFile, packageName)) {
message("No apk changes detected. Skipping file upload, force stopping package instead.", STDOUT);
forceStopPackageSilently(device, packageName, true);
return true;
} else {
device.pushFile(localFile.getPath(), remotePath);
boolean installed = installApp(device, remotePath, packageName);
if (installed) {
installedApks.setInstalled(device, localFile, packageName);
}
return installed;
}
}
catch (TimeoutException e) {
LOG.info(e);
exceptionMessage = e.getMessage();
errorMessage = "Connection timeout";
}
catch (AdbCommandRejectedException e) {
LOG.info(e);
exceptionMessage = e.getMessage();
errorMessage = "ADB refused the command";
}
catch (final SyncException e) {
LOG.info(e);
final SyncException.SyncError errorCode = e.getErrorCode();
if (SyncException.SyncError.NO_LOCAL_FILE.equals(errorCode)) {
// Sometimes, users see the issue that for Gradle projects, the apk location used is incorrect (points to build/classes/?.apk
// instead of build/apk/?.apk).
// This happens reasonably often, but isn't reproducible, so we add this workaround here to show a popup to 'Sync Project with
// Gradle Files' if it is a gradle project.
// See https://code.google.com/p/android/issues/detail?id=59018 for more info.
// The problem is that at this point, the project maybe a Gradle-based project, but its IdeaAndroidProject may be null.
// We can check if there is a top-level build.gradle or settings.gradle file.
DataManager.getInstance().getDataContextFromFocus().doWhenDone(new Consumer<DataContext>() {
@Override
public void consume(DataContext dataContext) {
if (dataContext != null) {
Project project = CommonDataKeys.PROJECT.getData(dataContext);
if (project != null && hasGradleFiles(project)) {
AndroidGradleNotification notification = AndroidGradleNotification.getInstance(project);
String message =
errorCode.getMessage() + '\n' + e.getMessage() + '\n' + "The project may need to be synced with Gradle files.";
notification.showBalloon("Unexpected Error", message, NotificationType.ERROR, new SyncProjectHyperlink());
}
}
}
private boolean hasGradleFiles(@NotNull Project project) {
File rootDirPath = new File(FileUtil.toSystemDependentName(project.getBasePath()));
return GradleUtil.getGradleBuildFilePath(rootDirPath).isFile() || GradleUtil.getGradleSettingsFilePath(rootDirPath).isFile();
}
});
}
errorMessage = errorCode.getMessage();
exceptionMessage = e.getMessage();
}
if (errorMessage.equals(exceptionMessage) || exceptionMessage == null) {
message(errorMessage, STDERR);
}
else {
message(errorMessage + '\n' + exceptionMessage, STDERR);
}
return false;
}
/** Attempts to force stop package running on given device. */
private void forceStopPackageSilently(@NotNull IDevice device, @NotNull String packageName, boolean ignoreErrors) {
try {
executeDeviceCommandAndWriteToConsole(device, "am force-stop " + packageName, new MyReceiver());
}
catch (Exception e) {
if (!ignoreErrors) {
throw new RuntimeException(e);
}
}
}
@SuppressWarnings({"DuplicateThrows"})
public void executeDeviceCommandAndWriteToConsole(@NotNull IDevice device,
@NotNull String command,
@NotNull AndroidOutputReceiver receiver)
throws IOException, TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException {
message("DEVICE SHELL COMMAND: " + command, STDOUT);
AndroidUtils.executeCommandOnDevice(device, command, receiver, false);
}
private boolean installApp(@NotNull IDevice device, @NotNull String remotePath, @NotNull String packageName)
throws IOException, AdbCommandRejectedException, TimeoutException {
message("Installing " + packageName, STDOUT);
InstallResult result = null;
boolean retry = true;
while (!myStopped && retry) {
result = installApp(device, remotePath);
if (result.installOutput != null) {
message(result.installOutput, result.failureCode == InstallFailureCode.NO_ERROR ? STDOUT : STDERR);
}
switch (result.failureCode) {
case DEVICE_NOT_RESPONDING:
message("Device is not ready. Waiting for " + WAITING_TIME_SECS + " sec.", STDOUT);
synchronized (myLock) {
try {
myLock.wait(WAITING_TIME_SECS * 1000);
}
catch (InterruptedException e) {
LOG.info(e);
}
}
retry = true;
break;
case INSTALL_FAILED_VERSION_DOWNGRADE:
String reason = AndroidBundle.message("deployment.failed.uninstall.prompt.text",
AndroidBundle.message("deployment.failed.reason.version.downgrade"));
retry = promptUninstallExistingApp(reason) && uninstallPackage(device, packageName);
break;
case INCONSISTENT_CERTIFICATES:
reason = AndroidBundle.message("deployment.failed.uninstall.prompt.text",
AndroidBundle.message("deployment.failed.reason.different.signature"));
retry = promptUninstallExistingApp(reason) && uninstallPackage(device, packageName);
break;
case INSTALL_FAILED_DEXOPT:
reason = AndroidBundle.message("deployment.failed.uninstall.prompt.text",
AndroidBundle.message("deployment.failed.reason.dexopt"));
retry = promptUninstallExistingApp(reason) && uninstallPackage(device, packageName);
break;
case NO_CERTIFICATE:
message(AndroidBundle.message("deployment.failed.no.certificates.explanation"), STDERR);
showMessageDialog(AndroidBundle.message("deployment.failed.no.certificates.explanation"));
retry = false;
break;
case UNTYPED_ERROR:
reason = AndroidBundle.message("deployment.failed.uninstall.prompt.generic.text", result.failureMessage);
retry = promptUninstallExistingApp(reason) && uninstallPackage(device, packageName);
break;
default:
retry = false;
break;
}
}
return result != null && result.failureCode == InstallFailureCode.NO_ERROR;
}
private boolean uninstallPackage(@NotNull IDevice device, @NotNull String packageName) {
message("DEVICE SHELL COMMAND: pm uninstall " + packageName, STDOUT);
String output;
try {
output = device.uninstallPackage(packageName);
}
catch (InstallException e) {
return false;
}
if (output != null) {
message(output, STDERR);
return false;
}
return true;
}
private boolean promptUninstallExistingApp(final String reason) {
final AtomicBoolean uninstall = new AtomicBoolean(false);
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override
public void run() {
int result = Messages.showOkCancelDialog(myFacet.getModule().getProject(),
reason,
AndroidBundle.message("deployment.failed.title"),
Messages.getQuestionIcon());
uninstall.set(result == Messages.OK);
}
}, ModalityState.defaultModalityState());
return uninstall.get();
}
private void showMessageDialog(@NotNull final String message) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
Messages.showErrorDialog(myFacet.getModule().getProject(), message, AndroidBundle.message("deployment.failed.title"));
}
});
}
private enum InstallFailureCode {
NO_ERROR,
DEVICE_NOT_RESPONDING,
INCONSISTENT_CERTIFICATES,
INSTALL_FAILED_VERSION_DOWNGRADE,
INSTALL_FAILED_DEXOPT,
NO_CERTIFICATE,
UNTYPED_ERROR
}
private static class InstallResult {
public final InstallFailureCode failureCode;
@Nullable public final String failureMessage;
@Nullable public final String installOutput;
public InstallResult(InstallFailureCode failureCode, @Nullable String failureMessage, @Nullable String installOutput) {
this.failureCode = failureCode;
this.failureMessage = failureMessage;
this.installOutput = installOutput;
}
}
private InstallResult installApp(@NotNull IDevice device, @NotNull String remotePath)
throws AdbCommandRejectedException, TimeoutException, IOException {
MyReceiver receiver = new MyReceiver();
try {
executeDeviceCommandAndWriteToConsole(device, "pm install -r \"" + remotePath + "\"", receiver);
}
catch (ShellCommandUnresponsiveException e) {
LOG.info(e);
return new InstallResult(InstallFailureCode.DEVICE_NOT_RESPONDING, null, null);
}
return new InstallResult(getFailureCode(receiver),
receiver.failureMessage,
receiver.output.toString());
}
private InstallFailureCode getFailureCode(MyReceiver receiver) {
if (receiver.errorType == NO_ERROR && receiver.failureMessage == null) {
return InstallFailureCode.NO_ERROR;
}
if ("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES".equals(receiver.failureMessage)) {
return InstallFailureCode.INCONSISTENT_CERTIFICATES;
} else if ("INSTALL_PARSE_FAILED_NO_CERTIFICATES".equals(receiver.failureMessage)) {
return InstallFailureCode.NO_CERTIFICATE;
} else if ("INSTALL_FAILED_VERSION_DOWNGRADE".equals(receiver.failureMessage)) {
return InstallFailureCode.INSTALL_FAILED_VERSION_DOWNGRADE;
} else if ("INSTALL_FAILED_DEXOPT".equals(receiver.failureMessage)) {
return InstallFailureCode.INSTALL_FAILED_DEXOPT;
}
return InstallFailureCode.UNTYPED_ERROR;
}
public void addListener(@NotNull AndroidRunningStateListener listener) {
myListeners.add(listener);
}
private class MyDeviceChangeListener implements AndroidDebugBridge.IDeviceChangeListener, Disposable {
private final MergingUpdateQueue myQueue =
new MergingUpdateQueue("ANDROID_DEVICE_STATE_UPDATE_QUEUE", 1000, true, null, this, null, false);
@GuardedBy("this")
private boolean installed;
@Override
public void deviceConnected(final IDevice device) {
// avd may be null if usb device is used, or if it didn't set by ddmlib yet
if (device.getAvdName() == null || isMyDevice(device)) {
message("Device connected: " + device.getSerialNumber(), STDOUT);
// we need this, because deviceChanged is not triggered if avd is set to the emulator
myQueue.queue(new MyDeviceStateUpdate(device));
}
}
@Override
public void deviceDisconnected(IDevice device) {
if (isMyDevice(device)) {
message("Device disconnected: " + device.getSerialNumber(), STDOUT);
}
}
@Override
public void deviceChanged(final IDevice device, int changeMask) {
myQueue.queue(new Update(device.getSerialNumber()) {
@Override
public void run() {
onDeviceChanged(device);
}
});
}
private synchronized void onDeviceChanged(IDevice device) {
if (installed || !isMyDevice(device) || !device.isOnline()) {
return;
}
if (myTargetDevices.length == 0) {
myTargetDevices = new IDevice[]{device};
}
// Devices (esp. emulators) may be reported as online, but may not have services running yet. Attempting to
// install at this time would result in an error like "Could not access the Package Manager".
// We use the following heuristic to check that the system is in a reasonable state to install apps.
if (device.getClients().length < 5 &&
device.getClient("android.process.acore") == null &&
device.getClient("com.google.android.wearable.app") == null) {
message(String.format("Device %1$s is online, waiting for processes to start up..", device.getName()), STDOUT);
return;
}
message("Device is ready: " + device.getName(), STDOUT);
installed = true;
if ((!prepareAndStartApp(device) || !myDebugMode) && !myStopped) {
getProcessHandler().destroyProcess();
}
}
@Override
public void dispose() {
}
private class MyDeviceStateUpdate extends Update {
private final IDevice myDevice;
public MyDeviceStateUpdate(IDevice device) {
super(device.getSerialNumber());
myDevice = device;
}
@Override
public void run() {
onDeviceChanged(myDevice);
myQueue.queue(new MyDeviceStateUpdate(myDevice));
}
}
}
}