blob: 0b64060f24edba4accb28a0ed21676662d18b2a0 [file] [log] [blame]
/*
* Copyright (C) 2019 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.run
import com.android.annotations.concurrency.AnyThread
import com.android.annotations.concurrency.WorkerThread
import com.android.ddmlib.Client
import com.android.ddmlib.IDevice
import com.android.tools.idea.run.deployable.SwappableProcessHandler
import com.android.tools.idea.run.deployment.AndroidExecutionTarget
import com.intellij.execution.DefaultExecutionTarget
import com.intellij.execution.ExecutionTarget
import com.intellij.execution.ExecutionTargetManager
import com.intellij.execution.KillableProcess
import com.intellij.execution.configurations.RunConfiguration
import com.intellij.execution.process.ProcessHandler
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.util.concurrency.AppExecutorUtil
import java.io.OutputStream
/**
* A [ProcessHandler] that corresponds to a single Android app potentially running on multiple connected devices.
*
* This process handler monitors remote processes running on Android devices with an application name of [targetApplicationId].
* You can add Android device by [addTargetDevice] and logcat messages from all monitored devices will be redirected and broadcast
* by [notifyTextAvailable].
*
* As same as regular process handler, it has two terminal states, detach and destroy.
*
* You can reach at detach state only if you call [detachProcess] before no target processes start or while those processes are running.
* When you detach, all those processes are kept running and this process handler just stops capturing logcat messages from them.
*
* There are two ways you can get to destroy state. First, if you call [destroyProcess] method, this process handler terminates all
* running target processes and moves to destroy state. Second, when all target processes terminate this process handler automatically
* terminate.
*
* @param project IDE project which uses this process handler
* @param targetApplicationId a target application id to be monitored
* @param captureLogcat true if you need logcat message to be captured and displayed in an attached console view, false otherwise
* @param deploymentApplicationService a service to be used to look up running processes on a device
* @param androidProcessMonitorManagerFactory a factory method to construct [AndroidProcessMonitorManager]
*/
class AndroidProcessHandler @JvmOverloads constructor(
private val project: Project,
val targetApplicationId: String,
val captureLogcat: Boolean = true,
private val deploymentApplicationService: DeploymentApplicationService = DeploymentApplicationService.getInstance(),
androidProcessMonitorManagerFactory: AndroidProcessMonitorManagerFactory = { _, _, textEmitter, listener ->
AndroidProcessMonitorManager(targetApplicationId, deploymentApplicationService, textEmitter, captureLogcat, listener)
}) : ProcessHandler(), KillableProcess, SwappableProcessHandler {
companion object {
private var LOG = Logger.getInstance(AndroidProcessHandler::class.java)
}
init {
putCopyableUserData(SwappableProcessHandler.EXTENSION_KEY, this)
}
/**
* Logcat messages from all target devices are redirected to [notifyTextAvailable]. When all target processes terminate on
* all devices, it invokes [destroyProcess] to terminate android process handler.
*/
private val myMonitorManager = androidProcessMonitorManagerFactory.invoke(
targetApplicationId,
deploymentApplicationService,
object : TextEmitter {
override fun emit(message: String, key: Key<*>) = notifyTextAvailable(message, key)
},
object : AndroidProcessMonitorManagerListener {
override fun onAllTargetProcessesTerminated() = destroyProcess()
})
/**
* Adds a target device to this handler.
*/
@AnyThread
fun addTargetDevice(device: IDevice) {
myMonitorManager.add(device)
// Keep track of the lowest API level among the monitored devices by this handler.
synchronized(this) {
val lowestApiLevel = getUserData(AndroidSessionInfo.ANDROID_DEVICE_API_LEVEL)
if (lowestApiLevel == null || device.version < lowestApiLevel) {
putUserData(AndroidSessionInfo.ANDROID_DEVICE_API_LEVEL, device.version)
}
}
LOG.info("Adding device ${device.name} to monitor for launched app: ${targetApplicationId}")
}
/**
* Detaches a given device from target devices. No-op if the given device is not associated with this handler.
*/
@WorkerThread
fun detachDevice(device: IDevice) {
myMonitorManager.getMonitor(device)?.detachAndClose()
}
/**
* Checks if a given device is monitored by this handler. Returns true if it is monitored otherwise false.
*/
@AnyThread
fun isAssociated(device: IDevice) = myMonitorManager.isAssociated(device)
/**
* Returns jdwp client of a target application running on a given device, or null if the device is not monitored by
* this handler or the process is not running on a device.
*/
@WorkerThread
fun getClient(device: IDevice): Client? {
return if (isAssociated(device)) {
deploymentApplicationService.findClient(device, targetApplicationId).firstOrNull()
}
else {
null
}
}
/**
* Initiates a termination of managed processes. This method returns without waiting for processes' termination.
* It just moves the process handler's state to to-be-destroyed state and [isProcessTerminating] becomes true
* after the method call. Upon the processes termination, the state moves to destroyed and [isProcessTerminated]
* becomes true. You can listen state changes by registering a lister by [addProcessListener]. When processes are
* being destroyed, [com.intellij.execution.process.ProcessListener.processWillTerminate] is called with
* willBeDestroyed = true.
*/
@AnyThread
override fun destroyProcessImpl() {
AppExecutorUtil.getAppExecutorService().submit {
myMonitorManager.close()
notifyProcessTerminated(0)
}
}
/**
* Initiates a detach of managed processes. This method returns without waiting for processes' to be detached.
* It just moves the process handler's state to to-be-destroyed state and [isProcessTerminating] becomes true
* after the method call. Upon the processes are detached, the state moves to destroyed and [isProcessTerminated]
* becomes true. You can listen state changes by registering a lister by [addProcessListener]. When processes are
* being destroyed, [com.intellij.execution.process.ProcessListener.processWillTerminate] is called with
* willBeDestroyed = false.
*/
@AnyThread
override fun detachProcessImpl() {
AppExecutorUtil.getAppExecutorService().submit {
myMonitorManager.detachAndClose()
notifyProcessDetached()
}
}
@AnyThread
override fun detachIsDefault() = false
@AnyThread
override fun getProcessInput(): OutputStream? = null
/**
* We provide a custom implementation to tie the device combo box selector to the global Stop button.
* Note the global Stop button prefers the result of this method over content descriptor internal state,
* but the tool window Stop button prefers the content descriptor internal state over this method.
*/
@AnyThread
override fun canKillProcess(): Boolean {
val activeTarget = ExecutionTargetManager.getInstance(project).activeTarget
if (activeTarget === DefaultExecutionTarget.INSTANCE || activeTarget !is AndroidExecutionTarget) {
return false
}
return areAnyDevicesAssociated(activeTarget)
}
@AnyThread
override fun killProcess() {
destroyProcess()
}
@AnyThread
override fun getExecutor() = getUserData(AndroidSessionInfo.KEY)?.executor
@AnyThread
override fun isRunningWith(runConfiguration: RunConfiguration, executionTarget: ExecutionTarget): Boolean {
val sessionInfo = getUserData(AndroidSessionInfo.KEY) ?: return false
if (sessionInfo.runConfiguration !== runConfiguration) {
return false
}
if (executionTarget is AndroidExecutionTarget) {
return areAnyDevicesAssociated(executionTarget)
}
return sessionInfo.executionTarget.id == executionTarget.id
}
@AnyThread
private fun areAnyDevicesAssociated(executionTarget: AndroidExecutionTarget): Boolean {
return executionTarget.devices.any { isAssociated(it) }
}
}
private typealias AndroidProcessMonitorManagerFactory = (targetApplicationId: String,
deploymentApplicationService: DeploymentApplicationService,
textEmitter: TextEmitter,
listener: AndroidProcessMonitorManagerListener) -> AndroidProcessMonitorManager