/* * 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.SdkConstants; import com.android.annotations.concurrency.GuardedBy; import com.android.build.SplitOutput; import com.android.builder.model.AndroidArtifact; import com.android.builder.model.AndroidArtifactOutput; import com.android.builder.model.Variant; import com.android.ddmlib.*; import com.android.ide.common.build.SplitOutputMatcher; 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.IdeaAndroidProject; 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.run.ApkUploaderService; import com.android.tools.idea.run.LaunchCompatibility; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Sets; 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.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.roots.ModuleOrderEntry; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.OrderEntry; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.*; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.packaging.artifacts.Artifact; import com.intellij.packaging.artifacts.ArtifactManager; 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.containers.HashMap; import com.intellij.util.ui.update.MergingUpdateQueue; import com.intellij.util.ui.update.Update; import com.intellij.util.xml.GenericAttributeValue; import com.intellij.xdebugger.DefaultDebugProcessHandler; import org.gradle.internal.reflect.*; import org.gradle.tooling.model.UnsupportedMethodException; import org.jetbrains.android.compiler.artifact.AndroidArtifactUtil; import org.jetbrains.android.dom.manifest.Manifest; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.facet.AndroidRootUtil; import org.jetbrains.android.facet.AvdsNotSupportedException; import org.jetbrains.android.logcat.AndroidLogcatUtil; import org.jetbrains.android.logcat.AndroidLogcatView; import org.jetbrains.android.logcat.AndroidToolWindowFactory; 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.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 = 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"; static final int NO_ERROR = -2; private static final int UNTYPED_ERROR = -1; /** Default suffix for test packages (as added by Android Gradle plugin) */ private static final String DEFAULT_TEST_PACKAGE_SUFFIX = ".test"; private String myPackageName; // In non gradle projects, test packages belong to a separate module, so their name is equal to // the package name of the module. i.e. myPackageName = myTestPackageName. // In gradle projects, tests are part of the same module, and their package name is either specified // in build.gradle or generated automatically by Android Gradle plugin private String myTestPackageName; private String myTargetPackageName; private final AndroidFacet myFacet; private final String myCommandLine; private final AndroidApplicationLauncher myApplicationLauncher; private Map<AndroidFacet, String> myAdditionalFacet2PackageName; 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 String myArtifactName; 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 { myProcessHandler = new DefaultDebugProcessHandler(); AndroidProcessText.attach(myProcessHandler); ConsoleView console; if (isDebugMode()) { Project project = myFacet.getModule().getProject(); final TextConsoleBuilder builder = TextConsoleBuilderFactory.getInstance().createBuilder(project); console = builder.getConsole(); if (console != null) { console.attachToProcess(myProcessHandler); } } else { console = myConfiguration.attachConsole(this, executor); } myConsole = console; if (myTargetChooser instanceof ManualTargetChooser) { 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; } final ExtendedDeviceChooserDialog chooser = new ExtendedDeviceChooserDialog(myFacet, platform.getTarget(), mySupportMultipleDevices, true, myConfiguration.USE_LAST_SELECTED_DEVICE); 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 { 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()); } } } } ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { @Override public void run() { start(true); } }); //noinspection ConstantConditions 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()); } @Nullable private String computePackageName(@NotNull final AndroidFacet facet) { if (facet.getProperties().USE_CUSTOM_MANIFEST_PACKAGE) { return facet.getProperties().CUSTOM_MANIFEST_PACKAGE; } else if (facet.getProperties().USE_CUSTOM_COMPILER_MANIFEST) { File manifestCopy = null; final Manifest manifest; final String manifestLocalPath; try { final Pair<File, String> pair = AndroidRunConfigurationBase.getCopyOfCompilerManifestFile(facet, getProcessHandler()); manifestCopy = pair != null ? pair.getFirst() : null; VirtualFile manifestVFile = manifestCopy != null ? LocalFileSystem.getInstance().findFileByIoFile(manifestCopy) : null; if (manifestVFile != null) { manifestVFile.refresh(false, false); manifest = AndroidUtils.loadDomElement(facet.getModule(), manifestVFile, Manifest.class); } else { manifest = null; } manifestLocalPath = pair != null ? pair.getSecond() : null; final Module module = facet.getModule(); final String moduleName = module.getName(); if (manifest == null) { message("Cannot find " + SdkConstants.FN_ANDROID_MANIFEST_XML + " file for module " + moduleName, STDERR); return null; } return ApplicationManager.getApplication().runReadAction(new Computable<String>() { @Override public String compute() { final GenericAttributeValue<String> packageAttrValue = manifest.getPackage(); final String aPackage = packageAttrValue.getValue(); if (aPackage == null || aPackage.isEmpty()) { message("[" + moduleName + "] Main package is not specified in file " + manifestLocalPath, STDERR); //noinspection ConstantConditions return null; } return aPackage; } }); } finally { if (manifestCopy != null) { FileUtil.delete(manifestCopy.getParentFile()); } } } else { String pkg = AndroidModuleInfo.get(facet).getPackage(); if (pkg == null || pkg.isEmpty()) { message("[" + facet.getModule().getName() + "] Unable to obtain main package from manifest.", STDERR); } return pkg; } } private boolean fillRuntimeAndTestDependencies(@NotNull Module module, @NotNull Map<AndroidFacet, String> module2PackageName) { for (OrderEntry entry : ModuleRootManager.getInstance(module).getOrderEntries()) { if (entry instanceof ModuleOrderEntry) { ModuleOrderEntry moduleOrderEntry = (ModuleOrderEntry)entry; Module depModule = moduleOrderEntry.getModule(); if (depModule != null) { AndroidFacet depFacet = AndroidFacet.getInstance(depModule); if (depFacet != null && !module2PackageName.containsKey(depFacet) && !depFacet.isLibraryProject()) { String packageName = computePackageName(depFacet); if (packageName == null) { return false; } module2PackageName.put(depFacet, packageName); if (!fillRuntimeAndTestDependencies(depModule, module2PackageName)) { return false; } } } } } return true; } @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() { return myPackageName; } public String getTestPackageName() { return myTestPackageName; } 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, @Nullable TargetChooser targetChooser, @NotNull String commandLine, AndroidApplicationLauncher applicationLauncher, boolean supportMultipleDevices, boolean clearLogcatBeforeStart, @NotNull AndroidRunConfigurationBase configuration, boolean nonDebuggableOnDevice) { myFacet = facet; 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 setArtifactName(@Nullable String artifactName) { myArtifactName = artifactName; } 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) { LocalFileSystem.getInstance().refresh(false); myPackageName = computePackageName(myFacet); if (myPackageName == null) { getProcessHandler().destroyProcess(); return; } myTestPackageName = computeTestPackageName(myFacet, myPackageName); setTargetPackageName(myPackageName); final HashMap<AndroidFacet, String> depFacet2PackageName = new HashMap<AndroidFacet, String>(); if (!fillRuntimeAndTestDependencies(getModule(), depFacet2PackageName)) { getProcessHandler().destroyProcess(); return; } myAdditionalFacet2PackageName = depFacet2PackageName; if (chooseTargetDevice) { 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, getProcessHandler()); } 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(" "); } private 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) { return true; } String description = data.getClientDescription(); if (description == null) { return false; } return description.equals(myTargetPackageName) && myApplicationLauncher.isReadyForDebugging(data, getProcessHandler()); } private void launchDebug(Client client) { String port = Integer.toString(client.getDebuggerListenPort()); myDebugLauncher.launchDebug(client.getDevice(), port); 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) { for (IDevice targetDevice : myTargetDevices) { if (targetDevice.isOnline()) { if (!prepareAndStartApp(targetDevice) && !myStopped) { // todo: check: it may be we don't need to assign it directly myStopped = true; getProcessHandler().destroyProcess(); break; } } } 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", myPackageName, 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) { if (!checkPackageNames()) return false; IdeaAndroidProject ideaAndroidProject = myFacet.getIdeaAndroidProject(); if (ideaAndroidProject == null) { if (!uploadAndInstall(device, myPackageName, myFacet)) return false; if (!uploadAndInstallDependentModules(device)) return false; } else { Variant selectedVariant = ideaAndroidProject.getSelectedVariant(); // install apk (note that variant.getOutputFile() will point to a .aar in the case of a library) if (!ideaAndroidProject.getDelegate().isLibrary()) { File apk = getApk(selectedVariant, device); if (apk == null) { String message = AndroidBundle.message("deployment.failed.cannot.determine.apk", selectedVariant.getDisplayName(), device.getName()); message(message, STDERR); return false; } if (!uploadAndInstallApk(device, myPackageName, apk.getAbsolutePath())) { return false; } } // install test apk if (getConfiguration() instanceof AndroidTestRunConfiguration) { AndroidArtifact testArtifactInfo = ideaAndroidProject.findInstrumentationTestArtifactInSelectedVariant(); if (testArtifactInfo != null) { AndroidArtifactOutput output = GradleUtil.getOutput(testArtifactInfo); File testApk = output.getOutputFile(); if (!uploadAndInstallApk(device, myTestPackageName, testApk.getAbsolutePath())) { return false; } } } } myApplicationDeployed = true; } final AndroidApplicationLauncher.LaunchResult launchResult = myApplicationLauncher.launch(this, device); if (launchResult == AndroidApplicationLauncher.LaunchResult.STOP) { return false; } else if (launchResult == AndroidApplicationLauncher.LaunchResult.SUCCESS) { checkDdms(); } synchronized (myDebugLock) { Client 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); AndroidLogcatView logcatView = content == null ? null : content.getUserData(AndroidLogcatView.ANDROID_LOGCAT_VIEW_KEY); if (devicePanel != null) { devicePanel.selectDevice(device); if (logcatView != null && myFilterLogcatAutomatically) { logcatView.createAndSelectFilterByPackage(myPackageName); } 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; } } @Nullable private static File getApk(@NotNull Variant variant, @NotNull IDevice device) { List<AndroidArtifactOutput> outputs = Lists.newArrayList(variant.getMainArtifact().getOutputs()); if (outputs.isEmpty()) { LOG.info("No outputs for the main artifact of variant: " + variant.getDisplayName()); return null; } // version 0.13 of the Gradle builder model introduces new APIs. We first need to check which version // is in use by this project. if (!hasSplitsModel(outputs.get(0))) { LOG.info("Using older Gradle model w/o information about split apks"); return outputs.get(0).getOutputFile(); } else { List<String> abis = device.getAbis(); int density = device.getDensity(); // TODO: The 2nd argument should be the ABI's supported by this variant. The model doesn't expose that right now. // Explanation: If you aren't using the split apk mechanism, then you'd specify an abi filter at the variant level. That information // is needed, so this argument was added to computeBestOutput, but the IDE model doesn't expose it. SplitOutput output = SplitOutputMatcher.computeBestOutput(outputs, Collections.<String>emptySet(), density, abis); if (output == null) { String message = AndroidBundle.message("deployment.failed.splitapk.nomatch", outputs.size(), density, Joiner.on(',').join(abis)); LOG.error(message); return null; } return output.getOutputFile(); } } // TODO: Remove this once we move to Gradle Model 1.0 or don't support 0.12.x, whichever is earlier (b.android.com/76248) private static boolean hasSplitsModel(AndroidArtifactOutput androidArtifactOutput) { try { androidArtifactOutput.getAbiFilter(); return true; } catch (UnsupportedMethodException e) { return false; } } private boolean checkPackageNames() { final Map<String, List<String>> packageName2ModuleNames = new HashMap<String, List<String>>(); packageName2ModuleNames.put(myPackageName, new ArrayList<String>(Arrays.asList(myFacet.getModule().getName()))); for (Map.Entry<AndroidFacet, String> entry : myAdditionalFacet2PackageName.entrySet()) { final String moduleName = entry.getKey().getModule().getName(); final String packageName = entry.getValue(); List<String> list = packageName2ModuleNames.get(packageName); if (list == null) { list = new ArrayList<String>(); packageName2ModuleNames.put(packageName, list); } list.add(moduleName); } boolean result = true; for (Map.Entry<String, List<String>> entry : packageName2ModuleNames.entrySet()) { final String packageName = entry.getKey(); final List<String> moduleNames = entry.getValue(); if (moduleNames.size() > 1) { final StringBuilder messageBuilder = new StringBuilder("Applications have the same package name "); messageBuilder.append(packageName).append(":\n "); for (Iterator<String> it = moduleNames.iterator(); it.hasNext(); ) { String moduleName = it.next(); messageBuilder.append(moduleName); if (it.hasNext()) { messageBuilder.append(", "); } } message(messageBuilder.toString(), STDERR); result = false; } } return result; } protected static void clearLogcatAndConsole(@NotNull final Project project, @NotNull final IDevice device) { final boolean[] result = {true}; ApplicationManager.getApplication().invokeAndWait(new Runnable() { @Override public void run() { final ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow(AndroidToolWindowFactory.TOOL_WINDOW_ID); if (toolWindow == null) { result[0] = false; return; } for (Content content : toolWindow.getContentManager().getContents()) { final AndroidLogcatView view = content.getUserData(AndroidLogcatView.ANDROID_LOGCAT_VIEW_KEY); if (view != null && device == view.getSelectedDevice()) { view.getLogConsole().clear(); } } } }, ModalityState.defaultModalityState()); if (result[0]) { AndroidLogcatUtil.clearLogcat(project, device); } } 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.restartDdmlib(project); final ProcessHandler processHandler = getProcessHandler(); if (!processHandler.isProcessTerminated()) { processHandler.destroyProcess(); } ExecutionUtil.restart(environment); } }); myConsole.print("\n", ConsoleViewContentType.NORMAL_OUTPUT); } return false; } return true; } private boolean uploadAndInstallDependentModules(@NotNull IDevice device) throws IOException, AdbCommandRejectedException, TimeoutException { for (AndroidFacet depFacet : myAdditionalFacet2PackageName.keySet()) { String packageName = AndroidModuleInfo.get(depFacet).getPackage(); if (packageName == null) { packageName = myAdditionalFacet2PackageName.get(depFacet); } assert packageName != null; if (!uploadAndInstall(device, packageName, depFacet)) { return false; } } return true; } private static String computeTestPackageName(@NotNull AndroidFacet facet, @NotNull String packageName) { IdeaAndroidProject ideaAndroidProject = facet.getIdeaAndroidProject(); if (ideaAndroidProject == null) { return packageName; } // In the case of Gradle projects, either the merged flavor provides a test package name, // or we just append ".test" to the source package name Variant selectedVariant = ideaAndroidProject.getSelectedVariant(); String testPackageName = selectedVariant.getMergedFlavor().getTestApplicationId(); return (testPackageName != null) ? testPackageName : packageName + DEFAULT_TEST_PACKAGE_SUFFIX; } private boolean uploadAndInstall(@NotNull IDevice device, @NotNull String packageName, AndroidFacet facet) throws IOException, AdbCommandRejectedException, TimeoutException { final Module module = facet.getModule(); String localPath; if (myArtifactName != null && myArtifactName.length() > 0) { final Artifact artifact = ArtifactManager.getInstance(myEnv.getProject()).findArtifact(myArtifactName); if (artifact == null) { message("ERROR: cannot find artifact \"" + myArtifactName + '"', STDERR); return false; } if (!AndroidArtifactUtil.isRelatedArtifact(artifact, module)) { message("ERROR: artifact \"" + myArtifactName + "\" doesn't contain packaged module \"" + module.getName() + '"', STDERR); return false; } final String artifactOutPath = artifact.getOutputFilePath(); if (artifactOutPath == null || artifactOutPath.length() == 0) { message("ERROR: output path is not specified for artifact \"" + myArtifactName + '"', STDERR); return false; } localPath = FileUtil.toSystemDependentName(artifactOutPath); } else { localPath = AndroidRootUtil.getApkPath(facet); } if (localPath == null) { message("ERROR: APK path is not specified for module \"" + module.getName() + '"', STDERR); return false; } return uploadAndInstallApk(device, packageName, localPath); } /** * Installs the given apk on the device. * @return whether the installation was successful */ private boolean uploadAndInstallApk(@NotNull IDevice device, @NotNull String packageName, @NotNull String localPath) throws IOException, AdbCommandRejectedException, TimeoutException { if (myStopped) return false; String remotePath = "/data/local/tmp/" + packageName; String exceptionMessage; String errorMessage; message("Uploading file\n\tlocal path: " + localPath + "\n\tremote path: " + remotePath, STDOUT); try { ApkUploaderService installer = ServiceManager.getService(ApkUploaderService.class); if (installer.uploadApk(device, localPath, remotePath)) { return installApp(device, remotePath, packageName); } else { message("No apk changes detected. Skipping file upload.", STDOUT); } return true; } 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; } @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 + " sec.", STDOUT); synchronized (myLock) { try { myLock.wait(WAITING_TIME * 1000); } catch (InterruptedException e) { LOG.info(e); } } retry = true; break; case INSTALL_FAILED_VERSION_DOWNGRADE: retry = promptUninstallExistingApp(AndroidBundle.message("deployment.failed.reason.version.downgrade")) && uninstallPackage(device, packageName); break; case INCONSISTENT_CERTIFICATES: retry = promptUninstallExistingApp(AndroidBundle.message("deployment.failed.reason.different.signature")) && uninstallPackage(device, packageName); break; case INSTALL_FAILED_DEXOPT: retry = promptUninstallExistingApp(AndroidBundle.message("deployment.failed.reason.dexopt")) && 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; 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(), AndroidBundle.message("deployment.failed.uninstall.prompt.text", 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 // check to see if the acore process is alive before continuing if (device.getClient("android.process.acore") == 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)); } } } }