diff options
41 files changed, 4452 insertions, 354 deletions
diff --git a/emulator/gps/Android.mk b/emulator/gps/Android.mk new file mode 100644 index 0000000..41bdc64 --- /dev/null +++ b/emulator/gps/Android.mk @@ -0,0 +1,39 @@ +# Copyright (C) 2010 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. + + +# We're moving the emulator-specific platform libs to +# development.git/tools/emulator/. The following test is to ensure +# smooth builds even if the tree contains both versions. +# +ifndef BUILD_EMULATOR_GPS_MODULE +BUILD_EMULATOR_GPS_MODULE := true + +LOCAL_PATH := $(call my-dir) + +ifneq ($(TARGET_PRODUCT),sim) +# HAL module implemenation, not prelinked and stored in +# hw/<GPS_HARDWARE_MODULE_ID>.<ro.hardware>.so +include $(CLEAR_VARS) +LOCAL_PRELINK_MODULE := false +LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)/hw +LOCAL_CFLAGS += -DQEMU_HARDWARE +LOCAL_SHARED_LIBRARIES := liblog libcutils libhardware +LOCAL_SRC_FILES := gps_qemu.c +LOCAL_MODULE := gps.goldfish +LOCAL_MODULE_TAGS := debug +include $(BUILD_SHARED_LIBRARY) +endif + +endif # BUILD_EMULATOR_GPS_MODULE diff --git a/emulator/gps/gps_qemu.c b/emulator/gps/gps_qemu.c new file mode 100644 index 0000000..a4699d3 --- /dev/null +++ b/emulator/gps/gps_qemu.c @@ -0,0 +1,941 @@ +/* + * Copyright (C) 2010 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. + */ + +/* this implements a GPS hardware library for the Android emulator. + * the following code should be built as a shared library that will be + * placed into /system/lib/hw/gps.goldfish.so + * + * it will be loaded by the code in hardware/libhardware/hardware.c + * which is itself called from android_location_GpsLocationProvider.cpp + */ + + +#include <errno.h> +#include <pthread.h> +#include <fcntl.h> +#include <sys/epoll.h> +#include <math.h> +#include <time.h> + +#define LOG_TAG "gps_qemu" +#include <cutils/log.h> +#include <cutils/sockets.h> +#include <hardware/gps.h> +#include <hardware/qemud.h> + +/* the name of the qemud-controlled socket */ +#define QEMU_CHANNEL_NAME "gps" + +#define GPS_DEBUG 0 + +#if GPS_DEBUG +# define D(...) LOGD(__VA_ARGS__) +#else +# define D(...) ((void)0) +#endif + +/*****************************************************************/ +/*****************************************************************/ +/***** *****/ +/***** N M E A T O K E N I Z E R *****/ +/***** *****/ +/*****************************************************************/ +/*****************************************************************/ + +typedef struct { + const char* p; + const char* end; +} Token; + +#define MAX_NMEA_TOKENS 16 + +typedef struct { + int count; + Token tokens[ MAX_NMEA_TOKENS ]; +} NmeaTokenizer; + +static int +nmea_tokenizer_init( NmeaTokenizer* t, const char* p, const char* end ) +{ + int count = 0; + char* q; + + // the initial '$' is optional + if (p < end && p[0] == '$') + p += 1; + + // remove trailing newline + if (end > p && end[-1] == '\n') { + end -= 1; + if (end > p && end[-1] == '\r') + end -= 1; + } + + // get rid of checksum at the end of the sentecne + if (end >= p+3 && end[-3] == '*') { + end -= 3; + } + + while (p < end) { + const char* q = p; + + q = memchr(p, ',', end-p); + if (q == NULL) + q = end; + + if (q > p) { + if (count < MAX_NMEA_TOKENS) { + t->tokens[count].p = p; + t->tokens[count].end = q; + count += 1; + } + } + if (q < end) + q += 1; + + p = q; + } + + t->count = count; + return count; +} + +static Token +nmea_tokenizer_get( NmeaTokenizer* t, int index ) +{ + Token tok; + static const char* dummy = ""; + + if (index < 0 || index >= t->count) { + tok.p = tok.end = dummy; + } else + tok = t->tokens[index]; + + return tok; +} + + +static int +str2int( const char* p, const char* end ) +{ + int result = 0; + int len = end - p; + + for ( ; len > 0; len--, p++ ) + { + int c; + + if (p >= end) + goto Fail; + + c = *p - '0'; + if ((unsigned)c >= 10) + goto Fail; + + result = result*10 + c; + } + return result; + +Fail: + return -1; +} + +static double +str2float( const char* p, const char* end ) +{ + int result = 0; + int len = end - p; + char temp[16]; + + if (len >= (int)sizeof(temp)) + return 0.; + + memcpy( temp, p, len ); + temp[len] = 0; + return strtod( temp, NULL ); +} + +/*****************************************************************/ +/*****************************************************************/ +/***** *****/ +/***** N M E A P A R S E R *****/ +/***** *****/ +/*****************************************************************/ +/*****************************************************************/ + +#define NMEA_MAX_SIZE 83 + +typedef struct { + int pos; + int overflow; + int utc_year; + int utc_mon; + int utc_day; + int utc_diff; + GpsLocation fix; + gps_location_callback callback; + char in[ NMEA_MAX_SIZE+1 ]; +} NmeaReader; + + +static void +nmea_reader_update_utc_diff( NmeaReader* r ) +{ + time_t now = time(NULL); + struct tm tm_local; + struct tm tm_utc; + long time_local, time_utc; + + gmtime_r( &now, &tm_utc ); + localtime_r( &now, &tm_local ); + + time_local = tm_local.tm_sec + + 60*(tm_local.tm_min + + 60*(tm_local.tm_hour + + 24*(tm_local.tm_yday + + 365*tm_local.tm_year))); + + time_utc = tm_utc.tm_sec + + 60*(tm_utc.tm_min + + 60*(tm_utc.tm_hour + + 24*(tm_utc.tm_yday + + 365*tm_utc.tm_year))); + + r->utc_diff = time_utc - time_local; +} + + +static void +nmea_reader_init( NmeaReader* r ) +{ + memset( r, 0, sizeof(*r) ); + + r->pos = 0; + r->overflow = 0; + r->utc_year = -1; + r->utc_mon = -1; + r->utc_day = -1; + r->callback = NULL; + r->fix.size = sizeof(r->fix); + + nmea_reader_update_utc_diff( r ); +} + + +static void +nmea_reader_set_callback( NmeaReader* r, gps_location_callback cb ) +{ + r->callback = cb; + if (cb != NULL && r->fix.flags != 0) { + D("%s: sending latest fix to new callback", __FUNCTION__); + r->callback( &r->fix ); + r->fix.flags = 0; + } +} + + +static int +nmea_reader_update_time( NmeaReader* r, Token tok ) +{ + int hour, minute; + double seconds; + struct tm tm; + time_t fix_time; + + if (tok.p + 6 > tok.end) + return -1; + + if (r->utc_year < 0) { + // no date yet, get current one + time_t now = time(NULL); + gmtime_r( &now, &tm ); + r->utc_year = tm.tm_year + 1900; + r->utc_mon = tm.tm_mon + 1; + r->utc_day = tm.tm_mday; + } + + hour = str2int(tok.p, tok.p+2); + minute = str2int(tok.p+2, tok.p+4); + seconds = str2float(tok.p+4, tok.end); + + tm.tm_hour = hour; + tm.tm_min = minute; + tm.tm_sec = (int) seconds; + tm.tm_year = r->utc_year - 1900; + tm.tm_mon = r->utc_mon - 1; + tm.tm_mday = r->utc_day; + tm.tm_isdst = -1; + + fix_time = mktime( &tm ) + r->utc_diff; + r->fix.timestamp = (long long)fix_time * 1000; + return 0; +} + +static int +nmea_reader_update_date( NmeaReader* r, Token date, Token time ) +{ + Token tok = date; + int day, mon, year; + + if (tok.p + 6 != tok.end) { + D("date not properly formatted: '%.*s'", tok.end-tok.p, tok.p); + return -1; + } + day = str2int(tok.p, tok.p+2); + mon = str2int(tok.p+2, tok.p+4); + year = str2int(tok.p+4, tok.p+6) + 2000; + + if ((day|mon|year) < 0) { + D("date not properly formatted: '%.*s'", tok.end-tok.p, tok.p); + return -1; + } + + r->utc_year = year; + r->utc_mon = mon; + r->utc_day = day; + + return nmea_reader_update_time( r, time ); +} + + +static double +convert_from_hhmm( Token tok ) +{ + double val = str2float(tok.p, tok.end); + int degrees = (int)(floor(val) / 100); + double minutes = val - degrees*100.; + double dcoord = degrees + minutes / 60.0; + return dcoord; +} + + +static int +nmea_reader_update_latlong( NmeaReader* r, + Token latitude, + char latitudeHemi, + Token longitude, + char longitudeHemi ) +{ + double lat, lon; + Token tok; + + tok = latitude; + if (tok.p + 6 > tok.end) { + D("latitude is too short: '%.*s'", tok.end-tok.p, tok.p); + return -1; + } + lat = convert_from_hhmm(tok); + if (latitudeHemi == 'S') + lat = -lat; + + tok = longitude; + if (tok.p + 6 > tok.end) { + D("longitude is too short: '%.*s'", tok.end-tok.p, tok.p); + return -1; + } + lon = convert_from_hhmm(tok); + if (longitudeHemi == 'W') + lon = -lon; + + r->fix.flags |= GPS_LOCATION_HAS_LAT_LONG; + r->fix.latitude = lat; + r->fix.longitude = lon; + return 0; +} + + +static int +nmea_reader_update_altitude( NmeaReader* r, + Token altitude, + Token units ) +{ + double alt; + Token tok = altitude; + + if (tok.p >= tok.end) + return -1; + + r->fix.flags |= GPS_LOCATION_HAS_ALTITUDE; + r->fix.altitude = str2float(tok.p, tok.end); + return 0; +} + + +static int +nmea_reader_update_bearing( NmeaReader* r, + Token bearing ) +{ + double alt; + Token tok = bearing; + + if (tok.p >= tok.end) + return -1; + + r->fix.flags |= GPS_LOCATION_HAS_BEARING; + r->fix.bearing = str2float(tok.p, tok.end); + return 0; +} + + +static int +nmea_reader_update_speed( NmeaReader* r, + Token speed ) +{ + double alt; + Token tok = speed; + + if (tok.p >= tok.end) + return -1; + + r->fix.flags |= GPS_LOCATION_HAS_SPEED; + r->fix.speed = str2float(tok.p, tok.end); + return 0; +} + + +static void +nmea_reader_parse( NmeaReader* r ) +{ + /* we received a complete sentence, now parse it to generate + * a new GPS fix... + */ + NmeaTokenizer tzer[1]; + Token tok; + + D("Received: '%.*s'", r->pos, r->in); + if (r->pos < 9) { + D("Too short. discarded."); + return; + } + + nmea_tokenizer_init(tzer, r->in, r->in + r->pos); +#if GPS_DEBUG + { + int n; + D("Found %d tokens", tzer->count); + for (n = 0; n < tzer->count; n++) { + Token tok = nmea_tokenizer_get(tzer,n); + D("%2d: '%.*s'", n, tok.end-tok.p, tok.p); + } + } +#endif + + tok = nmea_tokenizer_get(tzer, 0); + if (tok.p + 5 > tok.end) { + D("sentence id '%.*s' too short, ignored.", tok.end-tok.p, tok.p); + return; + } + + // ignore first two characters. + tok.p += 2; + if ( !memcmp(tok.p, "GGA", 3) ) { + // GPS fix + Token tok_time = nmea_tokenizer_get(tzer,1); + Token tok_latitude = nmea_tokenizer_get(tzer,2); + Token tok_latitudeHemi = nmea_tokenizer_get(tzer,3); + Token tok_longitude = nmea_tokenizer_get(tzer,4); + Token tok_longitudeHemi = nmea_tokenizer_get(tzer,5); + Token tok_altitude = nmea_tokenizer_get(tzer,9); + Token tok_altitudeUnits = nmea_tokenizer_get(tzer,10); + + nmea_reader_update_time(r, tok_time); + nmea_reader_update_latlong(r, tok_latitude, + tok_latitudeHemi.p[0], + tok_longitude, + tok_longitudeHemi.p[0]); + nmea_reader_update_altitude(r, tok_altitude, tok_altitudeUnits); + + } else if ( !memcmp(tok.p, "GSA", 3) ) { + // do something ? + } else if ( !memcmp(tok.p, "RMC", 3) ) { + Token tok_time = nmea_tokenizer_get(tzer,1); + Token tok_fixStatus = nmea_tokenizer_get(tzer,2); + Token tok_latitude = nmea_tokenizer_get(tzer,3); + Token tok_latitudeHemi = nmea_tokenizer_get(tzer,4); + Token tok_longitude = nmea_tokenizer_get(tzer,5); + Token tok_longitudeHemi = nmea_tokenizer_get(tzer,6); + Token tok_speed = nmea_tokenizer_get(tzer,7); + Token tok_bearing = nmea_tokenizer_get(tzer,8); + Token tok_date = nmea_tokenizer_get(tzer,9); + + D("in RMC, fixStatus=%c", tok_fixStatus.p[0]); + if (tok_fixStatus.p[0] == 'A') + { + nmea_reader_update_date( r, tok_date, tok_time ); + + nmea_reader_update_latlong( r, tok_latitude, + tok_latitudeHemi.p[0], + tok_longitude, + tok_longitudeHemi.p[0] ); + + nmea_reader_update_bearing( r, tok_bearing ); + nmea_reader_update_speed ( r, tok_speed ); + } + } else { + tok.p -= 2; + D("unknown sentence '%.*s", tok.end-tok.p, tok.p); + } + if (r->fix.flags != 0) { +#if GPS_DEBUG + char temp[256]; + char* p = temp; + char* end = p + sizeof(temp); + struct tm utc; + + p += snprintf( p, end-p, "sending fix" ); + if (r->fix.flags & GPS_LOCATION_HAS_LAT_LONG) { + p += snprintf(p, end-p, " lat=%g lon=%g", r->fix.latitude, r->fix.longitude); + } + if (r->fix.flags & GPS_LOCATION_HAS_ALTITUDE) { + p += snprintf(p, end-p, " altitude=%g", r->fix.altitude); + } + if (r->fix.flags & GPS_LOCATION_HAS_SPEED) { + p += snprintf(p, end-p, " speed=%g", r->fix.speed); + } + if (r->fix.flags & GPS_LOCATION_HAS_BEARING) { + p += snprintf(p, end-p, " bearing=%g", r->fix.bearing); + } + if (r->fix.flags & GPS_LOCATION_HAS_ACCURACY) { + p += snprintf(p,end-p, " accuracy=%g", r->fix.accuracy); + } + gmtime_r( (time_t*) &r->fix.timestamp, &utc ); + p += snprintf(p, end-p, " time=%s", asctime( &utc ) ); + D(temp); +#endif + if (r->callback) { + r->callback( &r->fix ); + r->fix.flags = 0; + } + else { + D("no callback, keeping data until needed !"); + } + } +} + + +static void +nmea_reader_addc( NmeaReader* r, int c ) +{ + if (r->overflow) { + r->overflow = (c != '\n'); + return; + } + + if (r->pos >= (int) sizeof(r->in)-1 ) { + r->overflow = 1; + r->pos = 0; + return; + } + + r->in[r->pos] = (char)c; + r->pos += 1; + + if (c == '\n') { + nmea_reader_parse( r ); + r->pos = 0; + } +} + + +/*****************************************************************/ +/*****************************************************************/ +/***** *****/ +/***** C O N N E C T I O N S T A T E *****/ +/***** *****/ +/*****************************************************************/ +/*****************************************************************/ + +/* commands sent to the gps thread */ +enum { + CMD_QUIT = 0, + CMD_START = 1, + CMD_STOP = 2 +}; + + +/* this is the state of our connection to the qemu_gpsd daemon */ +typedef struct { + int init; + int fd; + GpsCallbacks callbacks; + pthread_t thread; + int control[2]; +} GpsState; + +static GpsState _gps_state[1]; + + +static void +gps_state_done( GpsState* s ) +{ + // tell the thread to quit, and wait for it + char cmd = CMD_QUIT; + void* dummy; + write( s->control[0], &cmd, 1 ); + pthread_join(s->thread, &dummy); + + // close the control socket pair + close( s->control[0] ); s->control[0] = -1; + close( s->control[1] ); s->control[1] = -1; + + // close connection to the QEMU GPS daemon + close( s->fd ); s->fd = -1; + s->init = 0; +} + + +static void +gps_state_start( GpsState* s ) +{ + char cmd = CMD_START; + int ret; + + do { ret=write( s->control[0], &cmd, 1 ); } + while (ret < 0 && errno == EINTR); + + if (ret != 1) + D("%s: could not send CMD_START command: ret=%d: %s", + __FUNCTION__, ret, strerror(errno)); +} + + +static void +gps_state_stop( GpsState* s ) +{ + char cmd = CMD_STOP; + int ret; + + do { ret=write( s->control[0], &cmd, 1 ); } + while (ret < 0 && errno == EINTR); + + if (ret != 1) + D("%s: could not send CMD_STOP command: ret=%d: %s", + __FUNCTION__, ret, strerror(errno)); +} + + +static int +epoll_register( int epoll_fd, int fd ) +{ + struct epoll_event ev; + int ret, flags; + + /* important: make the fd non-blocking */ + flags = fcntl(fd, F_GETFL); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + + ev.events = EPOLLIN; + ev.data.fd = fd; + do { + ret = epoll_ctl( epoll_fd, EPOLL_CTL_ADD, fd, &ev ); + } while (ret < 0 && errno == EINTR); + return ret; +} + + +static int +epoll_deregister( int epoll_fd, int fd ) +{ + int ret; + do { + ret = epoll_ctl( epoll_fd, EPOLL_CTL_DEL, fd, NULL ); + } while (ret < 0 && errno == EINTR); + return ret; +} + +/* this is the main thread, it waits for commands from gps_state_start/stop and, + * when started, messages from the QEMU GPS daemon. these are simple NMEA sentences + * that must be parsed to be converted into GPS fixes sent to the framework + */ +static void +gps_state_thread( void* arg ) +{ + GpsState* state = (GpsState*) arg; + NmeaReader reader[1]; + int epoll_fd = epoll_create(2); + int started = 0; + int gps_fd = state->fd; + int control_fd = state->control[1]; + + nmea_reader_init( reader ); + + // register control file descriptors for polling + epoll_register( epoll_fd, control_fd ); + epoll_register( epoll_fd, gps_fd ); + + D("gps thread running"); + + // now loop + for (;;) { + struct epoll_event events[2]; + int ne, nevents; + + nevents = epoll_wait( epoll_fd, events, 2, -1 ); + if (nevents < 0) { + if (errno != EINTR) + LOGE("epoll_wait() unexpected error: %s", strerror(errno)); + continue; + } + D("gps thread received %d events", nevents); + for (ne = 0; ne < nevents; ne++) { + if ((events[ne].events & (EPOLLERR|EPOLLHUP)) != 0) { + LOGE("EPOLLERR or EPOLLHUP after epoll_wait() !?"); + return; + } + if ((events[ne].events & EPOLLIN) != 0) { + int fd = events[ne].data.fd; + + if (fd == control_fd) + { + char cmd = 255; + int ret; + D("gps control fd event"); + do { + ret = read( fd, &cmd, 1 ); + } while (ret < 0 && errno == EINTR); + + if (cmd == CMD_QUIT) { + D("gps thread quitting on demand"); + return; + } + else if (cmd == CMD_START) { + if (!started) { + D("gps thread starting location_cb=%p", state->callbacks.location_cb); + started = 1; + nmea_reader_set_callback( reader, state->callbacks.location_cb ); + } + } + else if (cmd == CMD_STOP) { + if (started) { + D("gps thread stopping"); + started = 0; + nmea_reader_set_callback( reader, NULL ); + } + } + } + else if (fd == gps_fd) + { + char buff[32]; + D("gps fd event"); + for (;;) { + int nn, ret; + + ret = read( fd, buff, sizeof(buff) ); + if (ret < 0) { + if (errno == EINTR) + continue; + if (errno != EWOULDBLOCK) + LOGE("error while reading from gps daemon socket: %s:", strerror(errno)); + break; + } + D("received %d bytes: %.*s", ret, ret, buff); + for (nn = 0; nn < ret; nn++) + nmea_reader_addc( reader, buff[nn] ); + } + D("gps fd event end"); + } + else + { + LOGE("epoll_wait() returned unkown fd %d ?", fd); + } + } + } + } +} + + +static void +gps_state_init( GpsState* state, GpsCallbacks* callbacks ) +{ + state->init = 1; + state->control[0] = -1; + state->control[1] = -1; + state->fd = -1; + + state->fd = qemud_channel_open(QEMU_CHANNEL_NAME); + + if (state->fd < 0) { + D("no gps emulation detected"); + return; + } + + D("gps emulation will read from '%s' qemud channel", QEMU_CHANNEL_NAME ); + + if ( socketpair( AF_LOCAL, SOCK_STREAM, 0, state->control ) < 0 ) { + LOGE("could not create thread control socket pair: %s", strerror(errno)); + goto Fail; + } + + state->thread = callbacks->create_thread_cb( "gps_state_thread", gps_state_thread, state ); + + if ( !state->thread ) { + LOGE("could not create gps thread: %s", strerror(errno)); + goto Fail; + } + + state->callbacks = *callbacks; + + D("gps state initialized"); + return; + +Fail: + gps_state_done( state ); +} + + +/*****************************************************************/ +/*****************************************************************/ +/***** *****/ +/***** I N T E R F A C E *****/ +/***** *****/ +/*****************************************************************/ +/*****************************************************************/ + + +static int +qemu_gps_init(GpsCallbacks* callbacks) +{ + GpsState* s = _gps_state; + + if (!s->init) + gps_state_init(s, callbacks); + + if (s->fd < 0) + return -1; + + return 0; +} + +static void +qemu_gps_cleanup(void) +{ + GpsState* s = _gps_state; + + if (s->init) + gps_state_done(s); +} + + +static int +qemu_gps_start() +{ + GpsState* s = _gps_state; + + if (!s->init) { + D("%s: called with uninitialized state !!", __FUNCTION__); + return -1; + } + + D("%s: called", __FUNCTION__); + gps_state_start(s); + return 0; +} + + +static int +qemu_gps_stop() +{ + GpsState* s = _gps_state; + + if (!s->init) { + D("%s: called with uninitialized state !!", __FUNCTION__); + return -1; + } + + D("%s: called", __FUNCTION__); + gps_state_stop(s); + return 0; +} + + +static int +qemu_gps_inject_time(GpsUtcTime time, int64_t timeReference, int uncertainty) +{ + return 0; +} + +static int +qemu_gps_inject_location(double latitude, double longitude, float accuracy) +{ + return 0; +} + +static void +qemu_gps_delete_aiding_data(GpsAidingData flags) +{ +} + +static int qemu_gps_set_position_mode(GpsPositionMode mode, int fix_frequency) +{ + // FIXME - support fix_frequency + return 0; +} + +static const void* +qemu_gps_get_extension(const char* name) +{ + // no extensions supported + return NULL; +} + +static const GpsInterface qemuGpsInterface = { + sizeof(GpsInterface), + qemu_gps_init, + qemu_gps_start, + qemu_gps_stop, + qemu_gps_cleanup, + qemu_gps_inject_time, + qemu_gps_inject_location, + qemu_gps_delete_aiding_data, + qemu_gps_set_position_mode, + qemu_gps_get_extension, +}; + +const GpsInterface* gps__get_gps_interface(struct gps_device_t* dev) +{ + return &qemuGpsInterface; +} + +static int open_gps(const struct hw_module_t* module, char const* name, + struct hw_device_t** device) +{ + struct gps_device_t *dev = malloc(sizeof(struct gps_device_t)); + memset(dev, 0, sizeof(*dev)); + + dev->common.tag = HARDWARE_DEVICE_TAG; + dev->common.version = 0; + dev->common.module = (struct hw_module_t*)module; +// dev->common.close = (int (*)(struct hw_device_t*))close_lights; + dev->get_gps_interface = gps__get_gps_interface; + + *device = (struct hw_device_t*)dev; + return 0; +} + + +static struct hw_module_methods_t gps_module_methods = { + .open = open_gps +}; + +const struct hw_module_t HAL_MODULE_INFO_SYM = { + .tag = HARDWARE_MODULE_TAG, + .version_major = 1, + .version_minor = 0, + .id = GPS_HARDWARE_MODULE_ID, + .name = "Goldfish GPS Module", + .author = "The Android Open Source Project", + .methods = &gps_module_methods, +}; diff --git a/emulator/qemud/Android.mk b/emulator/qemud/Android.mk new file mode 100644 index 0000000..5666a74 --- /dev/null +++ b/emulator/qemud/Android.mk @@ -0,0 +1,25 @@ +# Copyright 2008 The Android Open Source Project + +# We're moving the emulator-specific platform libs to +# development.git/tools/emulator/. The following test is to ensure +# smooth builds even if the tree contains both versions. +# +ifndef BUILD_EMULATOR_QEMUD +BUILD_EMULATOR_QEMUD := true + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + qemud.c + + +LOCAL_SHARED_LIBRARIES := \ + libcutils \ + +LOCAL_MODULE:= qemud +LOCAL_MODULE_TAGS := debug + +include $(BUILD_EXECUTABLE) + +endif # BUILD_EMULATOR_QEMUD
\ No newline at end of file diff --git a/emulator/qemud/qemud.c b/emulator/qemud/qemud.c new file mode 100644 index 0000000..e1c7b54 --- /dev/null +++ b/emulator/qemud/qemud.c @@ -0,0 +1,1719 @@ +#include <stdint.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <fcntl.h> +#include <errno.h> +#include <string.h> +#include <sys/socket.h> +#include <termios.h> +#include <cutils/sockets.h> + +/* + * the qemud daemon program is only used within Android as a bridge + * between the emulator program and the emulated system. it really works as + * a simple stream multiplexer that works as follows: + * + * - qemud is started by init following instructions in + * /system/etc/init.goldfish.rc (i.e. it is never started on real devices) + * + * - qemud communicates with the emulator program through a single serial + * port, whose name is passed through a kernel boot parameter + * (e.g. android.qemud=ttyS1) + * + * - qemud binds one unix local stream socket (/dev/socket/qemud, created + * by init through /system/etc/init.goldfish.rc). + * + * + * emulator <==serial==> qemud <---> /dev/socket/qemud <-+--> client1 + * | + * +--> client2 + * + * - the special channel index 0 is used by the emulator and qemud only. + * other channel numbers correspond to clients. More specifically, + * connection are created like this: + * + * * the client connects to /dev/socket/qemud + * + * * the client sends the service name through the socket, as + * <service-name> + * + * * qemud creates a "Client" object internally, assigns it an + * internal unique channel number > 0, then sends a connection + * initiation request to the emulator (i.e. through channel 0): + * + * connect:<id>:<name> + * + * where <name> is the service name, and <id> is a 2-hexchar + * number corresponding to the channel number. + * + * * in case of success, the emulator responds through channel 0 + * with: + * + * ok:connect:<id> + * + * after this, all messages between the client and the emulator + * are passed in pass-through mode. + * + * * if the emulator refuses the service connection, it will + * send the following through channel 0: + * + * ko:connect:<id>:reason-for-failure + * + * * If the client closes the connection, qemud sends the following + * to the emulator: + * + * disconnect:<id> + * + * The same message is the opposite direction if the emulator + * chooses to close the connection. + * + * * any command sent through channel 0 to the emulator that is + * not properly recognized will be answered by: + * + * ko:unknown command + * + * + * Internally, the daemon maintains a "Client" object for each client + * connection (i.e. accepting socket connection). + */ + +/* name of the single control socket used by the daemon */ +#define CONTROL_SOCKET_NAME "qemud" + +#define DEBUG 1 +#define T_ACTIVE 0 /* set to 1 to dump traffic */ + +#if DEBUG +# define LOG_TAG "qemud" +# include <cutils/log.h> +# define D(...) LOGD(__VA_ARGS__) +#else +# define D(...) ((void)0) +# define T(...) ((void)0) +#endif + +#if T_ACTIVE +# define T(...) D(__VA_ARGS__) +#else +# define T(...) ((void)0) +#endif + +/** UTILITIES + **/ + +static void +fatal( const char* fmt, ... ) +{ + va_list args; + va_start(args, fmt); + fprintf(stderr, "PANIC: "); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n" ); + va_end(args); + exit(1); +} + +static void* +xalloc( size_t sz ) +{ + void* p; + + if (sz == 0) + return NULL; + + p = malloc(sz); + if (p == NULL) + fatal( "not enough memory" ); + + return p; +} + +#define xnew(p) (p) = xalloc(sizeof(*(p))) + +static void* +xalloc0( size_t sz ) +{ + void* p = xalloc(sz); + memset( p, 0, sz ); + return p; +} + +#define xnew0(p) (p) = xalloc0(sizeof(*(p))) + +#define xfree(p) (free((p)), (p) = NULL) + +static void* +xrealloc( void* block, size_t size ) +{ + void* p = realloc( block, size ); + + if (p == NULL && size > 0) + fatal( "not enough memory" ); + + return p; +} + +#define xrenew(p,count) (p) = xrealloc((p),sizeof(*(p))*(count)) + +static int +hex2int( const uint8_t* data, int len ) +{ + int result = 0; + while (len > 0) { + int c = *data++; + unsigned d; + + result <<= 4; + do { + d = (unsigned)(c - '0'); + if (d < 10) + break; + + d = (unsigned)(c - 'a'); + if (d < 6) { + d += 10; + break; + } + + d = (unsigned)(c - 'A'); + if (d < 6) { + d += 10; + break; + } + + return -1; + } + while (0); + + result |= d; + len -= 1; + } + return result; +} + + +static void +int2hex( int value, uint8_t* to, int width ) +{ + int nn = 0; + static const char hexchars[16] = "0123456789abcdef"; + + for ( --width; width >= 0; width--, nn++ ) { + to[nn] = hexchars[(value >> (width*4)) & 15]; + } +} + +static int +fd_read(int fd, void* to, int len) +{ + int ret; + + do { + ret = read(fd, to, len); + } while (ret < 0 && errno == EINTR); + + return ret; +} + +static int +fd_write(int fd, const void* from, int len) +{ + int ret; + + do { + ret = write(fd, from, len); + } while (ret < 0 && errno == EINTR); + + return ret; +} + +static void +fd_setnonblock(int fd) +{ + int ret, flags; + + do { + flags = fcntl(fd, F_GETFD); + } while (flags < 0 && errno == EINTR); + + if (flags < 0) { + fatal( "%s: could not get flags for fd %d: %s", + __FUNCTION__, fd, strerror(errno) ); + } + + do { + ret = fcntl(fd, F_SETFD, flags | O_NONBLOCK); + } while (ret < 0 && errno == EINTR); + + if (ret < 0) { + fatal( "%s: could not set fd %d to non-blocking: %s", + __FUNCTION__, fd, strerror(errno) ); + } +} + + +static int +fd_accept(int fd) +{ + struct sockaddr from; + socklen_t fromlen = sizeof(from); + int ret; + + do { + ret = accept(fd, &from, &fromlen); + } while (ret < 0 && errno == EINTR); + + return ret; +} + +/** FD EVENT LOOP + **/ + +/* A Looper object is used to monitor activity on one or more + * file descriptors (e.g sockets). + * + * - call looper_add() to register a function that will be + * called when events happen on the file descriptor. + * + * - call looper_enable() or looper_disable() to enable/disable + * the set of monitored events for a given file descriptor. + * + * - call looper_del() to unregister a file descriptor. + * this does *not* close the file descriptor. + * + * Note that you can only provide a single function to handle + * all events related to a given file descriptor. + + * You can call looper_enable/_disable/_del within a function + * callback. + */ + +/* the current implementation uses Linux's epoll facility + * the event mask we use are simply combinations of EPOLLIN + * EPOLLOUT, EPOLLHUP and EPOLLERR + */ +#include <sys/epoll.h> + +#define MAX_CHANNELS 16 +#define MAX_EVENTS (MAX_CHANNELS+1) /* each channel + the serial fd */ + +/* the event handler function type, 'user' is a user-specific + * opaque pointer passed to looper_add(). + */ +typedef void (*EventFunc)( void* user, int events ); + +/* bit flags for the LoopHook structure. + * + * HOOK_PENDING means that an event happened on the + * corresponding file descriptor. + * + * HOOK_CLOSING is used to delay-close monitored + * file descriptors. + */ +enum { + HOOK_PENDING = (1 << 0), + HOOK_CLOSING = (1 << 1), +}; + +/* A LoopHook structure is used to monitor a given + * file descriptor and record its event handler. + */ +typedef struct { + int fd; + int wanted; /* events we are monitoring */ + int events; /* events that occured */ + int state; /* see HOOK_XXX constants */ + void* ev_user; /* user-provided handler parameter */ + EventFunc ev_func; /* event handler callback */ +} LoopHook; + +/* Looper is the main object modeling a looper object + */ +typedef struct { + int epoll_fd; + int num_fds; + int max_fds; + struct epoll_event* events; + LoopHook* hooks; +} Looper; + +/* initialize a looper object */ +static void +looper_init( Looper* l ) +{ + l->epoll_fd = epoll_create(4); + l->num_fds = 0; + l->max_fds = 0; + l->events = NULL; + l->hooks = NULL; +} + +/* finalize a looper object */ +static void +looper_done( Looper* l ) +{ + xfree(l->events); + xfree(l->hooks); + l->max_fds = 0; + l->num_fds = 0; + + close(l->epoll_fd); + l->epoll_fd = -1; +} + +/* return the LoopHook corresponding to a given + * monitored file descriptor, or NULL if not found + */ +static LoopHook* +looper_find( Looper* l, int fd ) +{ + LoopHook* hook = l->hooks; + LoopHook* end = hook + l->num_fds; + + for ( ; hook < end; hook++ ) { + if (hook->fd == fd) + return hook; + } + return NULL; +} + +/* grow the arrays in the looper object */ +static void +looper_grow( Looper* l ) +{ + int old_max = l->max_fds; + int new_max = old_max + (old_max >> 1) + 4; + int n; + + xrenew( l->events, new_max ); + xrenew( l->hooks, new_max ); + l->max_fds = new_max; + + /* now change the handles to all events */ + for (n = 0; n < l->num_fds; n++) { + struct epoll_event ev; + LoopHook* hook = l->hooks + n; + + ev.events = hook->wanted; + ev.data.ptr = hook; + epoll_ctl( l->epoll_fd, EPOLL_CTL_MOD, hook->fd, &ev ); + } +} + +/* register a file descriptor and its event handler. + * no event mask will be enabled + */ +static void +looper_add( Looper* l, int fd, EventFunc func, void* user ) +{ + struct epoll_event ev; + LoopHook* hook; + + if (l->num_fds >= l->max_fds) + looper_grow(l); + + hook = l->hooks + l->num_fds; + + hook->fd = fd; + hook->ev_user = user; + hook->ev_func = func; + hook->state = 0; + hook->wanted = 0; + hook->events = 0; + + fd_setnonblock(fd); + + ev.events = 0; + ev.data.ptr = hook; + epoll_ctl( l->epoll_fd, EPOLL_CTL_ADD, fd, &ev ); + + l->num_fds += 1; +} + +/* unregister a file descriptor and its event handler + */ +static void +looper_del( Looper* l, int fd ) +{ + LoopHook* hook = looper_find( l, fd ); + + if (!hook) { + D( "%s: invalid fd: %d", __FUNCTION__, fd ); + return; + } + /* don't remove the hook yet */ + hook->state |= HOOK_CLOSING; + + epoll_ctl( l->epoll_fd, EPOLL_CTL_DEL, fd, NULL ); +} + +/* enable monitoring of certain events for a file + * descriptor. This adds 'events' to the current + * event mask + */ +static void +looper_enable( Looper* l, int fd, int events ) +{ + LoopHook* hook = looper_find( l, fd ); + + if (!hook) { + D("%s: invalid fd: %d", __FUNCTION__, fd ); + return; + } + + if (events & ~hook->wanted) { + struct epoll_event ev; + + hook->wanted |= events; + ev.events = hook->wanted; + ev.data.ptr = hook; + + epoll_ctl( l->epoll_fd, EPOLL_CTL_MOD, fd, &ev ); + } +} + +/* disable monitoring of certain events for a file + * descriptor. This ignores events that are not + * currently enabled. + */ +static void +looper_disable( Looper* l, int fd, int events ) +{ + LoopHook* hook = looper_find( l, fd ); + + if (!hook) { + D("%s: invalid fd: %d", __FUNCTION__, fd ); + return; + } + + if (events & hook->wanted) { + struct epoll_event ev; + + hook->wanted &= ~events; + ev.events = hook->wanted; + ev.data.ptr = hook; + + epoll_ctl( l->epoll_fd, EPOLL_CTL_MOD, fd, &ev ); + } +} + +/* wait until an event occurs on one of the registered file + * descriptors. Only returns in case of error !! + */ +static void +looper_loop( Looper* l ) +{ + for (;;) { + int n, count; + + do { + count = epoll_wait( l->epoll_fd, l->events, l->num_fds, -1 ); + } while (count < 0 && errno == EINTR); + + if (count < 0) { + D("%s: error: %s", __FUNCTION__, strerror(errno) ); + return; + } + + if (count == 0) { + D("%s: huh ? epoll returned count=0", __FUNCTION__); + continue; + } + + /* mark all pending hooks */ + for (n = 0; n < count; n++) { + LoopHook* hook = l->events[n].data.ptr; + hook->state = HOOK_PENDING; + hook->events = l->events[n].events; + } + + /* execute hook callbacks. this may change the 'hooks' + * and 'events' array, as well as l->num_fds, so be careful */ + for (n = 0; n < l->num_fds; n++) { + LoopHook* hook = l->hooks + n; + if (hook->state & HOOK_PENDING) { + hook->state &= ~HOOK_PENDING; + hook->ev_func( hook->ev_user, hook->events ); + } + } + + /* now remove all the hooks that were closed by + * the callbacks */ + for (n = 0; n < l->num_fds;) { + struct epoll_event ev; + LoopHook* hook = l->hooks + n; + + if (!(hook->state & HOOK_CLOSING)) { + n++; + continue; + } + + hook[0] = l->hooks[l->num_fds-1]; + l->num_fds -= 1; + ev.events = hook->wanted; + ev.data.ptr = hook; + epoll_ctl( l->epoll_fd, EPOLL_CTL_MOD, hook->fd, &ev ); + } + } +} + +#if T_ACTIVE +char* +quote( const void* data, int len ) +{ + const char* p = data; + const char* end = p + len; + int count = 0; + int phase = 0; + static char* buff = NULL; + + for (phase = 0; phase < 2; phase++) { + if (phase != 0) { + xfree(buff); + buff = xalloc(count+1); + } + count = 0; + for (p = data; p < end; p++) { + int c = *p; + + if (c == '\\') { + if (phase != 0) { + buff[count] = buff[count+1] = '\\'; + } + count += 2; + continue; + } + + if (c >= 32 && c < 127) { + if (phase != 0) + buff[count] = c; + count += 1; + continue; + } + + + if (c == '\t') { + if (phase != 0) { + memcpy(buff+count, "<TAB>", 5); + } + count += 5; + continue; + } + if (c == '\n') { + if (phase != 0) { + memcpy(buff+count, "<LN>", 4); + } + count += 4; + continue; + } + if (c == '\r') { + if (phase != 0) { + memcpy(buff+count, "<CR>", 4); + } + count += 4; + continue; + } + + if (phase != 0) { + buff[count+0] = '\\'; + buff[count+1] = 'x'; + buff[count+2] = "0123456789abcdef"[(c >> 4) & 15]; + buff[count+3] = "0123456789abcdef"[ (c) & 15]; + } + count += 4; + } + } + buff[count] = 0; + return buff; +} +#endif /* T_ACTIVE */ + +/** PACKETS + ** + ** We need a way to buffer data before it can be sent to the + ** corresponding file descriptor. We use linked list of Packet + ** objects to do this. + **/ + +typedef struct Packet Packet; + +#define MAX_PAYLOAD 4000 + +struct Packet { + Packet* next; + int len; + int channel; + uint8_t data[ MAX_PAYLOAD ]; +}; + +/* we expect to alloc/free a lot of packets during + * operations so use a single linked list of free packets + * to keep things speedy and simple. + */ +static Packet* _free_packets; + +/* Allocate a packet */ +static Packet* +packet_alloc(void) +{ + Packet* p = _free_packets; + if (p != NULL) { + _free_packets = p->next; + } else { + xnew(p); + } + p->next = NULL; + p->len = 0; + p->channel = -1; + return p; +} + +/* Release a packet. This takes the address of a packet + * pointer that will be set to NULL on exit (avoids + * referencing dangling pointers in case of bugs) + */ +static void +packet_free( Packet* *ppacket ) +{ + Packet* p = *ppacket; + if (p) { + p->next = _free_packets; + _free_packets = p; + *ppacket = NULL; + } +} + +/** PACKET RECEIVER + ** + ** Simple abstraction for something that can receive a packet + ** from a FDHandler (see below) or something else. + ** + ** Send a packet to it with 'receiver_post' + ** + ** Call 'receiver_close' to indicate that the corresponding + ** packet source was closed. + **/ + +typedef void (*PostFunc) ( void* user, Packet* p ); +typedef void (*CloseFunc)( void* user ); + +typedef struct { + PostFunc post; + CloseFunc close; + void* user; +} Receiver; + +/* post a packet to a receiver. Note that this transfers + * ownership of the packet to the receiver. + */ +static __inline__ void +receiver_post( Receiver* r, Packet* p ) +{ + if (r->post) + r->post( r->user, p ); + else + packet_free(&p); +} + +/* tell a receiver the packet source was closed. + * this will also prevent further posting to the + * receiver. + */ +static __inline__ void +receiver_close( Receiver* r ) +{ + if (r->close) { + r->close( r->user ); + r->close = NULL; + } + r->post = NULL; +} + + +/** FD HANDLERS + ** + ** these are smart listeners that send incoming packets to a receiver + ** and can queue one or more outgoing packets and send them when + ** possible to the FD. + ** + ** note that we support clean shutdown of file descriptors, + ** i.e. we try to send all outgoing packets before destroying + ** the FDHandler. + **/ + +typedef struct FDHandler FDHandler; +typedef struct FDHandlerList FDHandlerList; + +struct FDHandler { + int fd; + FDHandlerList* list; + char closing; + Receiver receiver[1]; + + /* queue of outgoing packets */ + int out_pos; + Packet* out_first; + Packet** out_ptail; + + FDHandler* next; + FDHandler** pref; + +}; + +struct FDHandlerList { + /* the looper that manages the fds */ + Looper* looper; + + /* list of active FDHandler objects */ + FDHandler* active; + + /* list of closing FDHandler objects. + * these are waiting to push their + * queued packets to the fd before + * freeing themselves. + */ + FDHandler* closing; + +}; + +/* remove a FDHandler from its current list */ +static void +fdhandler_remove( FDHandler* f ) +{ + f->pref[0] = f->next; + if (f->next) + f->next->pref = f->pref; +} + +/* add a FDHandler to a given list */ +static void +fdhandler_prepend( FDHandler* f, FDHandler** list ) +{ + f->next = list[0]; + f->pref = list; + list[0] = f; + if (f->next) + f->next->pref = &f->next; +} + +/* initialize a FDHandler list */ +static void +fdhandler_list_init( FDHandlerList* list, Looper* looper ) +{ + list->looper = looper; + list->active = NULL; + list->closing = NULL; +} + + +/* close a FDHandler (and free it). Note that this will not + * perform a graceful shutdown, i.e. all packets in the + * outgoing queue will be immediately free. + * + * this *will* notify the receiver that the file descriptor + * was closed. + * + * you should call fdhandler_shutdown() if you want to + * notify the FDHandler that its packet source is closed. + */ +static void +fdhandler_close( FDHandler* f ) +{ + /* notify receiver */ + receiver_close(f->receiver); + + /* remove the handler from its list */ + fdhandler_remove(f); + + /* get rid of outgoing packet queue */ + if (f->out_first != NULL) { + Packet* p; + while ((p = f->out_first) != NULL) { + f->out_first = p->next; + packet_free(&p); + } + } + + /* get rid of file descriptor */ + if (f->fd >= 0) { + looper_del( f->list->looper, f->fd ); + close(f->fd); + f->fd = -1; + } + + f->list = NULL; + xfree(f); +} + +/* Ask the FDHandler to cleanly shutdown the connection, + * i.e. send any pending outgoing packets then auto-free + * itself. + */ +static void +fdhandler_shutdown( FDHandler* f ) +{ + /* prevent later fdhandler_close() to + * call the receiver's close. + */ + f->receiver->close = NULL; + + if (f->out_first != NULL && !f->closing) + { + /* move the handler to the 'closing' list */ + f->closing = 1; + fdhandler_remove(f); + fdhandler_prepend(f, &f->list->closing); + return; + } + + fdhandler_close(f); +} + +/* Enqueue a new packet that the FDHandler will + * send through its file descriptor. + */ +static void +fdhandler_enqueue( FDHandler* f, Packet* p ) +{ + Packet* first = f->out_first; + + p->next = NULL; + f->out_ptail[0] = p; + f->out_ptail = &p->next; + + if (first == NULL) { + f->out_pos = 0; + looper_enable( f->list->looper, f->fd, EPOLLOUT ); + } +} + + +/* FDHandler file descriptor event callback for read/write ops */ +static void +fdhandler_event( FDHandler* f, int events ) +{ + int len; + + /* in certain cases, it's possible to have both EPOLLIN and + * EPOLLHUP at the same time. This indicates that there is incoming + * data to read, but that the connection was nonetheless closed + * by the sender. Be sure to read the data before closing + * the receiver to avoid packet loss. + */ + + if (events & EPOLLIN) { + Packet* p = packet_alloc(); + int len; + + if ((len = fd_read(f->fd, p->data, MAX_PAYLOAD)) < 0) { + D("%s: can't recv: %s", __FUNCTION__, strerror(errno)); + packet_free(&p); + } else if (len > 0) { + p->len = len; + p->channel = -101; /* special debug value, not used */ + receiver_post( f->receiver, p ); + } + } + + if (events & (EPOLLHUP|EPOLLERR)) { + /* disconnection */ + D("%s: disconnect on fd %d", __FUNCTION__, f->fd); + fdhandler_close(f); + return; + } + + if (events & EPOLLOUT && f->out_first) { + Packet* p = f->out_first; + int avail, len; + + avail = p->len - f->out_pos; + if ((len = fd_write(f->fd, p->data + f->out_pos, avail)) < 0) { + D("%s: can't send: %s", __FUNCTION__, strerror(errno)); + } else { + f->out_pos += len; + if (f->out_pos >= p->len) { + f->out_pos = 0; + f->out_first = p->next; + packet_free(&p); + if (f->out_first == NULL) { + f->out_ptail = &f->out_first; + looper_disable( f->list->looper, f->fd, EPOLLOUT ); + } + } + } + } +} + + +/* Create a new FDHandler that monitors read/writes */ +static FDHandler* +fdhandler_new( int fd, + FDHandlerList* list, + Receiver* receiver ) +{ + FDHandler* f = xalloc0(sizeof(*f)); + + f->fd = fd; + f->list = list; + f->receiver[0] = receiver[0]; + f->out_first = NULL; + f->out_ptail = &f->out_first; + f->out_pos = 0; + + fdhandler_prepend(f, &list->active); + + looper_add( list->looper, fd, (EventFunc) fdhandler_event, f ); + looper_enable( list->looper, fd, EPOLLIN ); + + return f; +} + + +/* event callback function to monitor accepts() on server sockets. + * the convention used here is that the receiver will receive a + * dummy packet with the new client socket in p->channel + */ +static void +fdhandler_accept_event( FDHandler* f, int events ) +{ + if (events & EPOLLIN) { + /* this is an accept - send a dummy packet to the receiver */ + Packet* p = packet_alloc(); + + D("%s: accepting on fd %d", __FUNCTION__, f->fd); + p->data[0] = 1; + p->len = 1; + p->channel = fd_accept(f->fd); + if (p->channel < 0) { + D("%s: accept failed ?: %s", __FUNCTION__, strerror(errno)); + packet_free(&p); + return; + } + receiver_post( f->receiver, p ); + } + + if (events & (EPOLLHUP|EPOLLERR)) { + /* disconnecting !! */ + D("%s: closing accept fd %d", __FUNCTION__, f->fd); + fdhandler_close(f); + return; + } +} + + +/* Create a new FDHandler used to monitor new connections on a + * server socket. The receiver must expect the new connection + * fd in the 'channel' field of a dummy packet. + */ +static FDHandler* +fdhandler_new_accept( int fd, + FDHandlerList* list, + Receiver* receiver ) +{ + FDHandler* f = xalloc0(sizeof(*f)); + + f->fd = fd; + f->list = list; + f->receiver[0] = receiver[0]; + + fdhandler_prepend(f, &list->active); + + looper_add( list->looper, fd, (EventFunc) fdhandler_accept_event, f ); + looper_enable( list->looper, fd, EPOLLIN ); + listen( fd, 5 ); + + return f; +} + +/** SERIAL CONNECTION STATE + ** + ** The following is used to handle the framing protocol + ** used on the serial port connection. + **/ + +/* each packet is made of a 6 byte header followed by a payload + * the header looks like: + * + * offset size description + * 0 2 a 2-byte hex string for the channel number + * 4 4 a 4-char hex string for the size of the payload + * 6 n the payload itself + */ +#define HEADER_SIZE 6 +#define CHANNEL_OFFSET 0 +#define LENGTH_OFFSET 2 +#define CHANNEL_SIZE 2 +#define LENGTH_SIZE 4 + +#define CHANNEL_CONTROL 0 + +/* The Serial object receives data from the serial port, + * extracts the payload size and channel index, then sends + * the resulting messages as a packet to a generic receiver. + * + * You can also use serial_send to send a packet through + * the serial port. + */ +typedef struct Serial { + FDHandler* fdhandler; /* used to monitor serial port fd */ + Receiver receiver[1]; /* send payload there */ + int in_len; /* current bytes in input packet */ + int in_datalen; /* payload size, or 0 when reading header */ + int in_channel; /* extracted channel number */ + Packet* in_packet; /* used to read incoming packets */ +} Serial; + + +/* a callback called when the serial port's fd is closed */ +static void +serial_fd_close( Serial* s ) +{ + fatal("unexpected serial port close !!"); +} + +static void +serial_dump( Packet* p, const char* funcname ) +{ + T("%s: %03d bytes: '%s'", + funcname, p->len, quote(p->data, p->len)); +} + +/* a callback called when a packet arrives from the serial port's FDHandler. + * + * This will essentially parse the header, extract the channel number and + * the payload size and store them in 'in_datalen' and 'in_channel'. + * + * After that, the payload is sent to the receiver once completed. + */ +static void +serial_fd_receive( Serial* s, Packet* p ) +{ + int rpos = 0, rcount = p->len; + Packet* inp = s->in_packet; + int inpos = s->in_len; + + serial_dump( p, __FUNCTION__ ); + + while (rpos < rcount) + { + int avail = rcount - rpos; + + /* first, try to read the header */ + if (s->in_datalen == 0) { + int wanted = HEADER_SIZE - inpos; + if (avail > wanted) + avail = wanted; + + memcpy( inp->data + inpos, p->data + rpos, avail ); + inpos += avail; + rpos += avail; + + if (inpos == HEADER_SIZE) { + s->in_datalen = hex2int( inp->data + LENGTH_OFFSET, LENGTH_SIZE ); + s->in_channel = hex2int( inp->data + CHANNEL_OFFSET, CHANNEL_SIZE ); + + if (s->in_datalen <= 0) { + D("ignoring %s packet from serial port", + s->in_datalen ? "empty" : "malformed"); + s->in_datalen = 0; + } + + //D("received %d bytes packet for channel %d", s->in_datalen, s->in_channel); + inpos = 0; + } + } + else /* then, populate the packet itself */ + { + int wanted = s->in_datalen - inpos; + + if (avail > wanted) + avail = wanted; + + memcpy( inp->data + inpos, p->data + rpos, avail ); + inpos += avail; + rpos += avail; + + if (inpos == s->in_datalen) { + if (s->in_channel < 0) { + D("ignoring %d bytes addressed to channel %d", + inpos, s->in_channel); + } else { + inp->len = inpos; + inp->channel = s->in_channel; + receiver_post( s->receiver, inp ); + s->in_packet = inp = packet_alloc(); + } + s->in_datalen = 0; + inpos = 0; + } + } + } + s->in_len = inpos; + packet_free(&p); +} + + +/* send a packet to the serial port. + * this assumes that p->len and p->channel contain the payload's + * size and channel and will add the appropriate header. + */ +static void +serial_send( Serial* s, Packet* p ) +{ + Packet* h = packet_alloc(); + + //D("sending to serial %d bytes from channel %d: '%.*s'", p->len, p->channel, p->len, p->data); + + /* insert a small header before this packet */ + h->len = HEADER_SIZE; + int2hex( p->len, h->data + LENGTH_OFFSET, LENGTH_SIZE ); + int2hex( p->channel, h->data + CHANNEL_OFFSET, CHANNEL_SIZE ); + + serial_dump( h, __FUNCTION__ ); + serial_dump( p, __FUNCTION__ ); + + fdhandler_enqueue( s->fdhandler, h ); + fdhandler_enqueue( s->fdhandler, p ); +} + + +/* initialize serial reader */ +static void +serial_init( Serial* s, + int fd, + FDHandlerList* list, + Receiver* receiver ) +{ + Receiver recv; + + recv.user = s; + recv.post = (PostFunc) serial_fd_receive; + recv.close = (CloseFunc) serial_fd_close; + + s->receiver[0] = receiver[0]; + + s->fdhandler = fdhandler_new( fd, list, &recv ); + s->in_len = 0; + s->in_datalen = 0; + s->in_channel = 0; + s->in_packet = packet_alloc(); +} + + +/** CLIENTS + **/ + +typedef struct Client Client; +typedef struct Multiplexer Multiplexer; + +/* A Client object models a single qemud client socket + * connection in the emulated system. + * + * the client first sends the name of the system service + * it wants to contact (no framing), then waits for a 2 + * byte answer from qemud. + * + * the answer is either "OK" or "KO" to indicate + * success or failure. + * + * In case of success, the client can send messages + * to the service. + * + * In case of failure, it can disconnect or try sending + * the name of another service. + */ +struct Client { + Client* next; + Client** pref; + int channel; + char registered; + FDHandler* fdhandler; + Multiplexer* multiplexer; +}; + +struct Multiplexer { + Client* clients; + int last_channel; + Serial serial[1]; + Looper looper[1]; + FDHandlerList fdhandlers[1]; +}; + + +static int multiplexer_open_channel( Multiplexer* mult, Packet* p ); +static void multiplexer_close_channel( Multiplexer* mult, int channel ); +static void multiplexer_serial_send( Multiplexer* mult, int channel, Packet* p ); + +static void +client_dump( Client* c, Packet* p, const char* funcname ) +{ + T("%s: client %p (%d): %3d bytes: '%s'", + funcname, c, c->fdhandler->fd, + p->len, quote(p->data, p->len)); +} + +/* destroy a client */ +static void +client_free( Client* c ) +{ + /* remove from list */ + c->pref[0] = c->next; + if (c->next) + c->next->pref = c->pref; + + c->channel = -1; + c->registered = 0; + + /* gently ask the FDHandler to shutdown to + * avoid losing queued outgoing packets */ + if (c->fdhandler != NULL) { + fdhandler_shutdown(c->fdhandler); + c->fdhandler = NULL; + } + + xfree(c); +} + + +/* a function called when a client socket receives data */ +static void +client_fd_receive( Client* c, Packet* p ) +{ + client_dump(c, p, __FUNCTION__); + + if (c->registered) { + /* the client is registered, just send the + * data through the serial port + */ + multiplexer_serial_send(c->multiplexer, c->channel, p); + return; + } + + if (c->channel > 0) { + /* the client is waiting registration results. + * this should not happen because the client + * should wait for our 'ok' or 'ko'. + * close the connection. + */ + D("%s: bad client sending data before end of registration", + __FUNCTION__); + BAD_CLIENT: + packet_free(&p); + client_free(c); + return; + } + + /* the client hasn't registered a service yet, + * so this must be the name of a service, call + * the multiplexer to start registration for + * it. + */ + D("%s: attempting registration for service '%.*s'", + __FUNCTION__, p->len, p->data); + c->channel = multiplexer_open_channel(c->multiplexer, p); + if (c->channel < 0) { + D("%s: service name too long", __FUNCTION__); + goto BAD_CLIENT; + } + D("%s: -> received channel id %d", __FUNCTION__, c->channel); + packet_free(&p); +} + + +/* a function called when the client socket is closed. */ +static void +client_fd_close( Client* c ) +{ + T("%s: client %p (%d)", __FUNCTION__, c, c->fdhandler->fd); + + /* no need to shutdown the FDHandler */ + c->fdhandler = NULL; + + /* tell the emulator we're out */ + if (c->channel > 0) + multiplexer_close_channel(c->multiplexer, c->channel); + + /* free the client */ + client_free(c); +} + +/* a function called when the multiplexer received a registration + * response from the emulator for a given client. + */ +static void +client_registration( Client* c, int registered ) +{ + Packet* p = packet_alloc(); + + /* sends registration status to client */ + if (!registered) { + D("%s: registration failed for client %d", __FUNCTION__, c->channel); + memcpy( p->data, "KO", 2 ); + p->len = 2; + } else { + D("%s: registration succeeded for client %d", __FUNCTION__, c->channel); + memcpy( p->data, "OK", 2 ); + p->len = 2; + } + client_dump(c, p, __FUNCTION__); + fdhandler_enqueue(c->fdhandler, p); + + /* now save registration state + */ + c->registered = registered; + if (!registered) { + /* allow the client to try registering another service */ + c->channel = -1; + } +} + +/* send data to a client */ +static void +client_send( Client* c, Packet* p ) +{ + client_dump(c, p, __FUNCTION__); + fdhandler_enqueue(c->fdhandler, p); +} + + +/* Create new client socket handler */ +static Client* +client_new( Multiplexer* mult, + int fd, + FDHandlerList* pfdhandlers, + Client** pclients ) +{ + Client* c; + Receiver recv; + + xnew(c); + + c->multiplexer = mult; + c->next = NULL; + c->pref = &c->next; + c->channel = -1; + c->registered = 0; + + recv.user = c; + recv.post = (PostFunc) client_fd_receive; + recv.close = (CloseFunc) client_fd_close; + + c->fdhandler = fdhandler_new( fd, pfdhandlers, &recv ); + + /* add to client list */ + c->next = *pclients; + c->pref = pclients; + *pclients = c; + if (c->next) + c->next->pref = &c->next; + + return c; +} + +/** GLOBAL MULTIPLEXER + **/ + +/* find a client by its channel */ +static Client* +multiplexer_find_client( Multiplexer* mult, int channel ) +{ + Client* c = mult->clients; + + for ( ; c != NULL; c = c->next ) { + if (c->channel == channel) + return c; + } + return NULL; +} + +/* handle control messages coming from the serial port + * on CONTROL_CHANNEL. + */ +static void +multiplexer_handle_control( Multiplexer* mult, Packet* p ) +{ + /* connection registration success */ + if (p->len == 13 && !memcmp(p->data, "ok:connect:", 11)) { + int channel = hex2int(p->data+11, 2); + Client* client = multiplexer_find_client(mult, channel); + + /* note that 'client' can be NULL if the corresponding + * socket was closed before the emulator response arrived. + */ + if (client != NULL) { + client_registration(client, 1); + } else { + D("%s: NULL client: '%.*s'", __FUNCTION__, p->len, p->data+11); + } + goto EXIT; + } + + /* connection registration failure */ + if (p->len == 13 && !memcmp(p->data, "ko:connect:",11)) { + int channel = hex2int(p->data+11, 2); + Client* client = multiplexer_find_client(mult, channel); + + if (client != NULL) + client_registration(client, 0); + + goto EXIT; + } + + /* emulator-induced client disconnection */ + if (p->len == 13 && !memcmp(p->data, "disconnect:",11)) { + int channel = hex2int(p->data+11, 2); + Client* client = multiplexer_find_client(mult, channel); + + if (client != NULL) + client_free(client); + + goto EXIT; + } + + /* A message that begins with "X00" is a probe sent by + * the emulator used to detect which version of qemud it runs + * against (in order to detect 1.0/1.1 system images. Just + * silently ignore it there instead of printing an error + * message. + */ + if (p->len >= 3 && !memcmp(p->data,"X00",3)) { + goto EXIT; + } + + D("%s: unknown control message (%d bytes): '%.*s'", + __FUNCTION__, p->len, p->len, p->data); + +EXIT: + packet_free(&p); +} + +/* a function called when an incoming packet comes from the serial port */ +static void +multiplexer_serial_receive( Multiplexer* mult, Packet* p ) +{ + Client* client; + + T("%s: channel=%d '%.*s'", __FUNCTION__, p->channel, p->len, p->data); + + if (p->channel == CHANNEL_CONTROL) { + multiplexer_handle_control(mult, p); + return; + } + + client = multiplexer_find_client(mult, p->channel); + if (client != NULL) { + client_send(client, p); + return; + } + + D("%s: discarding packet for unknown channel %d", __FUNCTION__, p->channel); + packet_free(&p); +} + +/* a function called when the serial reader closes */ +static void +multiplexer_serial_close( Multiplexer* mult ) +{ + fatal("unexpected close of serial reader"); +} + +/* a function called to send a packet to the serial port */ +static void +multiplexer_serial_send( Multiplexer* mult, int channel, Packet* p ) +{ + p->channel = channel; + serial_send( mult->serial, p ); +} + + + +/* a function used by a client to allocate a new channel id and + * ask the emulator to open it. 'service' must be a packet containing + * the name of the service in its payload. + * + * returns -1 if the service name is too long. + * + * notice that client_registration() will be called later when + * the answer arrives. + */ +static int +multiplexer_open_channel( Multiplexer* mult, Packet* service ) +{ + Packet* p = packet_alloc(); + int len, channel; + + /* find a free channel number, assume we don't have many + * clients here. */ + { + Client* c; + TRY_AGAIN: + channel = (++mult->last_channel) & 0xff; + + for (c = mult->clients; c != NULL; c = c->next) + if (c->channel == channel) + goto TRY_AGAIN; + } + + len = snprintf((char*)p->data, sizeof p->data, "connect:%.*s:%02x", service->len, service->data, channel); + if (len >= (int)sizeof(p->data)) { + D("%s: weird, service name too long (%d > %d)", __FUNCTION__, len, sizeof(p->data)); + packet_free(&p); + return -1; + } + p->channel = CHANNEL_CONTROL; + p->len = len; + + serial_send(mult->serial, p); + return channel; +} + +/* used to tell the emulator a channel was closed by a client */ +static void +multiplexer_close_channel( Multiplexer* mult, int channel ) +{ + Packet* p = packet_alloc(); + int len = snprintf((char*)p->data, sizeof(p->data), "disconnect:%02x", channel); + + if (len > (int)sizeof(p->data)) { + /* should not happen */ + return; + } + + p->channel = CHANNEL_CONTROL; + p->len = len; + + serial_send(mult->serial, p); +} + +/* this function is used when a new connection happens on the control + * socket. + */ +static void +multiplexer_control_accept( Multiplexer* m, Packet* p ) +{ + /* the file descriptor for the new socket connection is + * in p->channel. See fdhandler_accept_event() */ + int fd = p->channel; + Client* client = client_new( m, fd, m->fdhandlers, &m->clients ); + + D("created client %p listening on fd %d", client, fd); + + /* free dummy packet */ + packet_free(&p); +} + +static void +multiplexer_control_close( Multiplexer* m ) +{ + fatal("unexpected multiplexer control close"); +} + +static void +multiplexer_init( Multiplexer* m, const char* serial_dev ) +{ + int fd, control_fd; + Receiver recv; + + /* initialize looper and fdhandlers list */ + looper_init( m->looper ); + fdhandler_list_init( m->fdhandlers, m->looper ); + + /* open the serial port */ + do { + fd = open(serial_dev, O_RDWR); + } while (fd < 0 && errno == EINTR); + + if (fd < 0) { + fatal( "%s: could not open '%s': %s", __FUNCTION__, serial_dev, + strerror(errno) ); + } + // disable echo on serial lines + if ( !memcmp( serial_dev, "/dev/ttyS", 9 ) ) { + struct termios ios; + tcgetattr( fd, &ios ); + ios.c_lflag = 0; /* disable ECHO, ICANON, etc... */ + tcsetattr( fd, TCSANOW, &ios ); + } + + /* initialize the serial reader/writer */ + recv.user = m; + recv.post = (PostFunc) multiplexer_serial_receive; + recv.close = (CloseFunc) multiplexer_serial_close; + + serial_init( m->serial, fd, m->fdhandlers, &recv ); + + /* open the qemud control socket */ + recv.user = m; + recv.post = (PostFunc) multiplexer_control_accept; + recv.close = (CloseFunc) multiplexer_control_close; + + fd = android_get_control_socket(CONTROL_SOCKET_NAME); + if (fd < 0) { + fatal("couldn't get fd for control socket '%s'", CONTROL_SOCKET_NAME); + } + + fdhandler_new_accept( fd, m->fdhandlers, &recv ); + + /* initialize clients list */ + m->clients = NULL; +} + +/** MAIN LOOP + **/ + +static Multiplexer _multiplexer[1]; + +int main( void ) +{ + Multiplexer* m = _multiplexer; + + /* extract the name of our serial device from the kernel + * boot options that are stored in /proc/cmdline + */ +#define KERNEL_OPTION "android.qemud=" + + { + char buff[1024]; + int fd, len; + char* p; + char* q; + + fd = open( "/proc/cmdline", O_RDONLY ); + if (fd < 0) { + D("%s: can't open /proc/cmdline !!: %s", __FUNCTION__, + strerror(errno)); + exit(1); + } + + len = fd_read( fd, buff, sizeof(buff)-1 ); + close(fd); + if (len < 0) { + D("%s: can't read /proc/cmdline: %s", __FUNCTION__, + strerror(errno)); + exit(1); + } + buff[len] = 0; + + p = strstr( buff, KERNEL_OPTION ); + if (p == NULL) { + D("%s: can't find '%s' in /proc/cmdline", + __FUNCTION__, KERNEL_OPTION ); + exit(1); + } + + p += sizeof(KERNEL_OPTION)-1; /* skip option */ + q = p; + while ( *q && *q != ' ' && *q != '\t' ) + q += 1; + + snprintf( buff, sizeof(buff), "/dev/%.*s", q-p, p ); + + multiplexer_init( m, buff ); + } + + D( "entering main loop"); + looper_loop( m->looper ); + D( "unexpected termination !!" ); + return 0; +} diff --git a/emulator/sensors/Android.mk b/emulator/sensors/Android.mk new file mode 100644 index 0000000..9b0e83d --- /dev/null +++ b/emulator/sensors/Android.mk @@ -0,0 +1,38 @@ +# Copyright (C) 2009 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. + + +# We're moving the emulator-specific platform libs to +# development.git/tools/emulator/. The following test is to ensure +# smooth builds even if the tree contains both versions. +# +ifndef BUILD_EMULATOR_SENSORS_MODULE +BUILD_EMULATOR_SENSORS_MODULE := true + +LOCAL_PATH := $(call my-dir) + +ifneq ($(TARGET_PRODUCT),sim) +# HAL module implemenation, not prelinked and stored in +# hw/<SENSORS_HARDWARE_MODULE_ID>.<ro.hardware>.so +include $(CLEAR_VARS) +LOCAL_PRELINK_MODULE := false +LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)/hw +LOCAL_SHARED_LIBRARIES := liblog libcutils +LOCAL_SRC_FILES := sensors_qemu.c +LOCAL_MODULE := sensors.goldfish +LOCAL_MODULE_TAGS := debug +include $(BUILD_SHARED_LIBRARY) +endif + +endif # BUILD_EMULATOR_SENSORS_MODULE diff --git a/emulator/sensors/sensors_qemu.c b/emulator/sensors/sensors_qemu.c new file mode 100644 index 0000000..9a776c7 --- /dev/null +++ b/emulator/sensors/sensors_qemu.c @@ -0,0 +1,637 @@ +/* + * Copyright (C) 2009 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. + */ + +/* this implements a sensors hardware library for the Android emulator. + * the following code should be built as a shared library that will be + * placed into /system/lib/hw/sensors.goldfish.so + * + * it will be loaded by the code in hardware/libhardware/hardware.c + * which is itself called from com_android_server_SensorService.cpp + */ + + +/* we connect with the emulator through the "sensors" qemud service + */ +#define SENSORS_SERVICE_NAME "sensors" + +#define LOG_TAG "QemuSensors" + +#include <unistd.h> +#include <fcntl.h> +#include <errno.h> +#include <string.h> +#include <cutils/log.h> +#include <cutils/native_handle.h> +#include <cutils/sockets.h> +#include <hardware/sensors.h> + +#if 0 +#define D(...) LOGD(__VA_ARGS__) +#else +#define D(...) ((void)0) +#endif + +#define E(...) LOGE(__VA_ARGS__) + +#include <hardware/qemud.h> + +/** SENSOR IDS AND NAMES + **/ + +#define MAX_NUM_SENSORS 5 + +#define SUPPORTED_SENSORS ((1<<MAX_NUM_SENSORS)-1) + +#define ID_BASE SENSORS_HANDLE_BASE +#define ID_ACCELERATION (ID_BASE+0) +#define ID_MAGNETIC_FIELD (ID_BASE+1) +#define ID_ORIENTATION (ID_BASE+2) +#define ID_TEMPERATURE (ID_BASE+3) +#define ID_PROXIMITY (ID_BASE+4) + +#define SENSORS_ACCELERATION (1 << ID_ACCELERATION) +#define SENSORS_MAGNETIC_FIELD (1 << ID_MAGNETIC_FIELD) +#define SENSORS_ORIENTATION (1 << ID_ORIENTATION) +#define SENSORS_TEMPERATURE (1 << ID_TEMPERATURE) +#define SENSORS_PROXIMITY (1 << ID_PROXIMITY) + +#define ID_CHECK(x) ((unsigned)((x)-ID_BASE) < MAX_NUM_SENSORS) + +#define SENSORS_LIST \ + SENSOR_(ACCELERATION,"acceleration") \ + SENSOR_(MAGNETIC_FIELD,"magnetic-field") \ + SENSOR_(ORIENTATION,"orientation") \ + SENSOR_(TEMPERATURE,"temperature") \ + SENSOR_(PROXIMITY,"proximity") \ + +static const struct { + const char* name; + int id; } _sensorIds[MAX_NUM_SENSORS] = +{ +#define SENSOR_(x,y) { y, ID_##x }, + SENSORS_LIST +#undef SENSOR_ +}; + +static const char* +_sensorIdToName( int id ) +{ + int nn; + for (nn = 0; nn < MAX_NUM_SENSORS; nn++) + if (id == _sensorIds[nn].id) + return _sensorIds[nn].name; + return "<UNKNOWN>"; +} + +static int +_sensorIdFromName( const char* name ) +{ + int nn; + + if (name == NULL) + return -1; + + for (nn = 0; nn < MAX_NUM_SENSORS; nn++) + if (!strcmp(name, _sensorIds[nn].name)) + return _sensorIds[nn].id; + + return -1; +} + +/** SENSORS POLL DEVICE + ** + ** This one is used to read sensor data from the hardware. + ** We implement this by simply reading the data from the + ** emulator through the QEMUD channel. + **/ + +typedef struct SensorPoll { + struct sensors_poll_device_t device; + sensors_event_t sensors[MAX_NUM_SENSORS]; + int events_fd; + uint32_t pendingSensors; + int64_t timeStart; + int64_t timeOffset; + int fd; + uint32_t active_sensors; +} SensorPoll; + +/* this must return a file descriptor that will be used to read + * the sensors data (it is passed to data__data_open() below + */ +static native_handle_t* +control__open_data_source(struct sensors_poll_device_t *dev) +{ + SensorPoll* ctl = (void*)dev; + native_handle_t* handle; + + if (ctl->fd < 0) { + ctl->fd = qemud_channel_open(SENSORS_SERVICE_NAME); + } + D("%s: fd=%d", __FUNCTION__, ctl->fd); + handle = native_handle_create(1, 0); + handle->data[0] = dup(ctl->fd); + return handle; +} + +static int +control__activate(struct sensors_poll_device_t *dev, + int handle, + int enabled) +{ + SensorPoll* ctl = (void*)dev; + uint32_t mask, sensors, active, new_sensors, changed; + char command[128]; + int ret; + + D("%s: handle=%s (%d) fd=%d enabled=%d", __FUNCTION__, + _sensorIdToName(handle), handle, ctl->fd, enabled); + + if (!ID_CHECK(handle)) { + E("%s: bad handle ID", __FUNCTION__); + return -1; + } + + mask = (1<<handle); + sensors = enabled ? mask : 0; + + active = ctl->active_sensors; + new_sensors = (active & ~mask) | (sensors & mask); + changed = active ^ new_sensors; + + if (!changed) + return 0; + + snprintf(command, sizeof command, "set:%s:%d", + _sensorIdToName(handle), enabled != 0); + + if (ctl->fd < 0) { + ctl->fd = qemud_channel_open(SENSORS_SERVICE_NAME); + } + + ret = qemud_channel_send(ctl->fd, command, -1); + if (ret < 0) { + E("%s: when sending command errno=%d: %s", __FUNCTION__, errno, strerror(errno)); + return -1; + } + ctl->active_sensors = new_sensors; + + return 0; +} + +static int +control__set_delay(struct sensors_poll_device_t *dev, int32_t ms) +{ + SensorPoll* ctl = (void*)dev; + char command[128]; + + D("%s: dev=%p delay-ms=%d", __FUNCTION__, dev, ms); + + snprintf(command, sizeof command, "set-delay:%d", ms); + + return qemud_channel_send(ctl->fd, command, -1); +} + +static int +control__close(struct hw_device_t *dev) +{ + SensorPoll* ctl = (void*)dev; + close(ctl->fd); + free(ctl); + return 0; +} + +/* return the current time in nanoseconds */ +static int64_t +data__now_ns(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + + return (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec; +} + +static int +data__data_open(struct sensors_poll_device_t *dev, native_handle_t* handle) +{ + SensorPoll* data = (void*)dev; + int i; + D("%s: dev=%p fd=%d", __FUNCTION__, dev, handle->data[0]); + memset(&data->sensors, 0, sizeof(data->sensors)); + + for (i=0 ; i<MAX_NUM_SENSORS ; i++) { + data->sensors[i].acceleration.status = SENSOR_STATUS_ACCURACY_HIGH; + } + data->pendingSensors = 0; + data->timeStart = 0; + data->timeOffset = 0; + + data->events_fd = dup(handle->data[0]); + D("%s: dev=%p fd=%d (was %d)", __FUNCTION__, dev, data->events_fd, handle->data[0]); + native_handle_close(handle); + native_handle_delete(handle); + return 0; +} + +static int +data__data_close(struct sensors_poll_device_t *dev) +{ + SensorPoll* data = (void*)dev; + D("%s: dev=%p", __FUNCTION__, dev); + if (data->events_fd >= 0) { + close(data->events_fd); + data->events_fd = -1; + } + return 0; +} + +static int +pick_sensor(SensorPoll* data, + sensors_event_t* values) +{ + uint32_t mask = SUPPORTED_SENSORS; + while (mask) { + uint32_t i = 31 - __builtin_clz(mask); + mask &= ~(1<<i); + if (data->pendingSensors & (1<<i)) { + data->pendingSensors &= ~(1<<i); + *values = data->sensors[i]; + values->sensor = i; + values->version = sizeof(*values); + + D("%s: %d [%f, %f, %f]", __FUNCTION__, + i, + values->data[0], + values->data[1], + values->data[2]); + return i; + } + } + LOGE("No sensor to return!!! pendingSensors=%08x", data->pendingSensors); + // we may end-up in a busy loop, slow things down, just in case. + usleep(100000); + return -EINVAL; +} + +static int +data__poll(struct sensors_poll_device_t *dev, sensors_event_t* values) +{ + SensorPoll* data = (void*)dev; + int fd = data->events_fd; + + D("%s: data=%p", __FUNCTION__, dev); + + // there are pending sensors, returns them now... + if (data->pendingSensors) { + return pick_sensor(data, values); + } + + // wait until we get a complete event for an enabled sensor + uint32_t new_sensors = 0; + + while (1) { + /* read the next event */ + char buff[256]; + int len = qemud_channel_recv(data->events_fd, buff, sizeof buff-1); + float params[3]; + int64_t event_time; + + if (len < 0) { + E("%s: len=%d, errno=%d: %s", __FUNCTION__, len, errno, strerror(errno)); + return -errno; + } + + buff[len] = 0; + + /* "wake" is sent from the emulator to exit this loop. */ + if (!strcmp((const char*)data, "wake")) { + return 0x7FFFFFFF; + } + + /* "acceleration:<x>:<y>:<z>" corresponds to an acceleration event */ + if (sscanf(buff, "acceleration:%g:%g:%g", params+0, params+1, params+2) == 3) { + new_sensors |= SENSORS_ACCELERATION; + data->sensors[ID_ACCELERATION].acceleration.x = params[0]; + data->sensors[ID_ACCELERATION].acceleration.y = params[1]; + data->sensors[ID_ACCELERATION].acceleration.z = params[2]; + continue; + } + + /* "orientation:<azimuth>:<pitch>:<roll>" is sent when orientation changes */ + if (sscanf(buff, "orientation:%g:%g:%g", params+0, params+1, params+2) == 3) { + new_sensors |= SENSORS_ORIENTATION; + data->sensors[ID_ORIENTATION].orientation.azimuth = params[0]; + data->sensors[ID_ORIENTATION].orientation.pitch = params[1]; + data->sensors[ID_ORIENTATION].orientation.roll = params[2]; + continue; + } + + /* "magnetic:<x>:<y>:<z>" is sent for the params of the magnetic field */ + if (sscanf(buff, "magnetic:%g:%g:%g", params+0, params+1, params+2) == 3) { + new_sensors |= SENSORS_MAGNETIC_FIELD; + data->sensors[ID_MAGNETIC_FIELD].magnetic.x = params[0]; + data->sensors[ID_MAGNETIC_FIELD].magnetic.y = params[1]; + data->sensors[ID_MAGNETIC_FIELD].magnetic.z = params[2]; + continue; + } + + /* "temperature:<celsius>" */ + if (sscanf(buff, "temperature:%g", params+0) == 2) { + new_sensors |= SENSORS_TEMPERATURE; + data->sensors[ID_TEMPERATURE].temperature = params[0]; + continue; + } + + /* "proximity:<value>" */ + if (sscanf(buff, "proximity:%g", params+0) == 1) { + new_sensors |= SENSORS_PROXIMITY; + data->sensors[ID_PROXIMITY].distance = params[0]; + continue; + } + + /* "sync:<time>" is sent after a series of sensor events. + * where 'time' is expressed in micro-seconds and corresponds + * to the VM time when the real poll occured. + */ + if (sscanf(buff, "sync:%lld", &event_time) == 1) { + if (new_sensors) { + data->pendingSensors = new_sensors; + int64_t t = event_time * 1000LL; /* convert to nano-seconds */ + + /* use the time at the first sync: as the base for later + * time values */ + if (data->timeStart == 0) { + data->timeStart = data__now_ns(); + data->timeOffset = data->timeStart - t; + } + t += data->timeOffset; + + while (new_sensors) { + uint32_t i = 31 - __builtin_clz(new_sensors); + new_sensors &= ~(1<<i); + data->sensors[i].timestamp = t; + } + return pick_sensor(data, values); + } else { + D("huh ? sync without any sensor data ?"); + } + continue; + } + D("huh ? unsupported command"); + } + return -1; +} + +static int +data__close(struct hw_device_t *dev) +{ + SensorPoll* data = (SensorPoll*)dev; + if (data) { + if (data->events_fd >= 0) { + //LOGD("(device close) about to close fd=%d", data->events_fd); + close(data->events_fd); + } + free(data); + } + return 0; +} + +/** SENSORS POLL DEVICE FUNCTIONS **/ + +static int poll__close(struct hw_device_t* dev) +{ + SensorPoll* ctl = (void*)dev; + close(ctl->fd); + if (ctl->fd >= 0) { + close(ctl->fd); + } + if (ctl->events_fd >= 0) { + close(ctl->events_fd); + } + free(ctl); + return 0; +} + +static int poll__poll(struct sensors_poll_device_t *dev, + sensors_event_t* data, int count) +{ + SensorPoll* datadev = (void*)dev; + int ret; + int i; + D("%s: dev=%p data=%p count=%d ", __FUNCTION__, dev, data, count); + + for (i = 0; i < count; i++) { + ret = data__poll(dev, data); + data++; + if (ret > MAX_NUM_SENSORS || ret < 0) { + return i; + } + if (!datadev->pendingSensors) { + return i + 1; + } + } + return count; +} + +static int poll__activate(struct sensors_poll_device_t *dev, + int handle, int enabled) +{ + int ret; + native_handle_t* hdl; + SensorPoll* ctl = (void*)dev; + D("%s: dev=%p handle=%x enable=%d ", __FUNCTION__, dev, handle, enabled); + if (ctl->fd < 0) { + D("%s: OPEN CTRL and DATA ", __FUNCTION__); + hdl = control__open_data_source(dev); + ret = data__data_open(dev,hdl); + } + ret = control__activate(dev, handle, enabled); + return ret; +} + +static int poll__setDelay(struct sensors_poll_device_t *dev, + int handle, int64_t ns) +{ + // TODO + return 0; +} + +/** MODULE REGISTRATION SUPPORT + ** + ** This is required so that hardware/libhardware/hardware.c + ** will dlopen() this library appropriately. + **/ + +/* + * the following is the list of all supported sensors. + * this table is used to build sSensorList declared below + * according to which hardware sensors are reported as + * available from the emulator (see get_sensors_list below) + * + * note: numerical values for maxRange/resolution/power were + * taken from the reference AK8976A implementation + */ +static const struct sensor_t sSensorListInit[] = { + { .name = "Goldfish 3-axis Accelerometer", + .vendor = "The Android Open Source Project", + .version = 1, + .handle = ID_ACCELERATION, + .type = SENSOR_TYPE_ACCELEROMETER, + .maxRange = 2.8f, + .resolution = 1.0f/4032.0f, + .power = 3.0f, + .reserved = {} + }, + + { .name = "Goldfish 3-axis Magnetic field sensor", + .vendor = "The Android Open Source Project", + .version = 1, + .handle = ID_MAGNETIC_FIELD, + .type = SENSOR_TYPE_MAGNETIC_FIELD, + .maxRange = 2000.0f, + .resolution = 1.0f, + .power = 6.7f, + .reserved = {} + }, + + { .name = "Goldfish Orientation sensor", + .vendor = "The Android Open Source Project", + .version = 1, + .handle = ID_ORIENTATION, + .type = SENSOR_TYPE_ORIENTATION, + .maxRange = 360.0f, + .resolution = 1.0f, + .power = 9.7f, + .reserved = {} + }, + + { .name = "Goldfish Temperature sensor", + .vendor = "The Android Open Source Project", + .version = 1, + .handle = ID_TEMPERATURE, + .type = SENSOR_TYPE_TEMPERATURE, + .maxRange = 80.0f, + .resolution = 1.0f, + .power = 0.0f, + .reserved = {} + }, + + { .name = "Goldfish Proximity sensor", + .vendor = "The Android Open Source Project", + .version = 1, + .handle = ID_PROXIMITY, + .type = SENSOR_TYPE_PROXIMITY, + .maxRange = 1.0f, + .resolution = 1.0f, + .power = 20.0f, + .reserved = {} + }, +}; + +static struct sensor_t sSensorList[MAX_NUM_SENSORS]; + +static int sensors__get_sensors_list(struct sensors_module_t* module, + struct sensor_t const** list) +{ + int fd = qemud_channel_open(SENSORS_SERVICE_NAME); + char buffer[12]; + int mask, nn, count; + + int ret; + if (fd < 0) { + E("%s: no qemud connection", __FUNCTION__); + return 0; + } + ret = qemud_channel_send(fd, "list-sensors", -1); + if (ret < 0) { + E("%s: could not query sensor list: %s", __FUNCTION__, + strerror(errno)); + close(fd); + return 0; + } + ret = qemud_channel_recv(fd, buffer, sizeof buffer-1); + if (ret < 0) { + E("%s: could not receive sensor list: %s", __FUNCTION__, + strerror(errno)); + close(fd); + return 0; + } + buffer[ret] = 0; + close(fd); + + /* the result is a integer used as a mask for available sensors */ + mask = atoi(buffer); + count = 0; + for (nn = 0; nn < MAX_NUM_SENSORS; nn++) { + if (((1 << nn) & mask) == 0) + continue; + + sSensorList[count++] = sSensorListInit[nn]; + } + D("%s: returned %d sensors (mask=%d)", __FUNCTION__, count, mask); + *list = sSensorList; + return count; +} + + +static int +open_sensors(const struct hw_module_t* module, + const char* name, + struct hw_device_t* *device) +{ + int status = -EINVAL; + + D("%s: name=%s", __FUNCTION__, name); + + if (!strcmp(name, SENSORS_HARDWARE_POLL)) { + SensorPoll *dev = malloc(sizeof(*dev)); + + memset(dev, 0, sizeof(*dev)); + + dev->device.common.tag = HARDWARE_DEVICE_TAG; + dev->device.common.version = 0; + dev->device.common.module = (struct hw_module_t*) module; + dev->device.common.close = poll__close; + dev->device.poll = poll__poll; + dev->device.activate = poll__activate; + dev->device.setDelay = poll__setDelay; + dev->events_fd = -1; + dev->fd = -1; + + *device = &dev->device.common; + status = 0; + } + return status; +} + + +static struct hw_module_methods_t sensors_module_methods = { + .open = open_sensors +}; + +const struct sensors_module_t HAL_MODULE_INFO_SYM = { + .common = { + .tag = HARDWARE_MODULE_TAG, + .version_major = 1, + .version_minor = 0, + .id = SENSORS_HARDWARE_MODULE_ID, + .name = "Goldfish SENSORS Module", + .author = "The Android Open Source Project", + .methods = &sensors_module_methods, + }, + .get_sensors_list = sensors__get_sensors_list +}; diff --git a/emulator/tests/Android.mk b/emulator/tests/Android.mk new file mode 100644 index 0000000..04917f4 --- /dev/null +++ b/emulator/tests/Android.mk @@ -0,0 +1,17 @@ +# This directory contains various host tests to be used with the emulator +# NOTE: Most of these are only built and run on Linux. + +LOCAL_PATH := $(call my-dir) + +# The test-qemud-pipes program is used to check the execution of QEMUD Pipes +# See external/qemu/docs/ANDROID-QEMUD-PIPES.TXT for details. +# +ifeq ($(HOST_OS),XXXXlinux) + +include $(CLEAR_VARS) +LOCAL_MODULE := test-qemud-pipes +LOCAL_SRC_FILES := test-qemud-pipes.c +LOCAL_MODULE_TAGS := debug +include $(BUILD_HOST_EXECUTABLE) + +endif # HOST_OS == linux
\ No newline at end of file diff --git a/emulator/tests/test-qemud-pipes.c b/emulator/tests/test-qemud-pipes.c new file mode 100644 index 0000000..f5db531 --- /dev/null +++ b/emulator/tests/test-qemud-pipes.c @@ -0,0 +1,113 @@ +/* This program is used to test the QEMUD fast pipes. + * See external/qemu/docs/ANDROID-QEMUD-PIPES.TXT for details. + * + * The program acts as a simple TCP server that accepts data and sends + * them back to the client. + */ + +#include <sys/socket.h> +#include <net/inet.h> +#include <stdio.h> +#include <unistd.h> +#include <errno.h> +#include <string.h> + +#define DEFAULT_PORT 8012 + +static void +socket_close(int sock) +{ + int old_errno = errno; + close(sock); + errno = old_errno; +} + +static int +socket_loopback_server( int port, int type ) +{ + struct sockaddr_in addr; + + int sock = socket(AF_INET, type, 0); + if (sock < 0) { + return -1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + int n = 1; + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &n, sizeof(n)); + + if (TEMP_FAILURE_RETRY(bind(sock, &addr, sizeof(addr))) < 0) { + socket_close(sock); + return -1; + } + + if (type == SOCK_STREAM) { + if (TEMP_FAILURE_RETRY(listen(sock, 4)) < 0) { + socket_close(sock); + return -1; + } + } + + return sock; +} + +int main(void) +{ + int sock, client; + int port = DEFAULT_PORT; + + printf("Starting pipe test server on local port %d\n", port); + sock = socket_loopback_server( port, SOCK_STREAM ); + if (sock < 0) { + fprintf(stderr, "Could not start server: %s\n", strerror(errno)); + return 1; + } + + client = accept(sock, NULL, NULL); + if (client < 0) { + fprintf(stderr, "Server error: %s\n", strerror(errno)); + return 2; + } + printf("Client connected!\n"); + + /* Now, accept any incoming data, and send it back */ + for (;;) { + char buff[1024], *p; + int ret, count; + + do { + ret = read(client, buff, sizeof(buff)); + } while (ret < 0 && errno == EINTR); + + if (ret < 0) { + fprintf(stderr, "Client read error: %s\n", strerror(errno)); + close(client); + return 3; + } + count = ret; + p = buff; + printf(" received: %d bytes\n", count); + + while (count > 0) { + do { + ret = write(client, p, count); + } while (ret < 0 && errno == EINTR); + + if (ret < 0) { + fprintf(stderr, "Client write error: %s\n", strerror(errno)); + close(client); + return 4; + } + printf(" sent: %d bytes\n", ret); + + p += ret; + count -= ret; + } + } + + return 0; +} diff --git a/emulator/tools/Android.mk b/emulator/tools/Android.mk new file mode 100644 index 0000000..1bdbf68 --- /dev/null +++ b/emulator/tools/Android.mk @@ -0,0 +1,44 @@ +# Copyright (C) 2009 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. + +# this file is used to build emulator-specific program tools +# that should only run in the emulator. +# + +# We're moving the emulator-specific platform libs to +# development.git/tools/emulator/. The following test is to ensure +# smooth builds even if the tree contains both versions. +# +ifndef BUILD_EMULATOR_QEMU_PROPS +BUILD_EMULATOR_QEMU_PROPS := true + +LOCAL_PATH := $(call my-dir) + +ifneq ($(TARGET_PRODUCT),sim) + +# The 'qemu-props' program is run from /system/etc/init.goldfish.rc +# to setup various system properties sent by the emulator program. +# +include $(CLEAR_VARS) +LOCAL_MODULE := qemu-props +LOCAL_SRC_FILES := qemu-props.c +LOCAL_SHARED_LIBRARIES := libcutils +# we don't want this in 'user' builds which don't have +# emulator-specific binaries. +LOCAL_MODULE_TAGS := debug +include $(BUILD_EXECUTABLE) + +endif # TARGET_PRODUCT != sim + +endif # BUILD_EMULATOR_QEMU_PROPS diff --git a/emulator/tools/qemu-props.c b/emulator/tools/qemu-props.c new file mode 100644 index 0000000..3f086a1 --- /dev/null +++ b/emulator/tools/qemu-props.c @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2009 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. + */ + +/* this program is used to read a set of system properties and their values + * from the emulator program and set them in the currently-running emulated + * system. It does so by connecting to the 'boot-properties' qemud service. + * + * This program should be run as root and called from + * /system/etc/init.goldfish.rc exclusively. + */ + +#define LOG_TAG "qemu-props" + +#define DEBUG 1 + +#if DEBUG +# include <cutils/log.h> +# define DD(...) LOGI(__VA_ARGS__) +#else +# define DD(...) ((void)0) +#endif + +#include <cutils/properties.h> +#include <unistd.h> +#include <hardware/qemud.h> + +/* Name of the qemud service we want to connect to. + */ +#define QEMUD_SERVICE "boot-properties" + +#define MAX_TRIES 5 + +int main(void) +{ + int qemud_fd, count = 0; + + /* try to connect to the qemud service */ + { + int tries = MAX_TRIES; + + while (1) { + qemud_fd = qemud_channel_open( "boot-properties" ); + if (qemud_fd >= 0) + break; + + if (--tries <= 0) { + DD("Could not connect after too many tries. Aborting"); + return 1; + } + + DD("waiting 1s to wait for qemud."); + sleep(1); + } + } + + DD("connected to '%s' qemud service.", QEMUD_SERVICE); + + /* send the 'list' command to the service */ + if (qemud_channel_send(qemud_fd, "list", -1) < 0) { + DD("could not send command to '%s' service", QEMUD_SERVICE); + return 1; + } + + /* read each system property as a single line from the service, + * until exhaustion. + */ + for (;;) + { +#define BUFF_SIZE (PROPERTY_KEY_MAX + PROPERTY_VALUE_MAX + 2) + DD("receiving.."); + char* q; + char temp[BUFF_SIZE]; + int len = qemud_channel_recv(qemud_fd, temp, sizeof temp - 1); + + /* lone NUL-byte signals end of properties */ + if (len < 0 || len > BUFF_SIZE-1 || temp[0] == '\0') + break; + + temp[len] = '\0'; /* zero-terminate string */ + + DD("received: %.*s", len, temp); + + /* separate propery name from value */ + q = strchr(temp, '='); + if (q == NULL) { + DD("invalid format, ignored."); + continue; + } + *q++ = '\0'; + + if (property_set(temp, q) < 0) { + DD("could not set property '%s' to '%s'", temp, q); + } else { + count += 1; + } + } + + + /* finally, close the channel and exit */ + close(qemud_fd); + DD("exiting (%d properties set).", count); + return 0; +} diff --git a/layoutlib_api/src/com/android/ide/common/rendering/api/Bridge.java b/layoutlib_api/src/com/android/ide/common/rendering/api/Bridge.java index f04b266..c044353 100644 --- a/layoutlib_api/src/com/android/ide/common/rendering/api/Bridge.java +++ b/layoutlib_api/src/com/android/ide/common/rendering/api/Bridge.java @@ -134,4 +134,17 @@ public abstract class Bridge { public Result getViewIndex(Object viewObject) { return NOT_IMPLEMENTED.createResult(); } + + /** + * Utility method returning the baseline value for a given view object. This basically returns + * View.getBaseline(). + * + * @param viewObject the object for which to return the index. + * + * @return the baseline value or -1 if not applicable to the view object or if this layout + * library does not implement this method. + */ + public int getViewBaseline(Object viewObject) { + return -1; + } } diff --git a/monkeyrunner/etc/monkeyrunner b/monkeyrunner/etc/monkeyrunner index 364be2a..fe9be0c 100755 --- a/monkeyrunner/etc/monkeyrunner +++ b/monkeyrunner/etc/monkeyrunner @@ -69,6 +69,29 @@ else jarpath="$frameworkdir/$jarfile" fi +# Figure out the path to the swt.jar for the current architecture. +# if ANDROID_SWT is defined, then just use this. +# else, if running in the Android source tree, then look for the correct swt folder in prebuilt +# else, look for the correct swt folder in the SDK under tools/lib/ +swtpath="" +if [ -n "$ANDROID_SWT" ]; then + swtpath="$ANDROID_SWT" +else + vmarch=`java -jar "${frameworkdir}"/archquery.jar` + if [ -n "$ANDROID_BUILD_TOP" ]; then + osname=`uname -s | tr A-Z a-z` + swtpath="${ANDROID_BUILD_TOP}/prebuilt/${osname}-${vmarch}/swt" + else + swtpath="${frameworkdir}/${vmarch}" + fi +fi + +if [ ! -d "$swtpath" ]; then + echo "SWT folder '${swtpath}' does not exist." + echo "Please export ANDROID_SWT to point to the folder containing swt.jar for your platform." + exit 1 +fi + # need to use "java.ext.dirs" because "-jar" causes classpath to be ignored # might need more memory, e.g. -Xmx128M -exec java -Xmx128M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir" -Djava.library.path="$libdir" -Dcom.android.monkeyrunner.bindir="$progdir" -jar "$jarpath" "$@" +exec java -Xmx128M $os_opts $java_debug -Djava.ext.dirs="$frameworkdir:$swtpath" -Djava.library.path="$libdir" -Dcom.android.monkeyrunner.bindir="$progdir" -jar "$jarpath" "$@" diff --git a/monkeyrunner/etc/monkeyrunner.bat b/monkeyrunner/etc/monkeyrunner.bat index 1cf38ca..5028b3f 100644 --- a/monkeyrunner/etc/monkeyrunner.bat +++ b/monkeyrunner/etc/monkeyrunner.bat @@ -43,4 +43,21 @@ if exist %frameworkdir%%jarfile% goto JarFileOk set jarpath=%frameworkdir%%jarfile% -call %java_exe% -Xmx512m -Djava.ext.dirs=%frameworkdir% -Dcom.android.monkeyrunner.bindir=..\framework -jar %jarpath% %* +if not defined ANDROID_SWT goto QueryArch + set swt_path=%ANDROID_SWT% + goto SwtDone + +:QueryArch + + for /f %%a in ('%java_exe% -jar %frameworkdir%archquery.jar') do set swt_path=%frameworkdir%%%a + +:SwtDone + +if exist %swt_path% goto SetPath + echo SWT folder '%swt_path%' does not exist. + echo Please set ANDROID_SWT to point to the folder containing swt.jar for your platform. + exit /B + +:SetPath + +call %java_exe% -Xmx512m -Djava.ext.dirs=%frameworkdir%;%swt_path% -Dcom.android.monkeyrunner.bindir=..\framework -jar %jarpath% %* diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java index 37b1cda..649e33c 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyDevice.java @@ -20,8 +20,6 @@ import java.util.Collections; import java.util.Map; import java.util.Set; -import javax.annotation.Nullable; - import org.python.core.ArgParser; import org.python.core.ClassDictInit; import org.python.core.Py; @@ -30,6 +28,8 @@ import org.python.core.PyException; import org.python.core.PyObject; import org.python.core.PyTuple; +import com.android.monkeyrunner.core.IMonkeyDevice; +import com.android.monkeyrunner.core.IMonkeyImage; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import com.android.monkeyrunner.easy.HierarchyViewer; @@ -45,7 +45,7 @@ import com.google.common.collect.ImmutableMap; * implementation of this class. */ @MonkeyRunnerExported(doc = "Represents a device attached to the system.") -public abstract class MonkeyDevice extends PyObject implements ClassDictInit { +public class MonkeyDevice extends PyObject implements ClassDictInit { public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(MonkeyDevice.class, dict); } @@ -57,49 +57,41 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { @MonkeyRunnerExported(doc = "Sends a DOWN event, immediately followed by an UP event when used with touch() or press()") public static final String DOWN_AND_UP = "downAndUp"; + // TODO: This may not be accessible from jython; if so, remove it. public enum TouchPressType { DOWN, UP, DOWN_AND_UP, } - public static final Map<String, TouchPressType> TOUCH_NAME_TO_ENUM = - ImmutableMap.of(MonkeyDevice.DOWN, TouchPressType.DOWN, - MonkeyDevice.UP, TouchPressType.UP, - MonkeyDevice.DOWN_AND_UP, TouchPressType.DOWN_AND_UP); + public static final Map<String, IMonkeyDevice.TouchPressType> TOUCH_NAME_TO_ENUM = + ImmutableMap.of(DOWN, IMonkeyDevice.TouchPressType.DOWN, + UP, IMonkeyDevice.TouchPressType.UP, + DOWN_AND_UP, IMonkeyDevice.TouchPressType.DOWN_AND_UP); private static final Set<String> VALID_DOWN_UP_TYPES = TOUCH_NAME_TO_ENUM.keySet(); - /** - * Create a MonkeyMananger for talking to this device. - * - * NOTE: This is not part of the jython API. - * - * @return the MonkeyManager - */ - public abstract MonkeyManager getManager(); - - /** - * Dispose of any native resoureces this device may have taken hold of. - * - * NOTE: This is not part of the jython API. - */ - public abstract void dispose(); - - /** - * @return hierarchy viewer implementation for querying state of the view - * hierarchy. - */ - public abstract HierarchyViewer getHierarchyViewer(); + private IMonkeyDevice impl; + + public MonkeyDevice(IMonkeyDevice impl) { + this.impl = impl; + } + + public IMonkeyDevice getImpl() { + return impl; + } @MonkeyRunnerExported(doc = "Get the HierarchyViewer object for the device.", returns = "A HierarchyViewer object") public HierarchyViewer getHierarchyViewer(PyObject[] args, String[] kws) { - return getHierarchyViewer(); + return impl.getHierarchyViewer(); } @MonkeyRunnerExported(doc = "Gets the device's screen buffer, yielding a screen capture of the entire display.", returns = "A MonkeyImage object (a bitmap wrapper)") - public abstract MonkeyImage takeSnapshot(); + public MonkeyImage takeSnapshot() { + IMonkeyImage image = impl.takeSnapshot(); + return new MonkeyImage(image); + } @MonkeyRunnerExported(doc = "Given the name of a variable on the device, " + "returns the variable's value", @@ -111,7 +103,7 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); - return getProperty(ap.getString(0)); + return impl.getProperty(ap.getString(0)); } @MonkeyRunnerExported(doc = "Synonym for getProperty()", @@ -121,7 +113,8 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { public String getSystemProperty(PyObject[] args, String[] kws) { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); - return getSystemProperty(ap.getString(0)); + + return impl.getSystemProperty(ap.getString(0)); } @MonkeyRunnerExported(doc = "Sends a touch event at the specified location", @@ -150,7 +143,7 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { // bad stuff was passed in, just use the already specified default value type = MonkeyDevice.DOWN_AND_UP; } - touch(x, y, TOUCH_NAME_TO_ENUM.get(type)); + impl.touch(x, y, TOUCH_NAME_TO_ENUM.get(type)); } @MonkeyRunnerExported(doc = "Simulates dragging (touch, hold, and move) on the device screen.", @@ -185,7 +178,7 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { int steps = ap.getInt(3, 10); - drag(startx, starty, endx, endy, steps, ms); + impl.drag(startx, starty, endx, endy, steps, ms); } @MonkeyRunnerExported(doc = "Send a key event to the specified key", @@ -213,7 +206,7 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { // bad stuff was passed in, just use the already specified default value type = MonkeyDevice.DOWN_AND_UP; } - press(name, TOUCH_NAME_TO_ENUM.get(type)); + impl.press(name, TOUCH_NAME_TO_ENUM.get(type)); } @MonkeyRunnerExported(doc = "Types the specified string on the keyboard. This is " + @@ -225,7 +218,7 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { Preconditions.checkNotNull(ap); String message = ap.getString(0); - type(message); + impl.type(message); } @MonkeyRunnerExported(doc = "Executes an adb shell command and returns the result, if any.", @@ -237,7 +230,7 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { Preconditions.checkNotNull(ap); String cmd = ap.getString(0); - return shell(cmd); + return impl.shell(cmd); } @MonkeyRunnerExported(doc = "Reboots the specified device into a specified bootloader.", @@ -249,7 +242,7 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { String into = ap.getString(0, null); - reboot(into); + impl.reboot(into); } @MonkeyRunnerExported(doc = "Installs the specified Android package (.apk file) " + @@ -262,7 +255,7 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { Preconditions.checkNotNull(ap); String path = ap.getString(0); - return installPackage(path); + return impl.installPackage(path); } @MonkeyRunnerExported(doc = "Deletes the specified package from the device, including its " + @@ -275,7 +268,7 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { Preconditions.checkNotNull(ap); String packageName = ap.getString(0); - return removePackage(packageName); + return impl.removePackage(packageName); } @MonkeyRunnerExported(doc = "Starts an Activity on the device by sending an Intent " + @@ -308,7 +301,7 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { String component = ap.getString(6, null); int flags = ap.getInt(7, 0); - startActivity(uri, action, data, mimetype, categories, extras, component, flags); + impl.startActivity(uri, action, data, mimetype, categories, extras, component, flags); } @MonkeyRunnerExported(doc = "Sends a broadcast intent to the device.", @@ -340,7 +333,7 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { String component = ap.getString(6, null); int flags = ap.getInt(7, 0); - broadcastIntent(uri, action, data, mimetype, categories, extras, component, flags); + impl.broadcastIntent(uri, action, data, mimetype, categories, extras, component, flags); } @MonkeyRunnerExported(doc = "Run the specified package with instrumentation and return " + @@ -368,7 +361,7 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { instrumentArgs = Collections.emptyMap(); } - Map<String, Object> result = instrument(packageName, instrumentArgs); + Map<String, Object> result = impl.instrument(packageName, instrumentArgs); return JythonUtils.convertMapToDict(result); } @@ -377,34 +370,6 @@ public abstract class MonkeyDevice extends PyObject implements ClassDictInit { ArgParser ap = JythonUtils.createArgParser(args, kws); Preconditions.checkNotNull(ap); - wake(); + impl.wake(); } - - /** - * Reboot the device. - * - * @param into which bootloader to boot into. Null means default reboot. - */ - public abstract void reboot(@Nullable String into); - - public abstract String getProperty(String key); - public abstract String getSystemProperty(String key); - public abstract void touch(int x, int y, TouchPressType type); - public abstract void press(String keyName, TouchPressType type); - public abstract void drag(int startx, int starty, int endx, int endy, int steps, long ms); - public abstract void type(String string); - public abstract String shell(String cmd); - public abstract boolean installPackage(String path); - public abstract boolean removePackage(String packageName); - public abstract void startActivity(@Nullable String uri, @Nullable String action, - @Nullable String data, @Nullable String mimetype, - Collection<String> categories, Map<String, Object> extras, @Nullable String component, - int flags); - public abstract void broadcastIntent(@Nullable String uri, @Nullable String action, - @Nullable String data, @Nullable String mimetype, - Collection<String> categories, Map<String, Object> extras, @Nullable String component, - int flags); - public abstract Map<String, Object> instrument(String packageName, - Map<String, Object> args); - public abstract void wake(); } diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyImage.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyImage.java index 70201ee..b55b4f3 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/MonkeyImage.java +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyImage.java @@ -17,6 +17,7 @@ package com.android.monkeyrunner; import com.google.common.base.Preconditions; +import com.android.monkeyrunner.core.IMonkeyImage; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import org.python.core.ArgParser; @@ -25,61 +26,31 @@ import org.python.core.PyInteger; import org.python.core.PyObject; import org.python.core.PyTuple; -import java.awt.Graphics; import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.util.Iterator; -import java.util.logging.Level; import java.util.logging.Logger; -import javax.imageio.ImageIO; -import javax.imageio.ImageWriter; -import javax.imageio.stream.ImageOutputStream; - /** * Jython object to encapsulate images that have been taken. */ @MonkeyRunnerExported(doc = "An image") -public abstract class MonkeyImage extends PyObject implements ClassDictInit { +public class MonkeyImage extends PyObject implements ClassDictInit { private static Logger LOG = Logger.getLogger(MonkeyImage.class.getCanonicalName()); public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(MonkeyImage.class, dict); } - /** - * Convert the MonkeyImage into a BufferedImage. - * - * @return a BufferedImage for this MonkeyImage. - */ - public abstract BufferedImage createBufferedImage(); - - // Cache the BufferedImage so we don't have to generate it every time. - private WeakReference<BufferedImage> cachedBufferedImage = null; + private IMonkeyImage impl; - /** - * Utility method to handle getting the BufferedImage and managing the cache. - * - * @return the BufferedImage for this image. - */ - private BufferedImage getBufferedImage() { - // Check the cache first - if (cachedBufferedImage != null) { - BufferedImage img = cachedBufferedImage.get(); - if (img != null) { - return img; - } - } + public MonkeyImage(IMonkeyImage impl) { + this.impl = impl; + } - // Not in the cache, so create it and cache it. - BufferedImage img = createBufferedImage(); - cachedBufferedImage = new WeakReference<BufferedImage>(img); - return img; + public IMonkeyImage getImpl() { + return impl; } + @MonkeyRunnerExported(doc = "Converts the MonkeyImage into a particular format and returns " + "the result as a String. Use this to get access to the raw" + "pixels in a particular format. String output is for better " + @@ -93,16 +64,7 @@ public abstract class MonkeyImage extends PyObject implements ClassDictInit { Preconditions.checkNotNull(ap); String format = ap.getString(0, "png"); - - BufferedImage argb = convertSnapshot(); - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - try { - ImageIO.write(argb, format, os); - } catch (IOException e) { - return new byte[0]; - } - return os.toByteArray(); + return impl.convertToBytes(format); } @MonkeyRunnerExported(doc = "Write the MonkeyImage to a file. If no " + @@ -120,38 +82,7 @@ public abstract class MonkeyImage extends PyObject implements ClassDictInit { String path = ap.getString(0); String format = ap.getString(1, null); - - if (format != null) { - return writeToFile(path, format); - } - int offset = path.lastIndexOf('.'); - if (offset < 0) { - return writeToFile(path, "png"); - } - String ext = path.substring(offset + 1); - Iterator<ImageWriter> writers = ImageIO.getImageWritersBySuffix(ext); - if (!writers.hasNext()) { - return writeToFile(path, "png"); - } - ImageWriter writer = writers.next(); - BufferedImage image = convertSnapshot(); - try { - File f = new File(path); - f.delete(); - - ImageOutputStream outputStream = ImageIO.createImageOutputStream(f); - writer.setOutput(outputStream); - - try { - writer.write(image); - } finally { - writer.dispose(); - outputStream.flush(); - } - } catch (IOException e) { - return false; - } - return true; + return impl.writeToFile(path, format); } @MonkeyRunnerExported(doc = "Get a single ARGB (alpha, red, green, blue) pixel at location " + @@ -167,7 +98,7 @@ public abstract class MonkeyImage extends PyObject implements ClassDictInit { int x = ap.getInt(0); int y = ap.getInt(1); - int pixel = getPixel(x, y); + int pixel = impl.getPixel(x, y); PyInteger a = new PyInteger((pixel & 0xFF000000) >> 24); PyInteger r = new PyInteger((pixel & 0x00FF0000) >> 16); PyInteger g = new PyInteger((pixel & 0x0000FF00) >> 8); @@ -188,35 +119,7 @@ public abstract class MonkeyImage extends PyObject implements ClassDictInit { int x = ap.getInt(0); int y = ap.getInt(1); - return getPixel(x, y); - } - - private int getPixel(int x, int y) { - BufferedImage image = getBufferedImage(); - return image.getRGB(x, y); - } - - private BufferedImage convertSnapshot() { - BufferedImage image = getBufferedImage(); - - // Convert the image to ARGB so ImageIO writes it out nicely - BufferedImage argb = new BufferedImage(image.getWidth(), image.getHeight(), - BufferedImage.TYPE_INT_ARGB); - Graphics g = argb.createGraphics(); - g.drawImage(image, 0, 0, null); - g.dispose(); - return argb; - } - - public boolean writeToFile(String path, String format) { - BufferedImage argb = convertSnapshot(); - - try { - ImageIO.write(argb, format, new File(path)); - } catch (IOException e) { - return false; - } - return true; + return impl.getPixel(x, y); } @MonkeyRunnerExported(doc = "Compare this MonkeyImage object to aother MonkeyImage object.", @@ -231,72 +134,13 @@ public abstract class MonkeyImage extends PyObject implements ClassDictInit { Preconditions.checkNotNull(ap); PyObject otherObject = ap.getPyObject(0); - MonkeyImage other = (MonkeyImage) otherObject.__tojava__(MonkeyImage.class); + // TODO: check if this conversion wortks + IMonkeyImage other = (IMonkeyImage) otherObject.__tojava__( + IMonkeyImage.class); double percent = JythonUtils.getFloat(ap, 1, 1.0); - BufferedImage otherImage = other.getBufferedImage(); - BufferedImage myImage = getBufferedImage(); - - // Easy size check - if (otherImage.getWidth() != myImage.getWidth()) { - return false; - } - if (otherImage.getHeight() != myImage.getHeight()) { - return false; - } - - int[] otherPixel = new int[1]; - int[] myPixel = new int[1]; - - int width = myImage.getWidth(); - int height = myImage.getHeight(); - - int numDiffPixels = 0; - // Now, go through pixel-by-pixel and check that the images are the same; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - if (myImage.getRGB(x, y) != otherImage.getRGB(x, y)) { - numDiffPixels++; - } - } - } - double numberPixels = (height * width); - double diffPercent = numDiffPixels / numberPixels; - return percent <= 1.0 - diffPercent; - } - - private static class BufferedImageMonkeyImage extends MonkeyImage { - private final BufferedImage image; - - public BufferedImageMonkeyImage(BufferedImage image) { - this.image = image; - } - - @Override - public BufferedImage createBufferedImage() { - return image; - } - } - - /* package */ static MonkeyImage loadImageFromFile(String path) { - File f = new File(path); - if (f.exists() && f.canRead()) { - try { - BufferedImage bufferedImage = ImageIO.read(new File(path)); - if (bufferedImage == null) { - LOG.log(Level.WARNING, "Cannot decode file %s", path); - return null; - } - return new BufferedImageMonkeyImage(bufferedImage); - } catch (IOException e) { - LOG.log(Level.WARNING, "Exception trying to decode image", e); - return null; - } - } else { - LOG.log(Level.WARNING, "Cannot read file %s", path); - return null; - } + return impl.sameAs(other, percent); } @MonkeyRunnerExported(doc = "Copy a rectangular region of the image.", @@ -315,7 +159,7 @@ public abstract class MonkeyImage extends PyObject implements ClassDictInit { int w = rect.__getitem__(2).asInt(); int h = rect.__getitem__(3).asInt(); - BufferedImage image = getBufferedImage(); - return new BufferedImageMonkeyImage(image.getSubimage(x, y, w, h)); + IMonkeyImage image = impl.getSubImage(x, y, w, h); + return new MonkeyImage(image); } }
\ No newline at end of file diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java index 5f137cd..5529802 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunner.java @@ -19,6 +19,10 @@ import com.google.common.base.Functions; import com.google.common.base.Preconditions; import com.google.common.collect.Collections2; +import com.android.monkeyrunner.core.IMonkeyBackend; +import com.android.monkeyrunner.core.IMonkeyDevice; +import com.android.monkeyrunner.core.IMonkeyImage; +import com.android.monkeyrunner.core.MonkeyImageBase; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import org.python.core.ArgParser; @@ -38,7 +42,7 @@ import javax.swing.JOptionPane; @MonkeyRunnerExported(doc = "Main entry point for MonkeyRunner") public class MonkeyRunner extends PyObject implements ClassDictInit { private static final Logger LOG = Logger.getLogger(MonkeyRunner.class.getCanonicalName()); - private static MonkeyRunnerBackend backend; + private static IMonkeyBackend backend; public static void classDictInit(PyObject dict) { JythonUtils.convertDocAnnotationsForClass(MonkeyRunner.class, dict); @@ -49,7 +53,7 @@ public class MonkeyRunner extends PyObject implements ClassDictInit { * * @param backend the backend to use. */ - /* package */ static void setBackend(MonkeyRunnerBackend backend) { + /* package */ static void setBackend(IMonkeyBackend backend) { MonkeyRunner.backend = backend; } @@ -71,8 +75,10 @@ public class MonkeyRunner extends PyObject implements ClassDictInit { timeoutMs = Long.MAX_VALUE; } - return backend.waitForConnection(timeoutMs, + IMonkeyDevice device = backend.waitForConnection(timeoutMs, ap.getString(1, ".*")); + MonkeyDevice monkeyDevice = new MonkeyDevice(device); + return monkeyDevice; } @MonkeyRunnerExported(doc = "Pause the currently running program for the specified " + @@ -185,8 +191,8 @@ public class MonkeyRunner extends PyObject implements ClassDictInit { Preconditions.checkNotNull(ap); String path = ap.getString(0); - - return MonkeyImage.loadImageFromFile(path); + IMonkeyImage image = MonkeyImageBase.loadImageFromFile(path); + return new MonkeyImage(image); } /** diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java index 5244dbc..8c9942c 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java +++ b/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerStarter.java @@ -20,6 +20,7 @@ import com.google.common.base.Predicates; import com.google.common.collect.ImmutableMap; import com.android.monkeyrunner.adb.AdbBackend; +import com.android.monkeyrunner.core.IMonkeyBackend; import com.android.monkeyrunner.stub.StubBackend; import org.python.util.PythonInterpreter; @@ -50,7 +51,7 @@ public class MonkeyRunnerStarter { private static final Logger LOG = Logger.getLogger(MonkeyRunnerStarter.class.getName()); private static final String MONKEY_RUNNER_MAIN_MANIFEST_NAME = "MonkeyRunnerStartupRunner"; - private final MonkeyRunnerBackend backend; + private final IMonkeyBackend backend; private final MonkeyRunnerOptions options; public MonkeyRunnerStarter(MonkeyRunnerOptions options) { @@ -68,7 +69,7 @@ public class MonkeyRunnerStarter { * @param backendName the name of the backend to create * @return the new backend, or null if none were found. */ - public static MonkeyRunnerBackend createBackendByName(String backendName) { + public static IMonkeyBackend createBackendByName(String backendName) { if ("adb".equals(backendName)) { return new AdbBackend(); } else if ("stub".equals(backendName)) { diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbBackend.java b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbBackend.java index 455d131..49cac08 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbBackend.java +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbBackend.java @@ -19,8 +19,8 @@ import com.google.common.collect.Lists; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.IDevice; -import com.android.monkeyrunner.MonkeyDevice; -import com.android.monkeyrunner.MonkeyRunnerBackend; +import com.android.monkeyrunner.core.IMonkeyBackend; +import com.android.monkeyrunner.core.IMonkeyDevice; import com.android.sdklib.SdkConstants; import java.io.File; @@ -32,12 +32,11 @@ import java.util.regex.Pattern; /** * Backend implementation that works over ADB to talk to the device. */ -public class AdbBackend implements MonkeyRunnerBackend { +public class AdbBackend implements IMonkeyBackend { private static Logger LOG = Logger.getLogger(AdbBackend.class.getCanonicalName()); // How long to wait each time we check for the device to be connected. private static final int CONNECTION_ITERATION_TIMEOUT_MS = 200; - private final List<AdbMonkeyDevice> devices = Lists.newArrayList(); - + private final List<IMonkeyDevice> devices = Lists.newArrayList(); private final AndroidDebugBridge bridge; public AdbBackend() { @@ -87,18 +86,20 @@ public class AdbBackend implements MonkeyRunnerBackend { return null; } - public MonkeyDevice waitForConnection() { + @Override + public IMonkeyDevice waitForConnection() { return waitForConnection(Integer.MAX_VALUE, ".*"); } - public MonkeyDevice waitForConnection(long timeoutMs, String deviceIdRegex) { + @Override + public IMonkeyDevice waitForConnection(long timeoutMs, String deviceIdRegex) { do { IDevice device = findAttacedDevice(deviceIdRegex); // Only return the device when it is online if (device != null && device.getState() == IDevice.DeviceState.ONLINE) { - AdbMonkeyDevice amd = new AdbMonkeyDevice(device); - devices.add(amd); - return amd; + IMonkeyDevice monkeyDevice = new AdbMonkeyDevice(device); + devices.add(monkeyDevice); + return monkeyDevice; } try { @@ -113,8 +114,9 @@ public class AdbBackend implements MonkeyRunnerBackend { return null; } + @Override public void shutdown() { - for (AdbMonkeyDevice device : devices) { + for (IMonkeyDevice device : devices) { device.dispose(); } AndroidDebugBridge.terminate(); diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java index 614e656..60eaba9 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyDevice.java @@ -25,10 +25,10 @@ import com.android.ddmlib.IDevice; import com.android.ddmlib.InstallException; import com.android.ddmlib.ShellCommandUnresponsiveException; import com.android.ddmlib.TimeoutException; -import com.android.monkeyrunner.MonkeyDevice; -import com.android.monkeyrunner.MonkeyImage; import com.android.monkeyrunner.MonkeyManager; import com.android.monkeyrunner.adb.LinearInterpolator.Point; +import com.android.monkeyrunner.core.IMonkeyImage; +import com.android.monkeyrunner.core.IMonkeyDevice; import com.android.monkeyrunner.easy.HierarchyViewer; import java.io.IOException; @@ -48,7 +48,7 @@ import java.util.regex.Pattern; import javax.annotation.Nullable; -public class AdbMonkeyDevice extends MonkeyDevice { +public class AdbMonkeyDevice implements IMonkeyDevice { private static final Logger LOG = Logger.getLogger(AdbMonkeyDevice.class.getName()); private static final String[] ZERO_LENGTH_STRING_ARRAY = new String[0]; @@ -90,6 +90,7 @@ public class AdbMonkeyDevice extends MonkeyDevice { private void executeAsyncCommand(final String command, final LoggingOutputReceiver logger) { executor.submit(new Runnable() { + @Override public void run() { try { device.executeShellCommand(command, logger); @@ -190,7 +191,7 @@ public class AdbMonkeyDevice extends MonkeyDevice { } @Override - public MonkeyImage takeSnapshot() { + public IMonkeyImage takeSnapshot() { try { return new AdbMonkeyImage(device.getScreenshot()); } catch (TimeoutException e) { @@ -505,6 +506,7 @@ public class AdbMonkeyDevice extends MonkeyDevice { LinearInterpolator.Point start = new LinearInterpolator.Point(startx, starty); LinearInterpolator.Point end = new LinearInterpolator.Point(endx, endy); lerp.interpolate(start, end, new LinearInterpolator.Callback() { + @Override public void step(Point point) { try { manager.touchMove(point.getX(), point.getY()); @@ -519,6 +521,7 @@ public class AdbMonkeyDevice extends MonkeyDevice { } } + @Override public void start(Point point) { try { manager.touchDown(point.getX(), point.getY()); @@ -534,6 +537,7 @@ public class AdbMonkeyDevice extends MonkeyDevice { } } + @Override public void end(Point point) { try { manager.touchMove(point.getX(), point.getY()); diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyImage.java b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyImage.java index fc32600..e2bd86e 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyImage.java +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/AdbMonkeyImage.java @@ -16,15 +16,15 @@ package com.android.monkeyrunner.adb; import com.android.ddmlib.RawImage; -import com.android.monkeyrunner.MonkeyImage; import com.android.monkeyrunner.adb.image.ImageUtils; +import com.android.monkeyrunner.core.MonkeyImageBase; import java.awt.image.BufferedImage; /** * ADB implementation of the MonkeyImage class. */ -public class AdbMonkeyImage extends MonkeyImage { +public class AdbMonkeyImage extends MonkeyImageBase { private final RawImage image; /** diff --git a/monkeyrunner/src/com/android/monkeyrunner/adb/image/CaptureRawAndConvertedImage.java b/monkeyrunner/src/com/android/monkeyrunner/adb/image/CaptureRawAndConvertedImage.java index 7e31ea5..5a317f1 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/adb/image/CaptureRawAndConvertedImage.java +++ b/monkeyrunner/src/com/android/monkeyrunner/adb/image/CaptureRawAndConvertedImage.java @@ -16,9 +16,11 @@ package com.android.monkeyrunner.adb.image; import com.android.ddmlib.RawImage; -import com.android.monkeyrunner.MonkeyDevice; import com.android.monkeyrunner.adb.AdbBackend; import com.android.monkeyrunner.adb.AdbMonkeyImage; +import com.android.monkeyrunner.core.IMonkeyBackend; +import com.android.monkeyrunner.core.IMonkeyImage; +import com.android.monkeyrunner.core.IMonkeyDevice; import java.io.FileOutputStream; import java.io.IOException; @@ -94,13 +96,13 @@ public class CaptureRawAndConvertedImage { } public static void main(String[] args) throws IOException { - AdbBackend backend = new AdbBackend(); - MonkeyDevice device = backend.waitForConnection(); - AdbMonkeyImage snapshot = (AdbMonkeyImage) device.takeSnapshot(); + IMonkeyBackend backend = new AdbBackend(); + IMonkeyDevice device = backend.waitForConnection(); + IMonkeyImage snapshot = (IMonkeyImage) device.takeSnapshot(); // write out to a file snapshot.writeToFile("output.png", "png"); - writeOutImage(snapshot.getRawImage(), "output.raw"); + writeOutImage(((AdbMonkeyImage)snapshot).getRawImage(), "output.raw"); System.exit(0); } } diff --git a/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyController.java b/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyController.java index e199a75..ca3195c 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyController.java +++ b/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyController.java @@ -15,8 +15,9 @@ */ package com.android.monkeyrunner.controller; -import com.android.monkeyrunner.MonkeyDevice; import com.android.monkeyrunner.adb.AdbBackend; +import com.android.monkeyrunner.core.IMonkeyBackend; +import com.android.monkeyrunner.core.IMonkeyDevice; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; @@ -36,9 +37,10 @@ public class MonkeyController extends JFrame { public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { + @Override public void run() { - AdbBackend adb = new AdbBackend(); - final MonkeyDevice device = adb.waitForConnection(); + IMonkeyBackend adb = new AdbBackend(); + final IMonkeyDevice device = adb.waitForConnection(); MonkeyControllerFrame mf = new MonkeyControllerFrame(device); mf.setVisible(true); mf.addWindowListener(new WindowAdapter() { diff --git a/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyControllerFrame.java b/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyControllerFrame.java index 7f5a7d8..7750936 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyControllerFrame.java +++ b/monkeyrunner/src/com/android/monkeyrunner/controller/MonkeyControllerFrame.java @@ -15,10 +15,10 @@ */ package com.android.monkeyrunner.controller; -import com.android.monkeyrunner.MonkeyDevice; -import com.android.monkeyrunner.MonkeyImage; import com.android.monkeyrunner.MonkeyManager; import com.android.monkeyrunner.PhysicalButton; +import com.android.monkeyrunner.core.IMonkeyImage; +import com.android.monkeyrunner.core.IMonkeyDevice; import java.awt.KeyEventDispatcher; import java.awt.KeyboardFocusManager; @@ -61,7 +61,7 @@ public class MonkeyControllerFrame extends JFrame { } }); - private final MonkeyDevice device; + private final IMonkeyDevice device; private class PressAction extends AbstractAction { private final PhysicalButton button; @@ -85,7 +85,7 @@ public class MonkeyControllerFrame extends JFrame { return button; } - public MonkeyControllerFrame(MonkeyDevice device) { + public MonkeyControllerFrame(IMonkeyDevice device) { super("MonkeyController"); this.device = device; @@ -155,7 +155,7 @@ public class MonkeyControllerFrame extends JFrame { } private void updateScreen() { - MonkeyImage snapshot = device.takeSnapshot(); + IMonkeyImage snapshot = device.takeSnapshot(); currentImage = snapshot.createBufferedImage(); imageLabel.setIcon(new ImageIcon(currentImage)); diff --git a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerBackend.java b/monkeyrunner/src/com/android/monkeyrunner/core/IMonkeyBackend.java index 216d214..3c1b943 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/MonkeyRunnerBackend.java +++ b/monkeyrunner/src/com/android/monkeyrunner/core/IMonkeyBackend.java @@ -13,13 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.monkeyrunner; +package com.android.monkeyrunner.core; + +import com.android.monkeyrunner.MonkeyDevice; /** * Interface between MonkeyRunner common code and the MonkeyRunner backend. The backend is * responsible for communicating between the host and the device. */ -public interface MonkeyRunnerBackend { +public interface IMonkeyBackend { + /** + * Wait for a default device to connect to the backend. + * + * @return the connected device (or null if timeout); + */ + IMonkeyDevice waitForConnection(); + /** * Wait for a device to connect to the backend. * @@ -27,7 +36,7 @@ public interface MonkeyRunnerBackend { * @param deviceIdRegex the regular expression to specify which device to wait for. * @return the connected device (or null if timeout); */ - MonkeyDevice waitForConnection(long timeoutMs, String deviceIdRegex); + IMonkeyDevice waitForConnection(long timeoutMs, String deviceIdRegex); /** * Shutdown the backend and cleanup any resources it was using. diff --git a/monkeyrunner/src/com/android/monkeyrunner/core/IMonkeyDevice.java b/monkeyrunner/src/com/android/monkeyrunner/core/IMonkeyDevice.java new file mode 100644 index 0000000..c081a56 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/core/IMonkeyDevice.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2011 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.monkeyrunner.core; + +import com.android.monkeyrunner.MonkeyManager; +import com.android.monkeyrunner.easy.HierarchyViewer; + +import java.util.Collection; +import java.util.Map; + +import javax.annotation.Nullable; + +/** + * MonkeyDevice interface. + */ +public interface IMonkeyDevice { + enum TouchPressType { + DOWN, UP, DOWN_AND_UP, + } + + /** + * Create a MonkeyMananger for talking to this device. + * + * @return the MonkeyManager + */ + MonkeyManager getManager(); + + /** + * Dispose of any native resources this device may have taken hold of. + */ + void dispose(); + + /** + * @return hierarchy viewer implementation for querying state of the view + * hierarchy. + */ + HierarchyViewer getHierarchyViewer(); + + /** + * Take the current screen's snapshot. + * @return the snapshot image + */ + IMonkeyImage takeSnapshot(); + + /** + * Reboot the device. + * + * @param into which bootloader to boot into. Null means default reboot. + */ + void reboot(@Nullable String into); + + /** + * Get device's property. + * + * @param key the property name + * @return the property value + */ + String getProperty(String key); + + /** + * Get system property. + * + * @param key the name of the system property + * @return the property value + */ + String getSystemProperty(String key); + + /** + * Perform a touch of the given type at (x,y). + * + * @param x the x coordinate + * @param y the y coordinate + * @param type the touch type + */ + void touch(int x, int y, TouchPressType type); + + /** + * Perform a press of a given type using a given key. + * + * TODO: define standard key names in a separate class or enum + * + * @param keyName the name of the key to use + * @param type the type of press to perform + */ + void press(String keyName, TouchPressType type); + + /** + * Perform a drag from one one location to another + * + * @param startx the x coordinate of the drag's starting point + * @param starty the y coordinate of the drag's starting point + * @param endx the x coordinate of the drag's end point + * @param endy the y coordinate of the drag's end point + * @param steps the number of steps to take when interpolating points + * @param ms the duration of the drag + */ + void drag(int startx, int starty, int endx, int endy, int steps, long ms); + + /** + * Type a given string. + * + * @param string the string to type + */ + void type(String string); + + /** + * Execute a shell command. + * + * @param cmd the command to execute + * @return the output of the command + */ + String shell(String cmd); + + /** + * Install a given package. + * + * @param path the path to the installation package + * @return true if success + */ + boolean installPackage(String path); + + /** + * Uninstall a given package. + * + * @param packageName the name of the package + * @return true if success + */ + boolean removePackage(String packageName); + + /** + * Start an activity. + * + * @param uri the URI for the Intent + * @param action the action for the Intent + * @param data the data URI for the Intent + * @param mimeType the mime type for the Intent + * @param categories the category names for the Intent + * @param extras the extras to add to the Intent + * @param component the component of the Intent + * @param flags the flags for the Intent + */ + void startActivity(@Nullable String uri, @Nullable String action, + @Nullable String data, @Nullable String mimeType, + Collection<String> categories, Map<String, Object> extras, @Nullable String component, + int flags); + + /** + * Send a broadcast intent to the device. + * + * @param uri the URI for the Intent + * @param action the action for the Intent + * @param data the data URI for the Intent + * @param mimeType the mime type for the Intent + * @param categories the category names for the Intent + * @param extras the extras to add to the Intent + * @param component the component of the Intent + * @param flags the flags for the Intent + */ + void broadcastIntent(@Nullable String uri, @Nullable String action, + @Nullable String data, @Nullable String mimeType, + Collection<String> categories, Map<String, Object> extras, @Nullable String component, + int flags); + + /** + * Run the specified package with instrumentation and return the output it + * generates. + * + * Use this to run a test package using InstrumentationTestRunner. + * + * @param packageName The class to run with instrumentation. The format is + * packageName/className. Use packageName to specify the Android package to + * run, and className to specify the class to run within that package. For + * test packages, this is usually testPackageName/InstrumentationTestRunner + * @param args a map of strings to objects containing the arguments to pass + * to this instrumentation. + * @return A map of strings to objects for the output from the package. + * For a test package, contains a single key-value pair: the key is 'stream' + * and the value is a string containing the test output. + */ + Map<String, Object> instrument(String packageName, + Map<String, Object> args); + + /** + * Wake up the screen on the device. + */ + void wake(); +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/core/IMonkeyImage.java b/monkeyrunner/src/com/android/monkeyrunner/core/IMonkeyImage.java new file mode 100644 index 0000000..5a24fa7 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/core/IMonkeyImage.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 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.monkeyrunner.core; + +import java.awt.image.BufferedImage; + +/** + * MonkeyImage interface. + * + * This interface defines an image representing a screen snapshot. + */ +public interface IMonkeyImage { + // TODO: add java docs + BufferedImage createBufferedImage(); + BufferedImage getBufferedImage(); + + IMonkeyImage getSubImage(int x, int y, int w, int h); + + byte[] convertToBytes(String format); + boolean writeToFile(String path, String format); + int getPixel(int x, int y); + boolean sameAs(IMonkeyImage other, double percent); +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/core/MonkeyImageBase.java b/monkeyrunner/src/com/android/monkeyrunner/core/MonkeyImageBase.java new file mode 100644 index 0000000..04ccb93 --- /dev/null +++ b/monkeyrunner/src/com/android/monkeyrunner/core/MonkeyImageBase.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2011 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.monkeyrunner.core; + +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.Iterator; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.imageio.ImageIO; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageOutputStream; + +/** + * Base class with basic functionality for MonkeyImage implementations. + */ +public abstract class MonkeyImageBase implements IMonkeyImage { + private static Logger LOG = Logger.getLogger(MonkeyImageBase.class.getCanonicalName()); + + /** + * Convert the MonkeyImage to a BufferedImage. + * + * @return a BufferedImage for this MonkeyImage. + */ + @Override + public abstract BufferedImage createBufferedImage(); + + // Cache the BufferedImage so we don't have to generate it every time. + private WeakReference<BufferedImage> cachedBufferedImage = null; + + /** + * Utility method to handle getting the BufferedImage and managing the cache. + * + * @return the BufferedImage for this image. + */ + @Override + public BufferedImage getBufferedImage() { + // Check the cache first + if (cachedBufferedImage != null) { + BufferedImage img = cachedBufferedImage.get(); + if (img != null) { + return img; + } + } + + // Not in the cache, so create it and cache it. + BufferedImage img = createBufferedImage(); + cachedBufferedImage = new WeakReference<BufferedImage>(img); + return img; + } + + @Override + public byte[] convertToBytes(String format) { + BufferedImage argb = convertSnapshot(); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + ImageIO.write(argb, format, os); + } catch (IOException e) { + return new byte[0]; + } + return os.toByteArray(); + } + + @Override + public boolean writeToFile(String path, String format) { + if (format != null) { + return writeToFileHelper(path, format); + } + int offset = path.lastIndexOf('.'); + if (offset < 0) { + return writeToFileHelper(path, "png"); + } + String ext = path.substring(offset + 1); + Iterator<ImageWriter> writers = ImageIO.getImageWritersBySuffix(ext); + if (!writers.hasNext()) { + return writeToFileHelper(path, "png"); + } + ImageWriter writer = writers.next(); + BufferedImage image = convertSnapshot(); + try { + File f = new File(path); + f.delete(); + + ImageOutputStream outputStream = ImageIO.createImageOutputStream(f); + writer.setOutput(outputStream); + + try { + writer.write(image); + } finally { + writer.dispose(); + outputStream.flush(); + } + } catch (IOException e) { + return false; + } + return true; + } + + @Override + public int getPixel(int x, int y) { + BufferedImage image = getBufferedImage(); + return image.getRGB(x, y); + } + + private BufferedImage convertSnapshot() { + BufferedImage image = getBufferedImage(); + + // Convert the image to ARGB so ImageIO writes it out nicely + BufferedImage argb = new BufferedImage(image.getWidth(), image.getHeight(), + BufferedImage.TYPE_INT_ARGB); + Graphics g = argb.createGraphics(); + g.drawImage(image, 0, 0, null); + g.dispose(); + return argb; + } + + private boolean writeToFileHelper(String path, String format) { + BufferedImage argb = convertSnapshot(); + + try { + ImageIO.write(argb, format, new File(path)); + } catch (IOException e) { + return false; + } + return true; + } + + @Override + public boolean sameAs(IMonkeyImage other, double percent) { + BufferedImage otherImage = other.getBufferedImage(); + BufferedImage myImage = getBufferedImage(); + + // Easy size check + if (otherImage.getWidth() != myImage.getWidth()) { + return false; + } + if (otherImage.getHeight() != myImage.getHeight()) { + return false; + } + + int[] otherPixel = new int[1]; + int[] myPixel = new int[1]; + + int width = myImage.getWidth(); + int height = myImage.getHeight(); + + int numDiffPixels = 0; + // Now, go through pixel-by-pixel and check that the images are the same; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (myImage.getRGB(x, y) != otherImage.getRGB(x, y)) { + numDiffPixels++; + } + } + } + double numberPixels = (height * width); + double diffPercent = numDiffPixels / numberPixels; + return percent <= 1.0 - diffPercent; + } + + // TODO: figure out the location of this class and is superclasses + private static class BufferedImageMonkeyImage extends MonkeyImageBase { + private final BufferedImage image; + + public BufferedImageMonkeyImage(BufferedImage image) { + this.image = image; + } + + @Override + public BufferedImage createBufferedImage() { + return image; + } + } + + public static IMonkeyImage loadImageFromFile(String path) { + File f = new File(path); + if (f.exists() && f.canRead()) { + try { + BufferedImage bufferedImage = ImageIO.read(new File(path)); + if (bufferedImage == null) { + LOG.log(Level.WARNING, "Cannot decode file %s", path); + return null; + } + return new BufferedImageMonkeyImage(bufferedImage); + } catch (IOException e) { + LOG.log(Level.WARNING, "Exception trying to decode image", e); + return null; + } + } else { + LOG.log(Level.WARNING, "Cannot read file %s", path); + return null; + } + } + + @Override + public IMonkeyImage getSubImage(int x, int y, int w, int h) { + BufferedImage image = getBufferedImage(); + return new BufferedImageMonkeyImage(image.getSubimage(x, y, w, h)); + } +} diff --git a/monkeyrunner/src/com/android/monkeyrunner/easy/By.java b/monkeyrunner/src/com/android/monkeyrunner/easy/By.java index a8be6c0..1ed1c6f 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/easy/By.java +++ b/monkeyrunner/src/com/android/monkeyrunner/easy/By.java @@ -39,6 +39,7 @@ import org.python.core.PyObject; * without notice. * * TODO: Implement other selectors, like classid, hash, and so on. + * TODO: separate java-only core from jython wrapper */ public class By extends PyObject implements ClassDictInit { public static void classDictInit(PyObject dict) { @@ -62,6 +63,10 @@ public class By extends PyObject implements ClassDictInit { return new By(id); } + public static By id(String id) { + return new By(id); + } + /** * Find the selected view from the root view node. */ diff --git a/monkeyrunner/src/com/android/monkeyrunner/easy/EasyMonkeyDevice.java b/monkeyrunner/src/com/android/monkeyrunner/easy/EasyMonkeyDevice.java index 8e6ec0f..e72e462 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/easy/EasyMonkeyDevice.java +++ b/monkeyrunner/src/com/android/monkeyrunner/easy/EasyMonkeyDevice.java @@ -22,7 +22,7 @@ import com.android.hierarchyviewerlib.device.ViewNode; import com.android.hierarchyviewerlib.device.ViewNode.Property; import com.android.monkeyrunner.JythonUtils; import com.android.monkeyrunner.MonkeyDevice; -import com.android.monkeyrunner.MonkeyDevice.TouchPressType; +import com.android.monkeyrunner.core.IMonkeyDevice.TouchPressType; import com.android.monkeyrunner.doc.MonkeyRunnerExported; import org.eclipse.swt.graphics.Point; @@ -32,6 +32,7 @@ import org.python.core.Py; import org.python.core.PyException; import org.python.core.PyInteger; import org.python.core.PyObject; +import org.python.core.PyString; import org.python.core.PyTuple; import java.util.Set; @@ -61,7 +62,7 @@ public class EasyMonkeyDevice extends PyObject implements ClassDictInit { argDocs = { "MonkeyDevice to extend." }) public EasyMonkeyDevice(MonkeyDevice device) { this.mDevice = device; - this.mHierarchyViewer = device.getHierarchyViewer(); + this.mHierarchyViewer = device.getImpl().getHierarchyViewer(); } @MonkeyRunnerExported(doc = "Sends a touch event to the selected object.", @@ -74,19 +75,16 @@ public class EasyMonkeyDevice extends PyObject implements ClassDictInit { Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); - ViewNode node = mHierarchyViewer.findView(selector); - if (node == null) { - throw new PyException(Py.ValueError, - String.format("View not found: %s", selector)); - } - Point p = HierarchyViewer.getAbsoluteCenterOfView(node); - - PyObject[] otherArgs = new PyObject[3]; - otherArgs[0] = new PyInteger(p.x); - otherArgs[1] = new PyInteger(p.y); - otherArgs[2] = args[1]; + String tmpType = ap.getString(1); + TouchPressType type = MonkeyDevice.TOUCH_NAME_TO_ENUM.get(tmpType); + if (type == null) type = TouchPressType.DOWN_AND_UP; + // TODO: try catch rethrow PyExc + touch(selector, type); + } - mDevice.touch(otherArgs, kws); + public void touch(By selector, TouchPressType type) { + Point p = getElementCenter(selector); + mDevice.getImpl().touch(p.x, p.y, type); } @MonkeyRunnerExported(doc = "Types a string into the specified object.", @@ -100,16 +98,13 @@ public class EasyMonkeyDevice extends PyObject implements ClassDictInit { By selector = getSelector(ap, 0); String text = ap.getString(1); + type(selector, text); + } - ViewNode node = mHierarchyViewer.findView(selector); - if (node == null) { - throw new PyException(Py.ValueError, - String.format("View not found: %s", selector)); - } - - Point p = HierarchyViewer.getAbsoluteCenterOfView(node); - mDevice.touch(p.x, p.y, TouchPressType.DOWN_AND_UP); - mDevice.type(text); + public void type(By selector, String text) { + Point p = getElementCenter(selector); + mDevice.getImpl().touch(p.x, p.y, TouchPressType.DOWN_AND_UP); + mDevice.getImpl().type(text); } @MonkeyRunnerExported(doc = "Locates the coordinates of the selected object.", @@ -141,7 +136,10 @@ public class EasyMonkeyDevice extends PyObject implements ClassDictInit { Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); + return exists(selector); + } + public boolean exists(By selector) { ViewNode node = mHierarchyViewer.findView(selector); return node != null; } @@ -155,13 +153,11 @@ public class EasyMonkeyDevice extends PyObject implements ClassDictInit { Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); + return visible(selector); + } - ViewNode node = mHierarchyViewer.findView(selector); - boolean ret = (node != null) - && node.namedProperties.containsKey("getVisibility()") - && "VISIBLE".equalsIgnoreCase( - node.namedProperties.get("getVisibility()").value); - return ret; + public boolean visible(By selector) { + return mHierarchyViewer.visible(selector); } @MonkeyRunnerExported(doc = "Obtain the text in the selected input box.", @@ -173,21 +169,20 @@ public class EasyMonkeyDevice extends PyObject implements ClassDictInit { Preconditions.checkNotNull(ap); By selector = getSelector(ap, 0); + return getText(selector); + } - ViewNode node = mHierarchyViewer.findView(selector); - if (node == null) { - throw new RuntimeException("Node not found"); - } - Property textProperty = node.namedProperties.get("text:mText"); - if (textProperty == null) { - throw new RuntimeException("No text property on node"); - } - return textProperty.value; + public String getText(By selector) { + return mHierarchyViewer.getText(selector); } @MonkeyRunnerExported(doc = "Gets the id of the focused window.", returns = "The symbolic id of the focused window or None.") public String getFocusedWindowId(PyObject[] args, String[] kws) { + return getFocusedWindowId(); + } + + public String getFocusedWindowId() { return mHierarchyViewer.getFocusedWindowName(); } @@ -210,6 +205,24 @@ public class EasyMonkeyDevice extends PyObject implements ClassDictInit { * @return selector object. */ private By getSelector(ArgParser ap, int i) { - return (By)ap.getPyObject(0).__tojava__(By.class); + return (By)ap.getPyObject(i).__tojava__(By.class); } + + /** + * Get the coordinates of the element's center. + * + * @param selector the element selector + * @return the (x,y) coordinates of the center + */ + private Point getElementCenter(By selector) { + ViewNode node = mHierarchyViewer.findView(selector); + if (node == null) { + throw new PyException(Py.ValueError, + String.format("View not found: %s", selector)); + } + + Point p = HierarchyViewer.getAbsoluteCenterOfView(node); + return p; + } + }
\ No newline at end of file diff --git a/monkeyrunner/src/com/android/monkeyrunner/easy/HierarchyViewer.java b/monkeyrunner/src/com/android/monkeyrunner/easy/HierarchyViewer.java index 5d6911e..450571c 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/easy/HierarchyViewer.java +++ b/monkeyrunner/src/com/android/monkeyrunner/easy/HierarchyViewer.java @@ -121,4 +121,38 @@ public class HierarchyViewer { return new Point( point.x + (node.width / 2), point.y + (node.height / 2)); } + + /** + * Gets the visibility of a given element. + * + * @param selector selector for the view. + * @return True if the element is visible. + */ + public boolean visible(By selector) { + ViewNode node = findView(selector); + boolean ret = (node != null) + && node.namedProperties.containsKey("getVisibility()") + && "VISIBLE".equalsIgnoreCase( + node.namedProperties.get("getVisibility()").value); + return ret; + + } + + /** + * Gets the text of a given element. + * + * @param selector selector for the view. + * @return the text of the given element. + */ + public String getText(By selector) { + ViewNode node = findView(selector); + if (node == null) { + throw new RuntimeException("Node not found"); + } + ViewNode.Property textProperty = node.namedProperties.get("text:mText"); + if (textProperty == null) { + throw new RuntimeException("No text property on node"); + } + return textProperty.value; + } } diff --git a/monkeyrunner/src/com/android/monkeyrunner/recorder/MonkeyRecorder.java b/monkeyrunner/src/com/android/monkeyrunner/recorder/MonkeyRecorder.java index c1a8f7f..914a5b9 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/recorder/MonkeyRecorder.java +++ b/monkeyrunner/src/com/android/monkeyrunner/recorder/MonkeyRecorder.java @@ -15,8 +15,9 @@ */ package com.android.monkeyrunner.recorder; -import com.android.monkeyrunner.MonkeyDevice; import com.android.monkeyrunner.adb.AdbBackend; +import com.android.monkeyrunner.core.IMonkeyBackend; +import com.android.monkeyrunner.core.IMonkeyDevice; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; @@ -44,7 +45,7 @@ public class MonkeyRecorder { * * @param device */ - public static void start(final MonkeyDevice device) { + public static void start(final IMonkeyDevice device) { MonkeyRecorderFrame frame = new MonkeyRecorderFrame(device); // TODO: this is a hack until the window listener works. frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); @@ -69,7 +70,7 @@ public class MonkeyRecorder { } public static void main(String[] args) { - AdbBackend adb = new AdbBackend(); + IMonkeyBackend adb = new AdbBackend(); MonkeyRecorder.start(adb.waitForConnection()); } } diff --git a/monkeyrunner/src/com/android/monkeyrunner/recorder/MonkeyRecorderFrame.java b/monkeyrunner/src/com/android/monkeyrunner/recorder/MonkeyRecorderFrame.java index b6c1f78..88c1e16 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/recorder/MonkeyRecorderFrame.java +++ b/monkeyrunner/src/com/android/monkeyrunner/recorder/MonkeyRecorderFrame.java @@ -16,7 +16,8 @@ package com.android.monkeyrunner.recorder; import com.android.monkeyrunner.MonkeyDevice; -import com.android.monkeyrunner.MonkeyImage; +import com.android.monkeyrunner.core.IMonkeyImage; +import com.android.monkeyrunner.core.IMonkeyDevice; import com.android.monkeyrunner.recorder.actions.Action; import com.android.monkeyrunner.recorder.actions.DragAction; import com.android.monkeyrunner.recorder.actions.DragAction.Direction; @@ -60,7 +61,7 @@ public class MonkeyRecorderFrame extends JFrame { private static final Logger LOG = Logger.getLogger(MonkeyRecorderFrame.class.getName()); - private final MonkeyDevice device; + private final IMonkeyDevice device; private static final long serialVersionUID = 1L; private JPanel jContentPane = null; @@ -83,6 +84,7 @@ public class MonkeyRecorderFrame extends JFrame { private ActionListModel actionListModel; private final Timer refreshTimer = new Timer(1000, new ActionListener() { + @Override public void actionPerformed(ActionEvent e) { refreshDisplay(); // @jve:decl-index=0: } @@ -91,7 +93,7 @@ public class MonkeyRecorderFrame extends JFrame { /** * This is the default constructor */ - public MonkeyRecorderFrame(MonkeyDevice device) { + public MonkeyRecorderFrame(IMonkeyDevice device) { this.device = device; initialize(); } @@ -110,7 +112,7 @@ public class MonkeyRecorderFrame extends JFrame { } private void refreshDisplay() { - MonkeyImage snapshot = device.takeSnapshot(); + IMonkeyImage snapshot = device.takeSnapshot(); currentImage = snapshot.createBufferedImage(); Graphics2D g = scaledImage.createGraphics(); @@ -200,6 +202,7 @@ public class MonkeyRecorderFrame extends JFrame { waitButton = new JButton(); waitButton.setText("Wait"); waitButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { String howLongStr = JOptionPane.showInputDialog("How many seconds to wait?"); if (howLongStr != null) { @@ -222,6 +225,7 @@ public class MonkeyRecorderFrame extends JFrame { pressButton = new JButton(); pressButton.setText("Press a Button"); pressButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { JPanel panel = new JPanel(); JLabel text = new JLabel("What button to press?"); @@ -255,6 +259,7 @@ public class MonkeyRecorderFrame extends JFrame { typeButton = new JButton(); typeButton.setText("Type Something"); typeButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { String whatToType = JOptionPane.showInputDialog("What to type?"); if (whatToType != null) { @@ -276,6 +281,7 @@ public class MonkeyRecorderFrame extends JFrame { flingButton = new JButton(); flingButton.setText("Fling"); flingButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); @@ -358,6 +364,7 @@ public class MonkeyRecorderFrame extends JFrame { exportActionButton = new JButton(); exportActionButton.setText("Export Actions"); exportActionButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent ev) { JFileChooser fc = new JFileChooser(); if (fc.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) { @@ -383,6 +390,7 @@ public class MonkeyRecorderFrame extends JFrame { refreshButton = new JButton(); refreshButton.setText("Refresh Display"); refreshButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { refreshDisplay(); } diff --git a/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/Action.java b/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/Action.java index d582aa4..6fa91ab 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/Action.java +++ b/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/Action.java @@ -15,7 +15,7 @@ */ package com.android.monkeyrunner.recorder.actions; -import com.android.monkeyrunner.MonkeyDevice; +import com.android.monkeyrunner.core.IMonkeyDevice; /** * All actions that can be recorded must implement this interface. @@ -41,5 +41,5 @@ public interface Action { * * @param device the device to execute the action on. */ - void execute(MonkeyDevice device) throws Exception; + void execute(IMonkeyDevice device) throws Exception; } diff --git a/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/DragAction.java b/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/DragAction.java index 082bfe4..2461c0d 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/DragAction.java +++ b/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/DragAction.java @@ -15,7 +15,7 @@ */ package com.android.monkeyrunner.recorder.actions; -import com.android.monkeyrunner.MonkeyDevice; +import com.android.monkeyrunner.core.IMonkeyDevice; /** * Action to drag the "finger" across the device. @@ -77,7 +77,7 @@ public class DragAction implements Action { } @Override - public void execute(MonkeyDevice device) { + public void execute(IMonkeyDevice device) { device.drag(startx, starty, endx, endy, steps, timeMs); } } diff --git a/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/PressAction.java b/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/PressAction.java index a0d9e0e..66a933a 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/PressAction.java +++ b/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/PressAction.java @@ -19,6 +19,7 @@ import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableBiMap; import com.android.monkeyrunner.MonkeyDevice; +import com.android.monkeyrunner.core.IMonkeyDevice; /** * Action to press a certain button. @@ -60,7 +61,7 @@ public class PressAction implements Action { } @Override - public void execute(MonkeyDevice device) { + public void execute(IMonkeyDevice device) { device.press(key, MonkeyDevice.TOUCH_NAME_TO_ENUM.get(downUpFlag)); } diff --git a/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/TouchAction.java b/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/TouchAction.java index 4633edb..4e0ae2d 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/TouchAction.java +++ b/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/TouchAction.java @@ -19,6 +19,7 @@ import com.google.common.collect.BiMap; import com.google.common.collect.ImmutableBiMap; import com.android.monkeyrunner.MonkeyDevice; +import com.android.monkeyrunner.core.IMonkeyDevice; /** * Action to touch the touchscreen at a certain location. @@ -46,7 +47,7 @@ public class TouchAction implements Action { } @Override - public void execute(MonkeyDevice device) throws Exception { + public void execute(IMonkeyDevice device) throws Exception { device.touch(x, y, MonkeyDevice.TOUCH_NAME_TO_ENUM.get(direction)); } diff --git a/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/TypeAction.java b/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/TypeAction.java index 1bfb9e9..78e90b0 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/TypeAction.java +++ b/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/TypeAction.java @@ -15,7 +15,7 @@ */ package com.android.monkeyrunner.recorder.actions; -import com.android.monkeyrunner.MonkeyDevice; +import com.android.monkeyrunner.core.IMonkeyDevice; /** * Action to type in a string on the device. @@ -34,12 +34,13 @@ public class TypeAction implements Action { @Override public String serialize() { - String pydict = PyDictUtilBuilder.newBuilder().add("message", whatToType).build(); + String pydict = PyDictUtilBuilder.newBuilder() + .add("message", whatToType).build(); return "TYPE|" + pydict; } @Override - public void execute(MonkeyDevice device) { + public void execute(IMonkeyDevice device) { device.type(whatToType); } } diff --git a/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/WaitAction.java b/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/WaitAction.java index 9115f9a..bd2d421 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/WaitAction.java +++ b/monkeyrunner/src/com/android/monkeyrunner/recorder/actions/WaitAction.java @@ -15,7 +15,7 @@ */ package com.android.monkeyrunner.recorder.actions; -import com.android.monkeyrunner.MonkeyDevice; +import com.android.monkeyrunner.core.IMonkeyDevice; /** * Action that specifies to wait for a certain amount of time. @@ -27,19 +27,16 @@ public class WaitAction implements Action { this.howLongSeconds = howLongSeconds; } - @Override public String getDisplayName() { return String.format("Wait for %g seconds", this.howLongSeconds); } - @Override public String serialize() { String pydict = PyDictUtilBuilder.newBuilder().add("seconds", howLongSeconds).build(); return "WAIT|" + pydict; } - @Override - public void execute(MonkeyDevice device) throws Exception { + public void execute(IMonkeyDevice device) throws Exception { long ms = (long) (1000.0f * howLongSeconds); Thread.sleep(ms); } diff --git a/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java b/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java index c2fa5f7..b868bf1 100644 --- a/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java +++ b/monkeyrunner/src/com/android/monkeyrunner/stub/StubBackend.java @@ -15,18 +15,22 @@ */ package com.android.monkeyrunner.stub; -import com.android.monkeyrunner.MonkeyDevice; import com.android.monkeyrunner.MonkeyManager; -import com.android.monkeyrunner.MonkeyRunnerBackend; - -public class StubBackend implements MonkeyRunnerBackend { +import com.android.monkeyrunner.core.IMonkeyBackend; +import com.android.monkeyrunner.core.IMonkeyDevice; +public class StubBackend implements IMonkeyBackend { public MonkeyManager createManager(String address, int port) { // TODO Auto-generated method stub return null; } - public MonkeyDevice waitForConnection(long timeout, String deviceId) { + public IMonkeyDevice waitForConnection() { + // TODO Auto-generated method stub + return null; + } + + public IMonkeyDevice waitForConnection(long timeout, String deviceId) { // TODO Auto-generated method stub return null; } diff --git a/ninepatch/tests/Android.mk b/ninepatch/tests/Android.mk index 51a55f0..8a9fd71 100644 --- a/ninepatch/tests/Android.mk +++ b/ninepatch/tests/Android.mk @@ -18,6 +18,7 @@ include $(CLEAR_VARS) # Only compile source java files in this lib. LOCAL_SRC_FILES := $(call all-java-files-under, src) +LOCAL_JAVA_RESOURCE_DIRS := res LOCAL_MODULE := ninepatch-tests LOCAL_MODULE_TAGS := optional diff --git a/ninepatch/tests/src/com/android/ninepatch/button.9.png b/ninepatch/tests/res/com/android/ninepatch/button.9.png Binary files differindex 9d52f40..9d52f40 100644 --- a/ninepatch/tests/src/com/android/ninepatch/button.9.png +++ b/ninepatch/tests/res/com/android/ninepatch/button.9.png |