blob: a05673d7c97b868c48ccfca4c79665a82283c540 [file] [log] [blame]
package com.android.adblib
import com.android.adblib.AdbLibProperties.DEFAULT_SHELL_BUFFER_SIZE
import com.android.adblib.impl.AbbCommandImpl
import com.android.adblib.impl.DevicePropertiesImpl
import com.android.adblib.impl.ShellCommandImpl
import com.android.adblib.utils.AdbProtocolUtils
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.first
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.charset.Charset
import java.nio.file.Path
import java.nio.file.attribute.FileTime
import java.time.Duration
import java.util.concurrent.TimeoutException
/**
* Exposes services that are executed by the ADB daemon of a given device
*/
@IsThreadSafe
interface AdbDeviceServices {
/**
* The session this [AdbDeviceServices] instance belongs to.
*/
val session: AdbSession
/**
* ## Note
*
* __It is strongly recommended to use [AdbDeviceServices.shellCommand] instead of
* this [shell] method which is a low-level call to the `shell` services that comes
* with a few caveats and backward compatibility issues addressed by
* [AdbDeviceServices.shellCommand].__
*
*  
* ## Description
*
* Returns a [Flow] that, when collected, executes a shell command on a device
* ("<device-transport>:shell" query) and emits the `stdout` and `stderr` output from of
* the command to the [Flow].
*
* This is the equivalent of running "`/system/bin/sh -c `[command]" on the [device], meaning
* [command] can be any arbitrary shell invocation, including pipes and redirections, as
* opposed to executing a single process.
*
* __Note__: When collecting the command output, there is no way to distinguish between
* `stdout` or `stderr`, i.e. both streams are merged. There is also no way to know
* the `exit code` of the shell command. It is recommended to use [shellV2] instead for
* devices that support [AdbFeatures.SHELL_V2].
*
* The returned [Flow] elements are collected and emitted through a [ShellCollector],
* which enables advanced use cases for collecting, mapping, filtering and joining
* the command output which is initially collected as [ByteBuffer]. A typical use
* case is to use a [ShellCollector] that decodes the output as a [Flow] of [String],
* one for each line of the output.
*
* The flow is active until an exception is thrown, cancellation is requested by
* the flow consumer, or the shell command is terminated.
*
* The flow can throw [AdbProtocolErrorException], [AdbFailResponseException],
* [IOException] or any [Exception] thrown by [shellCollector]
*
* @param [device] the [DeviceSelector] corresponding to the target device
* @param [command] the shell command to execute
* @param [shellCollector] The [ShellCollector] invoked to collect the shell command output
* and emit elements to the resulting [Flow]
* @param [stdinChannel] is an optional [AdbChannel] providing bytes to send to the `stdin`
* of the shell command
* @param [commandTimeout] timeout tracking the command execution, tracking starts *after* the
* device connection has been successfully established. If the command takes more time than
* the timeout, a [TimeoutException] is thrown and the underlying [AdbChannel] is closed.
* @param [bufferSize] the size of the buffer used to receive data from the shell command output
* @param [shutdownOutput] shutdown device channel output end after piping [stdinChannel]
* @param [stripCrLf] Convert sequences of `CRLF` characters to `LF`. This should be used for
* devices at API level <= 23.
*
* @see AdbDeviceServices.shellCommand
* @see shellV2
*/
fun <T> shell(
device: DeviceSelector,
command: String,
shellCollector: ShellCollector<T>,
stdinChannel: AdbInputChannel? = null,
commandTimeout: Duration = INFINITE_DURATION,
bufferSize: Int = session.property(DEFAULT_SHELL_BUFFER_SIZE),
shutdownOutput: Boolean = true,
stripCrLf: Boolean = false,
): Flow<T>
/**
* ## Note
*
* __It is strongly recommended to use [AdbDeviceServices.shellCommand] instead of
* this [exec] method which is a low-level call to the `shell` services that comes
* with a few caveats and backward compatibility issues addressed by
* [AdbDeviceServices.shellCommand].__
*
* &nbsp;
* ## Description
*
* Returns a [Flow] that, when collected, executes a shell command on a device
* ("<device-transport>:exec" query) and emits the `stdout` output from of
* the command to the [Flow].
*
* See [shell] for a more detailed description. Unlike [shell], the [exec] service allows
* binary data transfer without mangling data.
*
* [exec] is intended to be used for copying binary data in one direction (host -> device)
* or the other (device->host), but does not allow running general purpose shell commands.
* In particular, cancelling a command started via exec does not always terminate the process
* on the device if that command is not actively writing to stdout or reading from stdin.
*
* See [git commit](https://android.googlesource.com/platform/system/core/+/5d9d434efadf1c535c7fea634d5306e18c68ef1f)
*
* __Note__: When collecting the command output, there is no way to access the contents
* of `stderr`. There is also no way to know the `exit code` of the shell command.
* It is recommended to use [shellV2] instead for devices that support [AdbFeatures.SHELL_V2].
*
* @see [shellV2]
*/
fun <T> exec(
device: DeviceSelector,
command: String,
shellCollector: ShellCollector<T>,
stdinChannel: AdbInputChannel? = null,
commandTimeout: Duration = INFINITE_DURATION,
bufferSize: Int = session.property(DEFAULT_SHELL_BUFFER_SIZE),
shutdownOutput: Boolean = true
): Flow<T>
/**
* Returns an [AdbChannel] ready for communication after invoking [command] on the [device].
*
* Note that the caller is responsible for calling `AdbChannel.close()` to close the channel.
*/
suspend fun rawExec(device: DeviceSelector, command: String): AdbChannel
/**
* ## Note
*
* __It is strongly recommended to use [AdbDeviceServices.shellCommand] instead of
* this [shellV2] method which is a low-level call to the `shell` services that comes
* with a few caveats and backward compatibility issues addressed by
* [AdbDeviceServices.shellCommand].__
*
* &nbsp;
* ## Description
*
* Returns a [Flow] that, when collected, executes a shell command on a device
* ("<device-transport>:shell,v2" query) and emits the output, as well as `stderr` and
* exit code, of the command to the [Flow].
*
* The returned [Flow] elements are collected and emitted through a [ShellV2Collector],
* which enables advanced use cases for collecting, mapping, filtering and joining
* the command output which is initially collected as [ByteBuffer]. A typical use
* case is to use a [ShellV2Collector] that decodes the output as a [Flow] of [String],
* one for each line of the output.
*
* The flow is active until an exception is thrown, cancellation is requested by
* the flow consumer, or the shell command is terminated.
*
* The flow can throw [AdbProtocolErrorException], [AdbFailResponseException],
* [IOException] or any [Exception] thrown by [shellCollector].
*
* __Note__: Support for the "shell v2" protocol was added in Android API 24 (Nougat).
* To verify the protocol is supported by the target device, call the
* [AdbHostServices.features] method and look for the [AdbFeatures.SHELL_V2] element in the
* resulting [List]. If protocol is not supported by the device, the returned [Flow] throws
* an [AdbFailResponseException].
*
* @param [device] the [DeviceSelector] corresponding to the target device
* @param [command] the shell command to execute
* @param [shellCollector] The [ShellV2Collector] invoked to collect the shell command output
* and emit elements to the resulting [Flow]
* @param [stdinChannel] is an optional [AdbChannel] providing bytes to send to the `stdin`
* of the shell command
* @param [commandTimeout] timeout tracking the command execution, tracking starts *after* the
* device connection has been successfully established. If the command takes more time than
* the timeout, a [TimeoutException] is thrown and the underlying [AdbChannel] is closed.
* @param [bufferSize] the size of the buffer used to receive data from shell command output
*/
fun <T> shellV2(
device: DeviceSelector,
command: String,
shellCollector: ShellV2Collector<T>,
stdinChannel: AdbInputChannel? = null,
commandTimeout: Duration = INFINITE_DURATION,
bufferSize: Int = session.property(DEFAULT_SHELL_BUFFER_SIZE),
): Flow<T>
/**
* Returns a [Flow] that, when collected, executes an "Android Binder Bridge" command on
* a device ("<device-transport>:abb_exec" query) and emits the `stdout` output from of
* the command to the [Flow]. This is the equivalent of running "`cmd `[args]" using
* [exec], except throughput is much higher.
*
* __Note__: To verify the "abb" protocol is supported by the target device, callers
* should invoke the [AdbHostServices.features] method and look for the
* [AdbFeatures.ABB_EXEC] element in the resulting [List]. If protocol is not supported
* by the device, the returned [Flow] throws an [AdbFailResponseException] and callers
* should fall back to using [shellV2] or [exec] with the equivalent "`cmd`" shell command.
*
* __Note__: When collecting the command output, there is no way to access the contents
* of `stderr`. There is also no way to know the `exit code` of the command. It is
* recommended to use [abb] instead for devices that support [AdbFeatures.ABB].
*
* The returned [Flow] elements are collected and emitted through a [ShellCollector],
* which enables advanced use cases for collecting, mapping, filtering and joining
* the command output which is initially collected as [ByteBuffer]. A typical use
* case is to use a [ShellCollector] that decodes the output as a [Flow] of [String],
* one for each line of the output.
*
* The flow is active until an exception is thrown, cancellation is requested by
* the flow consumer, or the shell command is terminated.
*
* The flow can throw [AdbProtocolErrorException], [AdbFailResponseException],
* [IOException] or any [Exception] thrown by [shellCollector]
*
* @param [device] the [DeviceSelector] corresponding to the target device
* @param [args] the arguments to pass to the "abb" service
* @param [shellCollector] The [ShellCollector] invoked to collect the shell command output
* and emit elements to the resulting [Flow]
* @param [stdinChannel] is an optional [AdbChannel] providing bytes to send to the `stdin`
* of the shell command
* @param [commandTimeout] timeout tracking the command execution, tracking starts *after* the
* device connection has been successfully established. If the command takes more time than
* the timeout, a [TimeoutException] is thrown and the underlying [AdbChannel] is closed.
* @param [bufferSize] the size of the buffer used to receive data from the shell command output
* @param [shutdownOutput] shutdown device channel output end after piping [stdinChannel]
*
* @see [abb]
*/
fun <T> abb_exec(
device: DeviceSelector,
args: List<String>,
shellCollector: ShellCollector<T>,
stdinChannel: AdbInputChannel? = null,
commandTimeout: Duration = INFINITE_DURATION,
bufferSize: Int = session.property(DEFAULT_SHELL_BUFFER_SIZE),
shutdownOutput: Boolean = true
): Flow<T>
/**
* Returns a [Flow] that, when collected, executes an "Android Binder Bridge" command on
* a device ("<device-transport>:abb" query) and emits the output, as well as `stderr` and
* exit code, of the command to the [Flow].
*
* The returned [Flow] elements are collected and emitted through a [ShellV2Collector],
* which enables advanced use cases for collecting, mapping, filtering and joining
* the command output which is initially collected as [ByteBuffer]. A typical use
* case is to use a [ShellV2Collector] that decodes the output as a [Flow] of [String],
* one for each line of the output.
*
* The flow is active until an exception is thrown, cancellation is requested by
* the flow consumer, or the shell command is terminated.
*
* The flow can throw [AdbProtocolErrorException], [AdbFailResponseException],
* [IOException] or any [Exception] thrown by [shellCollector].
*
* __Note__: To verify the protocol is supported by the target device, call the
* [AdbHostServices.features] method and look for the [AdbFeatures.ABB] element in the
* resulting [List]. If protocol is not supported by the device, the returned [Flow] throws
* an [AdbFailResponseException].
*
* @param [device] the [DeviceSelector] corresponding to the target device
* @param [args] the arguments to pass to the "abb" service
* @param [shellCollector] The [ShellV2Collector] invoked to collect the shell command output
* and emit elements to the resulting [Flow]
* @param [stdinChannel] is an optional [AdbChannel] providing bytes to send to the `stdin`
* of the shell command
* @param [commandTimeout] timeout tracking the command execution, tracking starts *after* the
* device connection has been successfully established. If the command takes more time than
* the timeout, a [TimeoutException] is thrown and the underlying [AdbChannel] is closed.
* @param [bufferSize] the size of the buffer used to receive data from shell command output
*/
fun <T> abb(
device: DeviceSelector,
args: List<String>,
shellCollector: ShellV2Collector<T>,
stdinChannel: AdbInputChannel? = null,
commandTimeout: Duration = INFINITE_DURATION,
bufferSize: Int = session.property(DEFAULT_SHELL_BUFFER_SIZE),
): Flow<T>
/**
* Opens a `sync` session on a device ("<device-transport>:sync" query) and returns
* an instance of [AdbDeviceSyncServices] that allows performing one or more file
* transfer operation with a device.
*
* The [AdbDeviceSyncServices] instance should be [closed][AutoCloseable.close]
* when no longer in use, to ensure the underlying connection to the device is
* closed.
*
* @param [device] the [DeviceSelector] corresponding to the target device
*/
suspend fun sync(device: DeviceSelector): AdbDeviceSyncServices
/**
* Returns the [list][ReverseSocketList] of all
* [reverse socket connections][ReverseSocketInfo] currently active on the
* given [device] ("`<device-transport>:reverse:list-forward`" query).
*/
suspend fun reverseListForward(device: DeviceSelector): ReverseSocketList
/**
* Creates a reverse forward socket connection from a [remote] device to the [local] host
* ("`<device-transport>:reverse:forward(:norebind):<remote>:<local>`" query).
*
* This method tells the ADB Daemon of the [device] to create a [server socket][SocketSpec]
* as specified by [remote]. The ADB Daemon listens to client connections made (on the
* device) to that server socket, and forwards each client connection to the [SocketSpec] on
* the host machine.
*
* When invoking this method, the ADB Daemon does not validate the format of the [local]
* socket specification. A connection to the [local] socket on the host machine is made
* only when a client connects to the [remote] server socket (on the device). At that point,
* if [local] is invalid, the new client connection is immediately closed.
*
* This method fails if the device already has a reverse connection with [remote] as the
* source, unless [rebind] is `true`.
*
* Returns the ADB Daemon reply to the request, typically a TCP port number if using
* `tcp:0` for [remote].
*/
suspend fun reverseForward(
device: DeviceSelector,
remote: SocketSpec,
local: SocketSpec,
rebind: Boolean = false
): String?
/**
* Closes a reverse socket connection on the given [device]
* ("`<device-transport>:reverse:killforward:<remote>`" query).
*/
suspend fun reverseKillForward(device: DeviceSelector, remote: SocketSpec)
/**
* Closes all reverse socket connections on the given [device]
* ("`<device-transport>:reverse:killforward-all`" query).
*/
suspend fun reverseKillForwardAll(device: DeviceSelector)
/**
* Returns a [Flow] that emits a new [ProcessIdList] everytime the set of active JDWP processes
* on the device has changed ("`<device-transport>:track-jdwp`" query).
*
* Once activated, the flow remains active until cancellation (exceptional or not) occurs from
* either the flow collector or the flow implementation, e.g. [IOException] from the
* underlying [AdbChannel].
*/
fun trackJdwp(device: DeviceSelector): Flow<ProcessIdList>
/**
* Returns a [Flow] that emits a new list of [AppProcessEntry] everytime the set of active
* processes on the device has changed ("`<device-transport>:track-app`" query).
*
* Once activated, the flow remains active until cancellation (exceptional or not) occurs from
* either the flow collector or the flow implementation, e.g. [IOException] from the
* underlying [AdbChannel].
*
* Note: This service was first available in Android `S` (i.e. [DeviceProperties.api] >= 31).
*/
fun trackApp(device: DeviceSelector): Flow<List<AppProcessEntry>>
/**
* Open a JDWP connection to the [process ID][pid] and returns an [AdbChannel] for
* that connection ("`<device-transport>:jdwp:<pid>`" query).
*
* The returned [AdbChannel] must be [closed][AdbChannel.close] then the JDWP
* connection is not needed anymore.
*
* Note: Only **one JDWP connection** at a time can be active for a given process ID
* on a given device.
* * On API <= 28, opening a second connection immediately fails with an [IOException]
* ("connection refused").
* * On API > 29, opening a second connection is delayed until the current JDWP connection
* is closed.
*/
suspend fun jdwp(device: DeviceSelector, pid: Int): AdbChannel
/**
* Restart adbd with root permissions ("`<device-transport>:root`" query).
*
* Note: This function does *not* wait for the device to come back online after it restarts.
* See the [rootAndWait] function for such a capability.
*/
suspend fun root(device: DeviceSelector): RootResult
/**
* Restart adbd without root permissions ("`<device-transport>:unroot`" query).
*
* Note: This function does *not* wait for the device to come back online after it restarts.
* See the [unRootAndWait] function for such a capability.
*/
suspend fun unRoot(device: DeviceSelector): RootResult
}
/**
* List of process IDs as returned by [AdbDeviceServices.trackJdwp], as well as list of
* [ErrorLine] in case some lines in the output from ADB were not recognized.
*/
typealias ProcessIdList = ListWithErrors<Int>
fun emptyProcessIdList(): ProcessIdList = emptyListWithErrors()
/**
* A single process entry returned by [AdbDeviceServices.trackApp]
*/
data class AppProcessEntry(
val pid: Int,
val debuggable: Boolean,
val profileable: Boolean,
val architecture: String)
/**
* A [ShellCollector] is responsible for mapping raw binary output of a shell command,
* provided as [ByteBuffer] instances, and emit mapped value to a [FlowCollector] of
* type [T].
*
* @see [AdbDeviceServices.shellCommand]
* @see [AdbDeviceServices.shell]
* @see [AdbDeviceServices.exec]
* @see [AdbDeviceServices.abb_exec]
*/
interface ShellCollector<T> {
/**
* Invoked by [AdbDeviceServices.shell] as soon as the shell command execution has started
* on the device, but before any output from `stdout` has been processed.
*
* [collector] The [FlowCollector] where flow elements should be emitted, if any.
*/
suspend fun start(collector: FlowCollector<T>)
/**
* Process a single [ByteBuffer] received from `stdout` of the shell command.
*
* [collector] The [FlowCollector] where flow elements should be emitted, if any.
*
* [stdout] The [ByteBuffer] containing a chunk of bytes collected from `stdout`.
* For performance reasons, the buffer is only valid during the method call so the data must
* be consumed directly in this method implementation.
*/
suspend fun collect(collector: FlowCollector<T>, stdout: ByteBuffer)
/**
* Invoked when `stdout` from the command shell has reached EOF, i.e. when the command
* execution has ended.
*
* [collector] The [FlowCollector] where leftover flow elements should be emitted, if any.
*/
suspend fun end(collector: FlowCollector<T>)
}
/**
* A [ShellV2Collector] is responsible for mapping raw binary output of a shell command,
* provided as [ByteBuffer] instances, and emit mapped value to a [FlowCollector] of
* type [T].
*
* @see [AdbDeviceServices.shellCommand]
* @see [AdbDeviceServices.shellV2]
* @see [AdbDeviceServices.abb]
*/
interface ShellV2Collector<T> {
/**
* Invoked by [AdbDeviceServices.shellV2] as soon as the shell command execution has started
* on the device, but before any output from `stdout` has been processed.
*
* @param collector The [FlowCollector] where flow elements should be emitted, if any.
*/
suspend fun start(collector: FlowCollector<T>)
/**
* Process a single [ByteBuffer] received from `stdout` of the shell command.
*
* @param collector The [FlowCollector] where flow elements should be emitted, if any.
* @param stdout The [ByteBuffer] containing a chunk of bytes collected from `stdout`.
* For performance reasons, the buffer is only valid during the method
* call so the data must be consumed directly in this method implementation.
*/
suspend fun collectStdout(collector: FlowCollector<T>, stdout: ByteBuffer)
/**
* Process a single [ByteBuffer] received from `stderr` of the shell command.
*
* @param collector The [FlowCollector] where flow elements should be emitted, if any.
* @param stderr The [ByteBuffer] containing a chunk of bytes collected from `stderr`.
* For performance reasons, the buffer is only valid during the method call so
* the data must be consumed directly in this method implementation.
*/
suspend fun collectStderr(collector: FlowCollector<T>, stderr: ByteBuffer)
/**
* Invoked when the shell command has exited
*
* @param collector The [FlowCollector] where flow elements should be emitted, if any.
* @param exitCode The exit code of the command
*/
suspend fun end(collector: FlowCollector<T>, exitCode: Int)
}
/**
* Describes capabilities of [ShellCollector] or [ShellV2Collector] implementations
*/
interface ShellCollectorCapabilities {
/**
* Whether this [ShellCollector] or [ShellV2Collector] implementation should be
* used with [ShellCommand.executeAsSingleOutput] instead of [ShellCommand.execute]
*/
val isSingleOutput: Boolean
}
/**
* Creates a [ShellCommand] to [execute][ShellCommand.execute] a shell [command] on a
* given [device], taking advantage of features available only on more recent devices
* (e.g. [AdbDeviceServices.shellV2]), in addition to other customization such as
* applying an [ShellV2Collector] and configuring timeouts.
*
* The returned [ShellCommand] only becomes fully typed when [ShellCommand.withCollector]
* is invoked.
*
* Example:
* ```
* val stdout: String = shellCommand(device, "ls -l")
* .withCollector(TextShellV2Collector())
* .withCommandTimeout(Duration.ofSeconds(5))
* .execute()
* .first()
* .stdout
* ```
*/
fun AdbDeviceServices.shellCommand(device: DeviceSelector, command: String): ShellCommand<*> {
return ShellCommandImpl<Any>(this.session, device, command)
}
/**
* Use [shellCommand] to capture the command output as a single [ShellCommandOutput] instance.
* Both [ShellCommandOutput.stdout] and [ShellCommandOutput.stderr] are decoded using
* the [AdbProtocolUtils.ADB_CHARSET]&nbsp;[Charset] character set.
*
* Depending on the capabilities of the target device, either [AdbDeviceServices.shellV2],
* [AdbDeviceServices.exec] or [AdbDeviceServices.shell] is invoked.
*
* [ShellCommandOutput.stderr] and [ShellCommandOutput.exitCode] are initialized
* only if [AdbDeviceServices.shellV2] is used.
*
* Note: This method should be used only for commands that output a relatively small
* amount of text.
*
* @see shellCommand
* @see shellAsLines
* @see shellAsLineBatches
*/
suspend fun AdbDeviceServices.shellAsText(
device: DeviceSelector,
command: String,
stdinChannel: AdbInputChannel? = null,
commandTimeout: Duration = INFINITE_DURATION,
bufferSize: Int = session.property(DEFAULT_SHELL_BUFFER_SIZE),
): ShellCommandOutput {
return shellCommand(device, command)
.withTextCollector()
.withStdin(stdinChannel)
.withCommandTimeout(commandTimeout)
.withBufferSize(bufferSize)
.execute()
.first()
}
/**
* Use [shellCommand] to capture the command output as a [Flow] of [ShellCommandOutputElement],
* typically one entry per line of `stdout` or `stderr`.
*
* The last element of the [Flow] is always a [ShellCommandOutputElement.ExitCode] element,
* representing the exit code of the shell command.
*
* Depending on the capabilities of the target device, either [AdbDeviceServices.shellV2],
* [AdbDeviceServices.exec] or [AdbDeviceServices.shell].
*
* @see [shellCommand]
*/
fun AdbDeviceServices.shellAsLines(
device: DeviceSelector,
command: String,
stdinChannel: AdbInputChannel? = null,
commandTimeout: Duration = INFINITE_DURATION,
bufferSize: Int = session.property(DEFAULT_SHELL_BUFFER_SIZE),
): Flow<ShellCommandOutputElement> {
return shellCommand(device, command)
.withLineCollector()
.withStdin(stdinChannel)
.withCommandTimeout(commandTimeout)
.withBufferSize(bufferSize)
.execute()
}
/**
* Use [shellCommand] to capture the command output as a [Flow] of
* [BatchShellCommandOutputElement], which allows decoding `stdout` and `stderr`
* as [Lists][List] of [Strings][String] decoded a binary data packets are
* received. The size of each batch depends on the rate of output of the
* shell command on the device and [bufferSize].
*
* The last element of the [Flow] is always a [BatchShellCommandOutputElement.ExitCode] element,
* representing the exit code of the shell command.
*
* Depending on the capabilities of the target device, either [AdbDeviceServices.shellV2],
* [AdbDeviceServices.exec] or [AdbDeviceServices.shell].
*
* @see [shellCommand]
*/
fun AdbDeviceServices.shellAsLineBatches(
device: DeviceSelector,
command: String,
stdinChannel: AdbInputChannel? = null,
commandTimeout: Duration = INFINITE_DURATION,
bufferSize: Int = session.property(DEFAULT_SHELL_BUFFER_SIZE),
): Flow<BatchShellCommandOutputElement> {
return shellCommand(device, command)
.withLineBatchCollector()
.withStdin(stdinChannel)
.withCommandTimeout(commandTimeout)
.withBufferSize(bufferSize)
.execute()
}
/**
* Creates a [AbbCommand] to [execute][AbbCommand.execute] an abb command on a
* given [device].
*
* The returned [AbbCommand] only becomes fully typed when [ShellCommand.withCollector]
* is invoked.
*
* Example:
* ```
* val stdout: String = abbCommand(device, listOf("package", "list", "packages"))
* .withCollector(TextShellV2Collector())
* .withCommandTimeout(Duration.ofSeconds(5))
* .execute()
* .first()
* .stdout
* ```
*/
fun AdbDeviceServices.abbCommand(device: DeviceSelector, args: List<String>): AbbCommand<*> {
return AbbCommandImpl<Any>(this.session, device, args)
}
/**
* Uploads a single file to a remote device transferring the contents of [sourceChannel].
*
* @see [AdbDeviceSyncServices.send]
*/
suspend fun AdbDeviceServices.syncSend(
device: DeviceSelector,
sourceChannel: AdbInputChannel,
remoteFilePath: String,
remoteFileMode: RemoteFileMode,
remoteFileTime: FileTime? = null,
progress: SyncProgress? = null,
bufferSize: Int = SYNC_DATA_MAX
) {
sync(device).use {
it.send(
sourceChannel,
remoteFilePath,
remoteFileMode,
remoteFileTime,
progress,
bufferSize
)
}
}
/**
* Uploads a single file to a remote device transferring the contents of [sourcePath].
*
* @see [AdbDeviceSyncServices.send]
*/
suspend fun AdbDeviceServices.syncSend(
device: DeviceSelector,
sourcePath: Path,
remoteFilePath: String,
remoteFileMode: RemoteFileMode,
remoteFileTime: FileTime? = null,
progress: SyncProgress? = null,
bufferSize: Int = SYNC_DATA_MAX
) {
session.channelFactory.openFile(sourcePath).use { source ->
syncSend(
device,
source,
remoteFilePath,
remoteFileMode,
remoteFileTime,
progress,
bufferSize
)
source.close()
}
}
/**
* Retrieves a single file from a remote device and writes its contents to a [destinationChannel].
*
* @see [AdbDeviceSyncServices.recv]
*/
suspend fun AdbDeviceServices.syncRecv(
device: DeviceSelector,
remoteFilePath: String,
destinationChannel: AdbOutputChannel,
progress: SyncProgress? = null,
bufferSize: Int = SYNC_DATA_MAX
) {
sync(device).use {
it.recv(
remoteFilePath,
destinationChannel,
progress,
bufferSize
)
}
}
/**
* Retrieves a single file from a remote device and writes its contents to a [destinationPath].
*
* @see [AdbDeviceSyncServices.recv]
*/
suspend fun AdbDeviceServices.syncRecv(
device: DeviceSelector,
remoteFilePath: String,
destinationPath: Path,
progress: SyncProgress? = null,
bufferSize: Int = SYNC_DATA_MAX
) {
session.channelFactory.createFile(destinationPath).use { destination ->
syncRecv(
device,
remoteFilePath,
destination,
progress,
bufferSize
)
destination.close()
}
}
/**
* Stat a single file from a remote device.
*
* @see [AdbDeviceSyncServices.stat]
*/
suspend fun AdbDeviceServices.syncStat(
device: DeviceSelector,
remoteFilePath: String
): FileStat? {
return sync(device).use {
it.stat(remoteFilePath)
}
}
/**
* Returns a [DeviceProperties] instance for the given device. [DeviceProperties]
* gives access to device properties returned by the `getprop` shell command.
*/
fun AdbDeviceServices.deviceProperties(device: DeviceSelector): DeviceProperties {
return DevicePropertiesImpl(this, device)
}
interface DeviceProperties {
/**
* Returns a [List] of [DeviceProperty] entries representing the result of executing
* the `"getprop"` shell command on the device.
*/
suspend fun all(): List<DeviceProperty>
/**
* Returns a subset of [all] of properties that start with `"ro."`. Since these properties
* don't change until a device is restarted, the returned [Map] is cached as long as the
* device is online.
*/
suspend fun allReadonly(): Map<String, String>
/**
* Return the API level (as an [Int]) of the device, or [default] if an error
* occurs.
*/
suspend fun api(default: Int = 1): Int
}
data class DeviceProperty(val name: String, val value: String)
/**
* The result of [AdbDeviceServices.root] or [AdbDeviceServices.unRoot]
*/
class RootResult(
/**
* The full status string returned by `adbd` (the ADB Daemon on the device)
*/
val rawStatus: String
) {
/**
* Whether the [AdbDeviceServices.root] or [AdbDeviceServices.unRoot] operation resulted
* in the device restarting: When ADBD has to switch to/from the `root` or `unroot` state,
* it needs to restart itself, resulting the device disconnecting for a short period of time.
*/
val restarting: Boolean
get() = status.startsWith("restarting")
/**
* The full status string returned by `adbd` (the ADB Daemon on the device), without the
* trailing newline.
*/
val status: String
get() = rawStatus.trim('\n')
override fun toString(): String {
return "${this::class.simpleName}(status=\"$status\", restarting=$restarting)"
}
}
/**
* Restart adbd with root permissions, waiting for [device] to come back online
* if [RootResult.restarting] is `true`.
*/
suspend fun AdbDeviceServices.rootAndWait(device: DeviceSelector): RootResult {
return rootAndWaitImpl(device) { deviceSelector -> root(deviceSelector) }
}
/**
* Restart adbd without root permissions, waiting for [device] to come back online
* if [RootResult.restarting] is `true`.
*/
suspend fun AdbDeviceServices.unRootAndWait(device: DeviceSelector): RootResult {
return rootAndWaitImpl(device) { deviceSelector -> unRoot(deviceSelector) }
}
private suspend fun AdbDeviceServices.rootAndWaitImpl(
device: DeviceSelector,
block: suspend AdbDeviceServices.(DeviceSelector) -> RootResult
): RootResult {
// Call root/unroot, retrieving the device transport id
val deviceWithTransportId = device.withTransportIdInResponse()
return block(deviceWithTransportId).also { rootResult ->
if (rootResult.restarting) {
// Wait for device to come back online using the transport id we received
val transportId = deviceWithTransportId.transportId
?: throw AdbProtocolErrorException("Transport ID is not present in device selector")
session.hostServices.waitFor(
DeviceSelector.fromTransportId(transportId),
WaitForState.DISCONNECT)
session.hostServices.waitFor(device, WaitForState.ONLINE)
}
}
}