blob: 93d0d1f8a1bc7fc4483553dc8342e287fbe8fd2c [file] [log] [blame]
//
// Copyright 2010 The Android Open Source Project
//
// Provides a shared memory transport for input events.
//
#define LOG_TAG "InputTransport"
//#define LOG_NDEBUG 0
// Log debug messages about channel signalling (send signal, receive signal)
#define DEBUG_CHANNEL_SIGNALS 0
// Log debug messages whenever InputChannel objects are created/destroyed
#define DEBUG_CHANNEL_LIFECYCLE 0
// Log debug messages about transport actions (initialize, reset, publish, ...)
#define DEBUG_TRANSPORT_ACTIONS 0
#include <cutils/ashmem.h>
#include <cutils/log.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <ui/InputTransport.h>
#include <unistd.h>
namespace android {
#define ROUND_UP(value, boundary) (((value) + (boundary) - 1) & ~((boundary) - 1))
#define MIN_HISTORY_DEPTH 20
// Must be at least sizeof(InputMessage) + sufficient space for pointer data
static const int DEFAULT_MESSAGE_BUFFER_SIZE = ROUND_UP(
sizeof(InputMessage) + MIN_HISTORY_DEPTH
* (sizeof(InputMessage::SampleData) + MAX_POINTERS * sizeof(PointerCoords)),
4096);
// Signal sent by the producer to the consumer to inform it that a new message is
// available to be consumed in the shared memory buffer.
static const char INPUT_SIGNAL_DISPATCH = 'D';
// Signal sent by the consumer to the producer to inform it that it has finished
// consuming the most recent message and it handled it.
static const char INPUT_SIGNAL_FINISHED_HANDLED = 'f';
// Signal sent by the consumer to the producer to inform it that it has finished
// consuming the most recent message but it did not handle it.
static const char INPUT_SIGNAL_FINISHED_UNHANDLED = 'u';
// --- InputChannel ---
InputChannel::InputChannel(const String8& name, int32_t ashmemFd, int32_t receivePipeFd,
int32_t sendPipeFd) :
mName(name), mAshmemFd(ashmemFd), mReceivePipeFd(receivePipeFd), mSendPipeFd(sendPipeFd) {
#if DEBUG_CHANNEL_LIFECYCLE
LOGD("Input channel constructed: name='%s', ashmemFd=%d, receivePipeFd=%d, sendPipeFd=%d",
mName.string(), ashmemFd, receivePipeFd, sendPipeFd);
#endif
int result = fcntl(mReceivePipeFd, F_SETFL, O_NONBLOCK);
LOG_ALWAYS_FATAL_IF(result != 0, "channel '%s' ~ Could not make receive pipe "
"non-blocking. errno=%d", mName.string(), errno);
result = fcntl(mSendPipeFd, F_SETFL, O_NONBLOCK);
LOG_ALWAYS_FATAL_IF(result != 0, "channel '%s' ~ Could not make send pipe "
"non-blocking. errno=%d", mName.string(), errno);
}
InputChannel::~InputChannel() {
#if DEBUG_CHANNEL_LIFECYCLE
LOGD("Input channel destroyed: name='%s', ashmemFd=%d, receivePipeFd=%d, sendPipeFd=%d",
mName.string(), mAshmemFd, mReceivePipeFd, mSendPipeFd);
#endif
::close(mAshmemFd);
::close(mReceivePipeFd);
::close(mSendPipeFd);
}
status_t InputChannel::openInputChannelPair(const String8& name,
sp<InputChannel>& outServerChannel, sp<InputChannel>& outClientChannel) {
status_t result;
int serverAshmemFd = ashmem_create_region(name.string(), DEFAULT_MESSAGE_BUFFER_SIZE);
if (serverAshmemFd < 0) {
result = -errno;
LOGE("channel '%s' ~ Could not create shared memory region. errno=%d",
name.string(), errno);
} else {
result = ashmem_set_prot_region(serverAshmemFd, PROT_READ | PROT_WRITE);
if (result < 0) {
LOGE("channel '%s' ~ Error %d trying to set protection of ashmem fd %d.",
name.string(), result, serverAshmemFd);
} else {
// Dup the file descriptor because the server and client input channel objects that
// are returned may have different lifetimes but they share the same shared memory region.
int clientAshmemFd;
clientAshmemFd = dup(serverAshmemFd);
if (clientAshmemFd < 0) {
result = -errno;
LOGE("channel '%s' ~ Could not dup() shared memory region fd. errno=%d",
name.string(), errno);
} else {
int forward[2];
if (pipe(forward)) {
result = -errno;
LOGE("channel '%s' ~ Could not create forward pipe. errno=%d",
name.string(), errno);
} else {
int reverse[2];
if (pipe(reverse)) {
result = -errno;
LOGE("channel '%s' ~ Could not create reverse pipe. errno=%d",
name.string(), errno);
} else {
String8 serverChannelName = name;
serverChannelName.append(" (server)");
outServerChannel = new InputChannel(serverChannelName,
serverAshmemFd, reverse[0], forward[1]);
String8 clientChannelName = name;
clientChannelName.append(" (client)");
outClientChannel = new InputChannel(clientChannelName,
clientAshmemFd, forward[0], reverse[1]);
return OK;
}
::close(forward[0]);
::close(forward[1]);
}
::close(clientAshmemFd);
}
}
::close(serverAshmemFd);
}
outServerChannel.clear();
outClientChannel.clear();
return result;
}
status_t InputChannel::sendSignal(char signal) {
ssize_t nWrite;
do {
nWrite = ::write(mSendPipeFd, & signal, 1);
} while (nWrite == -1 && errno == EINTR);
if (nWrite == 1) {
#if DEBUG_CHANNEL_SIGNALS
LOGD("channel '%s' ~ sent signal '%c'", mName.string(), signal);
#endif
return OK;
}
#if DEBUG_CHANNEL_SIGNALS
LOGD("channel '%s' ~ error sending signal '%c', errno=%d", mName.string(), signal, errno);
#endif
return -errno;
}
status_t InputChannel::receiveSignal(char* outSignal) {
ssize_t nRead;
do {
nRead = ::read(mReceivePipeFd, outSignal, 1);
} while (nRead == -1 && errno == EINTR);
if (nRead == 1) {
#if DEBUG_CHANNEL_SIGNALS
LOGD("channel '%s' ~ received signal '%c'", mName.string(), *outSignal);
#endif
return OK;
}
if (nRead == 0) { // check for EOF
#if DEBUG_CHANNEL_SIGNALS
LOGD("channel '%s' ~ receive signal failed because peer was closed", mName.string());
#endif
return DEAD_OBJECT;
}
if (errno == EAGAIN) {
#if DEBUG_CHANNEL_SIGNALS
LOGD("channel '%s' ~ receive signal failed because no signal available", mName.string());
#endif
return WOULD_BLOCK;
}
#if DEBUG_CHANNEL_SIGNALS
LOGD("channel '%s' ~ receive signal failed, errno=%d", mName.string(), errno);
#endif
return -errno;
}
// --- InputPublisher ---
InputPublisher::InputPublisher(const sp<InputChannel>& channel) :
mChannel(channel), mSharedMessage(NULL),
mPinned(false), mSemaphoreInitialized(false), mWasDispatched(false),
mMotionEventSampleDataTail(NULL) {
}
InputPublisher::~InputPublisher() {
reset();
if (mSharedMessage) {
munmap(mSharedMessage, mAshmemSize);
}
}
status_t InputPublisher::initialize() {
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' publisher ~ initialize",
mChannel->getName().string());
#endif
int ashmemFd = mChannel->getAshmemFd();
int result = ashmem_get_size_region(ashmemFd);
if (result < 0) {
LOGE("channel '%s' publisher ~ Error %d getting size of ashmem fd %d.",
mChannel->getName().string(), result, ashmemFd);
return UNKNOWN_ERROR;
}
mAshmemSize = (size_t) result;
mSharedMessage = static_cast<InputMessage*>(mmap(NULL, mAshmemSize,
PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0));
if (! mSharedMessage) {
LOGE("channel '%s' publisher ~ mmap failed on ashmem fd %d.",
mChannel->getName().string(), ashmemFd);
return NO_MEMORY;
}
mPinned = true;
mSharedMessage->consumed = false;
return reset();
}
status_t InputPublisher::reset() {
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' publisher ~ reset",
mChannel->getName().string());
#endif
if (mPinned) {
// Destroy the semaphore since we are about to unpin the memory region that contains it.
int result;
if (mSemaphoreInitialized) {
if (mSharedMessage->consumed) {
result = sem_post(& mSharedMessage->semaphore);
if (result < 0) {
LOGE("channel '%s' publisher ~ Error %d in sem_post.",
mChannel->getName().string(), errno);
return UNKNOWN_ERROR;
}
}
result = sem_destroy(& mSharedMessage->semaphore);
if (result < 0) {
LOGE("channel '%s' publisher ~ Error %d in sem_destroy.",
mChannel->getName().string(), errno);
return UNKNOWN_ERROR;
}
mSemaphoreInitialized = false;
}
// Unpin the region since we no longer care about its contents.
int ashmemFd = mChannel->getAshmemFd();
result = ashmem_unpin_region(ashmemFd, 0, 0);
if (result < 0) {
LOGE("channel '%s' publisher ~ Error %d unpinning ashmem fd %d.",
mChannel->getName().string(), result, ashmemFd);
return UNKNOWN_ERROR;
}
mPinned = false;
}
mMotionEventSampleDataTail = NULL;
mWasDispatched = false;
return OK;
}
status_t InputPublisher::publishInputEvent(
int32_t type,
int32_t deviceId,
int32_t source) {
if (mPinned) {
LOGE("channel '%s' publisher ~ Attempted to publish a new event but publisher has "
"not yet been reset.", mChannel->getName().string());
return INVALID_OPERATION;
}
// Pin the region.
// We do not check for ASHMEM_NOT_PURGED because we don't care about the previous
// contents of the buffer so it does not matter whether it was purged in the meantime.
int ashmemFd = mChannel->getAshmemFd();
int result = ashmem_pin_region(ashmemFd, 0, 0);
if (result < 0) {
LOGE("channel '%s' publisher ~ Error %d pinning ashmem fd %d.",
mChannel->getName().string(), result, ashmemFd);
return UNKNOWN_ERROR;
}
mPinned = true;
result = sem_init(& mSharedMessage->semaphore, 1, 1);
if (result < 0) {
LOGE("channel '%s' publisher ~ Error %d in sem_init.",
mChannel->getName().string(), errno);
return UNKNOWN_ERROR;
}
mSemaphoreInitialized = true;
mSharedMessage->consumed = false;
mSharedMessage->type = type;
mSharedMessage->deviceId = deviceId;
mSharedMessage->source = source;
return OK;
}
status_t InputPublisher::publishKeyEvent(
int32_t deviceId,
int32_t source,
int32_t action,
int32_t flags,
int32_t keyCode,
int32_t scanCode,
int32_t metaState,
int32_t repeatCount,
nsecs_t downTime,
nsecs_t eventTime) {
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' publisher ~ publishKeyEvent: deviceId=%d, source=0x%x, "
"action=0x%x, flags=0x%x, keyCode=%d, scanCode=%d, metaState=0x%x, repeatCount=%d,"
"downTime=%lld, eventTime=%lld",
mChannel->getName().string(),
deviceId, source, action, flags, keyCode, scanCode, metaState, repeatCount,
downTime, eventTime);
#endif
status_t result = publishInputEvent(AINPUT_EVENT_TYPE_KEY, deviceId, source);
if (result < 0) {
return result;
}
mSharedMessage->key.action = action;
mSharedMessage->key.flags = flags;
mSharedMessage->key.keyCode = keyCode;
mSharedMessage->key.scanCode = scanCode;
mSharedMessage->key.metaState = metaState;
mSharedMessage->key.repeatCount = repeatCount;
mSharedMessage->key.downTime = downTime;
mSharedMessage->key.eventTime = eventTime;
return OK;
}
status_t InputPublisher::publishMotionEvent(
int32_t deviceId,
int32_t source,
int32_t action,
int32_t flags,
int32_t edgeFlags,
int32_t metaState,
float xOffset,
float yOffset,
float xPrecision,
float yPrecision,
nsecs_t downTime,
nsecs_t eventTime,
size_t pointerCount,
const int32_t* pointerIds,
const PointerCoords* pointerCoords) {
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' publisher ~ publishMotionEvent: deviceId=%d, source=0x%x, "
"action=0x%x, flags=0x%x, edgeFlags=0x%x, metaState=0x%x, xOffset=%f, yOffset=%f, "
"xPrecision=%f, yPrecision=%f, downTime=%lld, eventTime=%lld, "
"pointerCount=%d",
mChannel->getName().string(),
deviceId, source, action, flags, edgeFlags, metaState, xOffset, yOffset,
xPrecision, yPrecision, downTime, eventTime, pointerCount);
#endif
if (pointerCount > MAX_POINTERS || pointerCount < 1) {
LOGE("channel '%s' publisher ~ Invalid number of pointers provided: %d.",
mChannel->getName().string(), pointerCount);
return BAD_VALUE;
}
status_t result = publishInputEvent(AINPUT_EVENT_TYPE_MOTION, deviceId, source);
if (result < 0) {
return result;
}
mSharedMessage->motion.action = action;
mSharedMessage->motion.flags = flags;
mSharedMessage->motion.edgeFlags = edgeFlags;
mSharedMessage->motion.metaState = metaState;
mSharedMessage->motion.xOffset = xOffset;
mSharedMessage->motion.yOffset = yOffset;
mSharedMessage->motion.xPrecision = xPrecision;
mSharedMessage->motion.yPrecision = yPrecision;
mSharedMessage->motion.downTime = downTime;
mSharedMessage->motion.pointerCount = pointerCount;
mSharedMessage->motion.sampleCount = 1;
mSharedMessage->motion.sampleData[0].eventTime = eventTime;
for (size_t i = 0; i < pointerCount; i++) {
mSharedMessage->motion.pointerIds[i] = pointerIds[i];
mSharedMessage->motion.sampleData[0].coords[i].copyFrom(pointerCoords[i]);
}
// Cache essential information about the motion event to ensure that a malicious consumer
// cannot confuse the publisher by modifying the contents of the shared memory buffer while
// it is being updated.
if (action == AMOTION_EVENT_ACTION_MOVE
|| action == AMOTION_EVENT_ACTION_HOVER_MOVE) {
mMotionEventPointerCount = pointerCount;
mMotionEventSampleDataStride = InputMessage::sampleDataStride(pointerCount);
mMotionEventSampleDataTail = InputMessage::sampleDataPtrIncrement(
mSharedMessage->motion.sampleData, mMotionEventSampleDataStride);
} else {
mMotionEventSampleDataTail = NULL;
}
return OK;
}
status_t InputPublisher::appendMotionSample(
nsecs_t eventTime,
const PointerCoords* pointerCoords) {
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' publisher ~ appendMotionSample: eventTime=%lld",
mChannel->getName().string(), eventTime);
#endif
if (! mPinned || ! mMotionEventSampleDataTail) {
LOGE("channel '%s' publisher ~ Cannot append motion sample because there is no current "
"AMOTION_EVENT_ACTION_MOVE event.", mChannel->getName().string());
return INVALID_OPERATION;
}
InputMessage::SampleData* newTail = InputMessage::sampleDataPtrIncrement(
mMotionEventSampleDataTail, mMotionEventSampleDataStride);
size_t newBytesUsed = reinterpret_cast<char*>(newTail) -
reinterpret_cast<char*>(mSharedMessage);
if (newBytesUsed > mAshmemSize) {
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' publisher ~ Cannot append motion sample because the shared memory "
"buffer is full. Buffer size: %d bytes, pointers: %d, samples: %d",
mChannel->getName().string(),
mAshmemSize, mMotionEventPointerCount, mSharedMessage->motion.sampleCount);
#endif
return NO_MEMORY;
}
int result;
if (mWasDispatched) {
result = sem_trywait(& mSharedMessage->semaphore);
if (result < 0) {
if (errno == EAGAIN) {
// Only possible source of contention is the consumer having consumed (or being in the
// process of consuming) the message and left the semaphore count at 0.
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' publisher ~ Cannot append motion sample because the message has "
"already been consumed.", mChannel->getName().string());
#endif
return FAILED_TRANSACTION;
} else {
LOGE("channel '%s' publisher ~ Error %d in sem_trywait.",
mChannel->getName().string(), errno);
return UNKNOWN_ERROR;
}
}
}
mMotionEventSampleDataTail->eventTime = eventTime;
for (size_t i = 0; i < mMotionEventPointerCount; i++) {
mMotionEventSampleDataTail->coords[i].copyFrom(pointerCoords[i]);
}
mMotionEventSampleDataTail = newTail;
mSharedMessage->motion.sampleCount += 1;
if (mWasDispatched) {
result = sem_post(& mSharedMessage->semaphore);
if (result < 0) {
LOGE("channel '%s' publisher ~ Error %d in sem_post.",
mChannel->getName().string(), errno);
return UNKNOWN_ERROR;
}
}
return OK;
}
status_t InputPublisher::sendDispatchSignal() {
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' publisher ~ sendDispatchSignal",
mChannel->getName().string());
#endif
mWasDispatched = true;
return mChannel->sendSignal(INPUT_SIGNAL_DISPATCH);
}
status_t InputPublisher::receiveFinishedSignal(bool* outHandled) {
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' publisher ~ receiveFinishedSignal",
mChannel->getName().string());
#endif
char signal;
status_t result = mChannel->receiveSignal(& signal);
if (result) {
*outHandled = false;
return result;
}
if (signal == INPUT_SIGNAL_FINISHED_HANDLED) {
*outHandled = true;
} else if (signal == INPUT_SIGNAL_FINISHED_UNHANDLED) {
*outHandled = false;
} else {
LOGE("channel '%s' publisher ~ Received unexpected signal '%c' from consumer",
mChannel->getName().string(), signal);
return UNKNOWN_ERROR;
}
return OK;
}
// --- InputConsumer ---
InputConsumer::InputConsumer(const sp<InputChannel>& channel) :
mChannel(channel), mSharedMessage(NULL) {
}
InputConsumer::~InputConsumer() {
if (mSharedMessage) {
munmap(mSharedMessage, mAshmemSize);
}
}
status_t InputConsumer::initialize() {
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' consumer ~ initialize",
mChannel->getName().string());
#endif
int ashmemFd = mChannel->getAshmemFd();
int result = ashmem_get_size_region(ashmemFd);
if (result < 0) {
LOGE("channel '%s' consumer ~ Error %d getting size of ashmem fd %d.",
mChannel->getName().string(), result, ashmemFd);
return UNKNOWN_ERROR;
}
mAshmemSize = (size_t) result;
mSharedMessage = static_cast<InputMessage*>(mmap(NULL, mAshmemSize,
PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0));
if (! mSharedMessage) {
LOGE("channel '%s' consumer ~ mmap failed on ashmem fd %d.",
mChannel->getName().string(), ashmemFd);
return NO_MEMORY;
}
return OK;
}
status_t InputConsumer::consume(InputEventFactoryInterface* factory, InputEvent** outEvent) {
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' consumer ~ consume",
mChannel->getName().string());
#endif
*outEvent = NULL;
int ashmemFd = mChannel->getAshmemFd();
int result = ashmem_pin_region(ashmemFd, 0, 0);
if (result != ASHMEM_NOT_PURGED) {
if (result == ASHMEM_WAS_PURGED) {
LOGE("channel '%s' consumer ~ Error %d pinning ashmem fd %d because it was purged "
"which probably indicates that the publisher and consumer are out of sync.",
mChannel->getName().string(), result, ashmemFd);
return INVALID_OPERATION;
}
LOGE("channel '%s' consumer ~ Error %d pinning ashmem fd %d.",
mChannel->getName().string(), result, ashmemFd);
return UNKNOWN_ERROR;
}
if (mSharedMessage->consumed) {
LOGE("channel '%s' consumer ~ The current message has already been consumed.",
mChannel->getName().string());
return INVALID_OPERATION;
}
// Acquire but *never release* the semaphore. Contention on the semaphore is used to signal
// to the publisher that the message has been consumed (or is in the process of being
// consumed). Eventually the publisher will reinitialize the semaphore for the next message.
result = sem_wait(& mSharedMessage->semaphore);
if (result < 0) {
LOGE("channel '%s' consumer ~ Error %d in sem_wait.",
mChannel->getName().string(), errno);
return UNKNOWN_ERROR;
}
mSharedMessage->consumed = true;
switch (mSharedMessage->type) {
case AINPUT_EVENT_TYPE_KEY: {
KeyEvent* keyEvent = factory->createKeyEvent();
if (! keyEvent) return NO_MEMORY;
populateKeyEvent(keyEvent);
*outEvent = keyEvent;
break;
}
case AINPUT_EVENT_TYPE_MOTION: {
MotionEvent* motionEvent = factory->createMotionEvent();
if (! motionEvent) return NO_MEMORY;
populateMotionEvent(motionEvent);
*outEvent = motionEvent;
break;
}
default:
LOGE("channel '%s' consumer ~ Received message of unknown type %d",
mChannel->getName().string(), mSharedMessage->type);
return UNKNOWN_ERROR;
}
return OK;
}
status_t InputConsumer::sendFinishedSignal(bool handled) {
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' consumer ~ sendFinishedSignal: handled=%d",
mChannel->getName().string(), handled);
#endif
return mChannel->sendSignal(handled
? INPUT_SIGNAL_FINISHED_HANDLED
: INPUT_SIGNAL_FINISHED_UNHANDLED);
}
status_t InputConsumer::receiveDispatchSignal() {
#if DEBUG_TRANSPORT_ACTIONS
LOGD("channel '%s' consumer ~ receiveDispatchSignal",
mChannel->getName().string());
#endif
char signal;
status_t result = mChannel->receiveSignal(& signal);
if (result) {
return result;
}
if (signal != INPUT_SIGNAL_DISPATCH) {
LOGE("channel '%s' consumer ~ Received unexpected signal '%c' from publisher",
mChannel->getName().string(), signal);
return UNKNOWN_ERROR;
}
return OK;
}
void InputConsumer::populateKeyEvent(KeyEvent* keyEvent) const {
keyEvent->initialize(
mSharedMessage->deviceId,
mSharedMessage->source,
mSharedMessage->key.action,
mSharedMessage->key.flags,
mSharedMessage->key.keyCode,
mSharedMessage->key.scanCode,
mSharedMessage->key.metaState,
mSharedMessage->key.repeatCount,
mSharedMessage->key.downTime,
mSharedMessage->key.eventTime);
}
void InputConsumer::populateMotionEvent(MotionEvent* motionEvent) const {
motionEvent->initialize(
mSharedMessage->deviceId,
mSharedMessage->source,
mSharedMessage->motion.action,
mSharedMessage->motion.flags,
mSharedMessage->motion.edgeFlags,
mSharedMessage->motion.metaState,
mSharedMessage->motion.xOffset,
mSharedMessage->motion.yOffset,
mSharedMessage->motion.xPrecision,
mSharedMessage->motion.yPrecision,
mSharedMessage->motion.downTime,
mSharedMessage->motion.sampleData[0].eventTime,
mSharedMessage->motion.pointerCount,
mSharedMessage->motion.pointerIds,
mSharedMessage->motion.sampleData[0].coords);
size_t sampleCount = mSharedMessage->motion.sampleCount;
if (sampleCount > 1) {
InputMessage::SampleData* sampleData = mSharedMessage->motion.sampleData;
size_t sampleDataStride = InputMessage::sampleDataStride(
mSharedMessage->motion.pointerCount);
while (--sampleCount > 0) {
sampleData = InputMessage::sampleDataPtrIncrement(sampleData, sampleDataStride);
motionEvent->addSample(sampleData->eventTime, sampleData->coords);
}
}
}
} // namespace android