diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
commit | 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch) | |
tree | d88beb88001f2482911e3d28e43833b50e4b4e97 /tools | |
parent | d83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff) | |
download | frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.zip frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.gz frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'tools')
190 files changed, 42108 insertions, 0 deletions
diff --git a/tools/aapt/AaptAssets.cpp b/tools/aapt/AaptAssets.cpp new file mode 100644 index 0000000..6bc1ee6 --- /dev/null +++ b/tools/aapt/AaptAssets.cpp @@ -0,0 +1,1717 @@ +// +// Copyright 2006 The Android Open Source Project +// + +#include "AaptAssets.h" +#include "Main.h" + +#include <utils/misc.h> +#include <utils/SortedVector.h> + +#include <ctype.h> +#include <dirent.h> +#include <errno.h> + +static const char* kDefaultLocale = "default"; +static const char* kWildcardName = "any"; +static const char* kAssetDir = "assets"; +static const char* kResourceDir = "res"; +static const char* kInvalidChars = "/\\:"; +static const size_t kMaxAssetFileName = 100; + +static const String8 kResString(kResourceDir); + +/* + * Names of asset files must meet the following criteria: + * + * - the filename length must be less than kMaxAssetFileName bytes long + * (and can't be empty) + * - all characters must be 7-bit printable ASCII + * - none of { '/' '\\' ':' } + * + * Pass in just the filename, not the full path. + */ +static bool validateFileName(const char* fileName) +{ + const char* cp = fileName; + size_t len = 0; + + while (*cp != '\0') { + if ((*cp & 0x80) != 0) + return false; // reject high ASCII + if (*cp < 0x20 || *cp >= 0x7f) + return false; // reject control chars and 0x7f + if (strchr(kInvalidChars, *cp) != NULL) + return false; // reject path sep chars + cp++; + len++; + } + + if (len < 1 || len > kMaxAssetFileName) + return false; // reject empty or too long + + return true; +} + +static bool isHidden(const char *root, const char *path) +{ + const char *type = NULL; + + // Skip all hidden files. + if (path[0] == '.') { + // Skip ., .. and .svn but don't chatter about it. + if (strcmp(path, ".") == 0 + || strcmp(path, "..") == 0 + || strcmp(path, ".svn") == 0) { + return true; + } + type = "hidden"; + } else if (path[0] == '_') { + // skip directories starting with _ (don't chatter about it) + String8 subdirName(root); + subdirName.appendPath(path); + if (getFileType(subdirName.string()) == kFileTypeDirectory) { + return true; + } + } else if (strcmp(path, "CVS") == 0) { + // Skip CVS but don't chatter about it. + return true; + } else if (strcasecmp(path, "thumbs.db") == 0 + || strcasecmp(path, "picasa.ini") == 0) { + // Skip suspected image indexes files. + type = "index"; + } else if (path[strlen(path)-1] == '~') { + // Skip suspected emacs backup files. + type = "backup"; + } else { + // Let everything else through. + return false; + } + + /* If we get this far, "type" should be set and the file + * should be skipped. + */ + String8 subdirName(root); + subdirName.appendPath(path); + fprintf(stderr, " (skipping %s %s '%s')\n", type, + getFileType(subdirName.string())==kFileTypeDirectory ? "dir":"file", + subdirName.string()); + + return true; +} + +// ========================================================================= +// ========================================================================= +// ========================================================================= + +status_t +AaptGroupEntry::parseNamePart(const String8& part, int* axis, uint32_t* value) +{ + ResTable_config config; + + // IMSI - MCC + if (getMccName(part.string(), &config)) { + *axis = AXIS_MCC; + *value = config.mcc; + return 0; + } + + // IMSI - MNC + if (getMncName(part.string(), &config)) { + *axis = AXIS_MNC; + *value = config.mnc; + return 0; + } + + // locale - language + if (part.length() == 2 && isalpha(part[0]) && isalpha(part[1])) { + *axis = AXIS_LANGUAGE; + *value = part[1] << 8 | part[0]; + return 0; + } + + // locale - language_REGION + if (part.length() == 5 && isalpha(part[0]) && isalpha(part[1]) + && part[2] == '_' && isalpha(part[3]) && isalpha(part[4])) { + *axis = AXIS_LANGUAGE; + *value = (part[4] << 24) | (part[3] << 16) | (part[1] << 8) | (part[0]); + return 0; + } + + // orientation + if (getOrientationName(part.string(), &config)) { + *axis = AXIS_ORIENTATION; + *value = config.orientation; + return 0; + } + + // density + if (getDensityName(part.string(), &config)) { + *axis = AXIS_DENSITY; + *value = config.density; + return 0; + } + + // touchscreen + if (getTouchscreenName(part.string(), &config)) { + *axis = AXIS_TOUCHSCREEN; + *value = config.touchscreen; + return 0; + } + + // keyboard hidden + if (getKeysHiddenName(part.string(), &config)) { + *axis = AXIS_KEYSHIDDEN; + *value = config.inputFlags; + return 0; + } + + // keyboard + if (getKeyboardName(part.string(), &config)) { + *axis = AXIS_KEYBOARD; + *value = config.keyboard; + return 0; + } + + // navigation + if (getNavigationName(part.string(), &config)) { + *axis = AXIS_NAVIGATION; + *value = config.navigation; + return 0; + } + + // screen size + if (getScreenSizeName(part.string(), &config)) { + *axis = AXIS_SCREENSIZE; + *value = config.screenSize; + return 0; + } + + // version + if (getVersionName(part.string(), &config)) { + *axis = AXIS_VERSION; + *value = config.version; + return 0; + } + + return 1; +} + +bool +AaptGroupEntry::initFromDirName(const char* dir, String8* resType) +{ + Vector<String8> parts; + + String8 mcc, mnc, loc, orient, den, touch, key, keysHidden, nav, size, vers; + + const char *p = dir; + const char *q; + while (NULL != (q = strchr(p, '-'))) { + String8 val(p, q-p); + val.toLower(); + parts.add(val); + //printf("part: %s\n", parts[parts.size()-1].string()); + p = q+1; + } + String8 val(p); + val.toLower(); + parts.add(val); + //printf("part: %s\n", parts[parts.size()-1].string()); + + const int N = parts.size(); + int index = 0; + String8 part = parts[index]; + + // resource type + if (!isValidResourceType(part)) { + return false; + } + *resType = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + + // imsi - mcc + if (getMccName(part.string())) { + mcc = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not mcc: %s\n", part.string()); + } + + // imsi - mnc + if (getMncName(part.string())) { + mnc = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not mcc: %s\n", part.string()); + } + + // locale - language + if (part.length() == 2 && isalpha(part[0]) && isalpha(part[1])) { + loc = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not language: %s\n", part.string()); + } + + // locale - region + if (loc.length() > 0 + && part.length() == 3 && part[0] == 'r' && part[0] && part[1]) { + loc += "-"; + part.toUpper(); + loc += part.string() + 1; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not region: %s\n", part.string()); + } + + // orientation + if (getOrientationName(part.string())) { + orient = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not orientation: %s\n", part.string()); + } + + // density + if (getDensityName(part.string())) { + den = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not density: %s\n", part.string()); + } + + // touchscreen + if (getTouchscreenName(part.string())) { + touch = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not touchscreen: %s\n", part.string()); + } + + // keyboard hidden + if (getKeysHiddenName(part.string())) { + keysHidden = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not keysHidden: %s\n", part.string()); + } + + // keyboard + if (getKeyboardName(part.string())) { + key = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not keyboard: %s\n", part.string()); + } + + if (getNavigationName(part.string())) { + nav = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not navigation: %s\n", part.string()); + } + + if (getScreenSizeName(part.string())) { + size = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not screen size: %s\n", part.string()); + } + + if (getVersionName(part.string())) { + vers = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not version: %s\n", part.string()); + } + + // if there are extra parts, it doesn't match + return false; + +success: + this->mcc = mcc; + this->mnc = mnc; + this->locale = loc; + this->orientation = orient; + this->density = den; + this->touchscreen = touch; + this->keysHidden = keysHidden; + this->keyboard = key; + this->navigation = nav; + this->screenSize = size; + this->version = vers; + + // what is this anyway? + this->vendor = ""; + + return true; +} + +String8 +AaptGroupEntry::toString() const +{ + String8 s = this->mcc; + s += ","; + s += this->mnc; + s += ","; + s += this->locale; + s += ","; + s += this->orientation; + s += ","; + s += density; + s += ","; + s += touchscreen; + s += ","; + s += keysHidden; + s += ","; + s += keyboard; + s += ","; + s += navigation; + s += ","; + s += screenSize; + s += ","; + s += version; + return s; +} + +String8 +AaptGroupEntry::toDirName(const String8& resType) const +{ + String8 s = resType; + if (this->mcc != "") { + s += "-"; + s += mcc; + } + if (this->mnc != "") { + s += "-"; + s += mnc; + } + if (this->locale != "") { + s += "-"; + s += locale; + } + if (this->orientation != "") { + s += "-"; + s += orientation; + } + if (this->density != "") { + s += "-"; + s += density; + } + if (this->touchscreen != "") { + s += "-"; + s += touchscreen; + } + if (this->keysHidden != "") { + s += "-"; + s += keysHidden; + } + if (this->keyboard != "") { + s += "-"; + s += keyboard; + } + if (this->navigation != "") { + s += "-"; + s += navigation; + } + if (this->screenSize != "") { + s += "-"; + s += screenSize; + } + if (this->version != "") { + s += "-"; + s += version; + } + + return s; +} + +bool AaptGroupEntry::getMccName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->mcc = 0; + return true; + } + const char* c = name; + if (tolower(*c) != 'm') return false; + c++; + if (tolower(*c) != 'c') return false; + c++; + if (tolower(*c) != 'c') return false; + c++; + + const char* val = c; + + while (*c >= '0' && *c <= '9') { + c++; + } + if (*c != 0) return false; + if (c-val != 3) return false; + + int d = atoi(val); + if (d != 0) { + if (out) out->mcc = d; + return true; + } + + return false; +} + +bool AaptGroupEntry::getMncName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->mcc = 0; + return true; + } + const char* c = name; + if (tolower(*c) != 'm') return false; + c++; + if (tolower(*c) != 'n') return false; + c++; + if (tolower(*c) != 'c') return false; + c++; + + const char* val = c; + + while (*c >= '0' && *c <= '9') { + c++; + } + if (*c != 0) return false; + if (c-val == 0 || c-val > 3) return false; + + int d = atoi(val); + if (d != 0) { + if (out) out->mnc = d; + return true; + } + + return false; +} + +/* + * Does this directory name fit the pattern of a locale dir ("en-rUS" or + * "default")? + * + * TODO: Should insist that the first two letters are lower case, and the + * second two are upper. + */ +bool AaptGroupEntry::getLocaleName(const char* fileName, + ResTable_config* out) +{ + if (strcmp(fileName, kWildcardName) == 0 + || strcmp(fileName, kDefaultLocale) == 0) { + if (out) { + out->language[0] = 0; + out->language[1] = 0; + out->country[0] = 0; + out->country[1] = 0; + } + return true; + } + + if (strlen(fileName) == 2 && isalpha(fileName[0]) && isalpha(fileName[1])) { + if (out) { + out->language[0] = fileName[0]; + out->language[1] = fileName[1]; + out->country[0] = 0; + out->country[1] = 0; + } + return true; + } + + if (strlen(fileName) == 5 && + isalpha(fileName[0]) && + isalpha(fileName[1]) && + fileName[2] == '-' && + isalpha(fileName[3]) && + isalpha(fileName[4])) { + if (out) { + out->language[0] = fileName[0]; + out->language[1] = fileName[1]; + out->country[0] = fileName[3]; + out->country[1] = fileName[4]; + } + return true; + } + + return false; +} + +bool AaptGroupEntry::getOrientationName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->orientation = out->ORIENTATION_ANY; + return true; + } else if (strcmp(name, "port") == 0) { + if (out) out->orientation = out->ORIENTATION_PORT; + return true; + } else if (strcmp(name, "land") == 0) { + if (out) out->orientation = out->ORIENTATION_LAND; + return true; + } else if (strcmp(name, "square") == 0) { + if (out) out->orientation = out->ORIENTATION_SQUARE; + return true; + } + + return false; +} + +bool AaptGroupEntry::getDensityName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->density = 0; + return true; + } + char* c = (char*)name; + while (*c >= '0' && *c <= '9') { + c++; + } + + // check that we have 'dpi' after the last digit. + if (toupper(c[0]) != 'D' || + toupper(c[1]) != 'P' || + toupper(c[2]) != 'I' || + c[3] != 0) { + return false; + } + + // temporarily replace the first letter with \0 to + // use atoi. + char tmp = c[0]; + c[0] = '\0'; + + int d = atoi(name); + c[0] = tmp; + + if (d != 0) { + if (out) out->density = d; + return true; + } + + return false; +} + +bool AaptGroupEntry::getTouchscreenName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->touchscreen = out->TOUCHSCREEN_ANY; + return true; + } else if (strcmp(name, "notouch") == 0) { + if (out) out->touchscreen = out->TOUCHSCREEN_NOTOUCH; + return true; + } else if (strcmp(name, "stylus") == 0) { + if (out) out->touchscreen = out->TOUCHSCREEN_STYLUS; + return true; + } else if (strcmp(name, "finger") == 0) { + if (out) out->touchscreen = out->TOUCHSCREEN_FINGER; + return true; + } + + return false; +} + +bool AaptGroupEntry::getKeysHiddenName(const char* name, + ResTable_config* out) +{ + uint8_t mask = 0; + uint8_t value = 0; + if (strcmp(name, kWildcardName) == 0) { + mask = out->MASK_KEYSHIDDEN; + value = out->KEYSHIDDEN_ANY; + } else if (strcmp(name, "keysexposed") == 0) { + mask = out->MASK_KEYSHIDDEN; + value = out->KEYSHIDDEN_NO; + } else if (strcmp(name, "keyshidden") == 0) { + mask = out->MASK_KEYSHIDDEN; + value = out->KEYSHIDDEN_YES; + } else if (strcmp(name, "keyssoft") == 0) { + mask = out->MASK_KEYSHIDDEN; + value = out->KEYSHIDDEN_SOFT; + } + + if (mask != 0) { + if (out) out->inputFlags = (out->inputFlags&~mask) | value; + return true; + } + + return false; +} + +bool AaptGroupEntry::getKeyboardName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->keyboard = out->KEYBOARD_ANY; + return true; + } else if (strcmp(name, "nokeys") == 0) { + if (out) out->keyboard = out->KEYBOARD_NOKEYS; + return true; + } else if (strcmp(name, "qwerty") == 0) { + if (out) out->keyboard = out->KEYBOARD_QWERTY; + return true; + } else if (strcmp(name, "12key") == 0) { + if (out) out->keyboard = out->KEYBOARD_12KEY; + return true; + } + + return false; +} + +bool AaptGroupEntry::getNavigationName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->navigation = out->NAVIGATION_ANY; + return true; + } else if (strcmp(name, "nonav") == 0) { + if (out) out->navigation = out->NAVIGATION_NONAV; + return true; + } else if (strcmp(name, "dpad") == 0) { + if (out) out->navigation = out->NAVIGATION_DPAD; + return true; + } else if (strcmp(name, "trackball") == 0) { + if (out) out->navigation = out->NAVIGATION_TRACKBALL; + return true; + } else if (strcmp(name, "wheel") == 0) { + if (out) out->navigation = out->NAVIGATION_WHEEL; + return true; + } + + return false; +} + +bool AaptGroupEntry::getScreenSizeName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->screenWidth = out->SCREENWIDTH_ANY; + out->screenHeight = out->SCREENHEIGHT_ANY; + } + return true; + } + + const char* x = name; + while (*x >= '0' && *x <= '9') x++; + if (x == name || *x != 'x') return false; + String8 xName(name, x-name); + x++; + + const char* y = x; + while (*y >= '0' && *y <= '9') y++; + if (y == name || *y != 0) return false; + String8 yName(x, y-x); + + uint16_t w = (uint16_t)atoi(xName.string()); + uint16_t h = (uint16_t)atoi(yName.string()); + if (w < h) { + return false; + } + + if (out) { + out->screenWidth = w; + out->screenHeight = h; + } + + return true; +} + +bool AaptGroupEntry::getVersionName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->sdkVersion = out->SDKVERSION_ANY; + out->minorVersion = out->MINORVERSION_ANY; + } + return true; + } + + if (*name != 'v') { + return false; + } + + name++; + const char* s = name; + while (*s >= '0' && *s <= '9') s++; + if (s == name || *s != 0) return false; + String8 sdkName(name, s-name); + + if (out) { + out->sdkVersion = (uint16_t)atoi(sdkName.string()); + out->minorVersion = 0; + } + + return true; +} + +int AaptGroupEntry::compare(const AaptGroupEntry& o) const +{ + int v = mcc.compare(o.mcc); + if (v == 0) v = mnc.compare(o.mnc); + if (v == 0) v = locale.compare(o.locale); + if (v == 0) v = vendor.compare(o.vendor); + if (v == 0) v = orientation.compare(o.orientation); + if (v == 0) v = density.compare(o.density); + if (v == 0) v = touchscreen.compare(o.touchscreen); + if (v == 0) v = keysHidden.compare(o.keysHidden); + if (v == 0) v = keyboard.compare(o.keyboard); + if (v == 0) v = navigation.compare(o.navigation); + if (v == 0) v = screenSize.compare(o.screenSize); + if (v == 0) v = version.compare(o.version); + return v; +} + +ResTable_config AaptGroupEntry::toParams() const +{ + ResTable_config params; + memset(¶ms, 0, sizeof(params)); + getMccName(mcc.string(), ¶ms); + getMncName(mnc.string(), ¶ms); + getLocaleName(locale.string(), ¶ms); + getOrientationName(orientation.string(), ¶ms); + getDensityName(density.string(), ¶ms); + getTouchscreenName(touchscreen.string(), ¶ms); + getKeysHiddenName(keysHidden.string(), ¶ms); + getKeyboardName(keyboard.string(), ¶ms); + getNavigationName(navigation.string(), ¶ms); + getScreenSizeName(screenSize.string(), ¶ms); + getVersionName(version.string(), ¶ms); + return params; +} + +// ========================================================================= +// ========================================================================= +// ========================================================================= + +void* AaptFile::editData(size_t size) +{ + if (size <= mBufferSize) { + mDataSize = size; + return mData; + } + size_t allocSize = (size*3)/2; + void* buf = realloc(mData, allocSize); + if (buf == NULL) { + return NULL; + } + mData = buf; + mDataSize = size; + mBufferSize = allocSize; + return buf; +} + +void* AaptFile::editData(size_t* outSize) +{ + if (outSize) { + *outSize = mDataSize; + } + return mData; +} + +void* AaptFile::padData(size_t wordSize) +{ + const size_t extra = mDataSize%wordSize; + if (extra == 0) { + return mData; + } + + size_t initial = mDataSize; + void* data = editData(initial+(wordSize-extra)); + if (data != NULL) { + memset(((uint8_t*)data) + initial, 0, wordSize-extra); + } + return data; +} + +status_t AaptFile::writeData(const void* data, size_t size) +{ + size_t end = mDataSize; + size_t total = size + end; + void* buf = editData(total); + if (buf == NULL) { + return UNKNOWN_ERROR; + } + memcpy(((char*)buf)+end, data, size); + return NO_ERROR; +} + +void AaptFile::clearData() +{ + if (mData != NULL) free(mData); + mData = NULL; + mDataSize = 0; + mBufferSize = 0; +} + +String8 AaptFile::getPrintableSource() const +{ + if (hasData()) { + String8 name(mGroupEntry.locale.string()); + name.appendPath(mGroupEntry.vendor.string()); + name.appendPath(mPath); + name.append(" #generated"); + return name; + } + return mSourceFile; +} + +// ========================================================================= +// ========================================================================= +// ========================================================================= + +status_t AaptGroup::addFile(const sp<AaptFile>& file) +{ + if (mFiles.indexOfKey(file->getGroupEntry()) < 0) { + file->mPath = mPath; + mFiles.add(file->getGroupEntry(), file); + return NO_ERROR; + } + + SourcePos(file->getSourceFile(), -1).error("Duplicate file.\n%s: Original is here.", + getPrintableSource().string()); + return UNKNOWN_ERROR; +} + +void AaptGroup::removeFile(size_t index) +{ + mFiles.removeItemsAt(index); +} + +void AaptGroup::print() const +{ + printf(" %s\n", getPath().string()); + const size_t N=mFiles.size(); + size_t i; + for (i=0; i<N; i++) { + sp<AaptFile> file = mFiles.valueAt(i); + const AaptGroupEntry& e = file->getGroupEntry(); + if (file->hasData()) { + printf(" Gen: (%s) %d bytes\n", e.toString().string(), + (int)file->getSize()); + } else { + printf(" Src: %s\n", file->getPrintableSource().string()); + } + } +} + +String8 AaptGroup::getPrintableSource() const +{ + if (mFiles.size() > 0) { + // Arbitrarily pull the first source file out of the list. + return mFiles.valueAt(0)->getPrintableSource(); + } + + // Should never hit this case, but to be safe... + return getPath(); + +} + +// ========================================================================= +// ========================================================================= +// ========================================================================= + +status_t AaptDir::addFile(const String8& name, const sp<AaptGroup>& file) +{ + if (mFiles.indexOfKey(name) >= 0) { + return ALREADY_EXISTS; + } + mFiles.add(name, file); + return NO_ERROR; +} + +status_t AaptDir::addDir(const String8& name, const sp<AaptDir>& dir) +{ + if (mDirs.indexOfKey(name) >= 0) { + return ALREADY_EXISTS; + } + mDirs.add(name, dir); + return NO_ERROR; +} + +sp<AaptDir> AaptDir::makeDir(const String8& path) +{ + String8 name; + String8 remain = path; + + sp<AaptDir> subdir = this; + while (name = remain.walkPath(&remain), remain != "") { + subdir = subdir->makeDir(name); + } + + ssize_t i = subdir->mDirs.indexOfKey(name); + if (i >= 0) { + return subdir->mDirs.valueAt(i); + } + sp<AaptDir> dir = new AaptDir(name, subdir->mPath.appendPathCopy(name)); + subdir->mDirs.add(name, dir); + return dir; +} + +void AaptDir::removeFile(const String8& name) +{ + mFiles.removeItem(name); +} + +void AaptDir::removeDir(const String8& name) +{ + mDirs.removeItem(name); +} + +status_t AaptDir::renameFile(const sp<AaptFile>& file, const String8& newName) +{ + sp<AaptGroup> origGroup; + + // Find and remove the given file with shear, brute force! + const size_t NG = mFiles.size(); + size_t i; + for (i=0; origGroup == NULL && i<NG; i++) { + sp<AaptGroup> g = mFiles.valueAt(i); + const size_t NF = g->getFiles().size(); + for (size_t j=0; j<NF; j++) { + if (g->getFiles().valueAt(j) == file) { + origGroup = g; + g->removeFile(j); + if (NF == 1) { + mFiles.removeItemsAt(i); + } + break; + } + } + } + + //printf("Renaming %s to %s\n", file->getPath().getPathName(), newName.string()); + + // Place the file under its new name. + if (origGroup != NULL) { + return addLeafFile(newName, file); + } + + return NO_ERROR; +} + +status_t AaptDir::addLeafFile(const String8& leafName, const sp<AaptFile>& file) +{ + sp<AaptGroup> group; + if (mFiles.indexOfKey(leafName) >= 0) { + group = mFiles.valueFor(leafName); + } else { + group = new AaptGroup(leafName, mPath.appendPathCopy(leafName)); + mFiles.add(leafName, group); + } + + return group->addFile(file); +} + +ssize_t AaptDir::slurpFullTree(Bundle* bundle, const String8& srcDir, + const AaptGroupEntry& kind, const String8& resType) +{ + Vector<String8> fileNames; + + { + DIR* dir = NULL; + + dir = opendir(srcDir.string()); + if (dir == NULL) { + fprintf(stderr, "ERROR: opendir(%s): %s\n", srcDir.string(), strerror(errno)); + return UNKNOWN_ERROR; + } + + /* + * Slurp the filenames out of the directory. + */ + while (1) { + struct dirent* entry; + + entry = readdir(dir); + if (entry == NULL) + break; + + if (isHidden(srcDir.string(), entry->d_name)) + continue; + + fileNames.add(String8(entry->d_name)); + } + + closedir(dir); + } + + ssize_t count = 0; + + /* + * Stash away the files and recursively descend into subdirectories. + */ + const size_t N = fileNames.size(); + size_t i; + for (i = 0; i < N; i++) { + String8 pathName(srcDir); + FileType type; + + pathName.appendPath(fileNames[i].string()); + type = getFileType(pathName.string()); + if (type == kFileTypeDirectory) { + sp<AaptDir> subdir; + bool notAdded = false; + if (mDirs.indexOfKey(fileNames[i]) >= 0) { + subdir = mDirs.valueFor(fileNames[i]); + } else { + subdir = new AaptDir(fileNames[i], mPath.appendPathCopy(fileNames[i])); + notAdded = true; + } + ssize_t res = subdir->slurpFullTree(bundle, pathName, kind, + resType); + if (res < NO_ERROR) { + return res; + } + if (res > 0 && notAdded) { + mDirs.add(fileNames[i], subdir); + } + count += res; + } else if (type == kFileTypeRegular) { + sp<AaptFile> file = new AaptFile(pathName, kind, resType); + status_t err = addLeafFile(fileNames[i], file); + if (err != NO_ERROR) { + return err; + } + + count++; + + } else { + if (bundle->getVerbose()) + printf(" (ignoring non-file/dir '%s')\n", pathName.string()); + } + } + + return count; +} + +status_t AaptDir::validate() const +{ + const size_t NF = mFiles.size(); + const size_t ND = mDirs.size(); + size_t i; + for (i = 0; i < NF; i++) { + if (!validateFileName(mFiles.valueAt(i)->getLeaf().string())) { + SourcePos(mFiles.valueAt(i)->getPrintableSource(), -1).error( + "Invalid filename. Unable to add."); + return UNKNOWN_ERROR; + } + + size_t j; + for (j = i+1; j < NF; j++) { + if (strcasecmp(mFiles.valueAt(i)->getLeaf().string(), + mFiles.valueAt(j)->getLeaf().string()) == 0) { + SourcePos(mFiles.valueAt(i)->getPrintableSource(), -1).error( + "File is case-insensitive equivalent to: %s", + mFiles.valueAt(j)->getPrintableSource().string()); + return UNKNOWN_ERROR; + } + + // TODO: if ".gz", check for non-.gz; if non-, check for ".gz" + // (this is mostly caught by the "marked" stuff, below) + } + + for (j = 0; j < ND; j++) { + if (strcasecmp(mFiles.valueAt(i)->getLeaf().string(), + mDirs.valueAt(j)->getLeaf().string()) == 0) { + SourcePos(mFiles.valueAt(i)->getPrintableSource(), -1).error( + "File conflicts with dir from: %s", + mDirs.valueAt(j)->getPrintableSource().string()); + return UNKNOWN_ERROR; + } + } + } + + for (i = 0; i < ND; i++) { + if (!validateFileName(mDirs.valueAt(i)->getLeaf().string())) { + SourcePos(mDirs.valueAt(i)->getPrintableSource(), -1).error( + "Invalid directory name, unable to add."); + return UNKNOWN_ERROR; + } + + size_t j; + for (j = i+1; j < ND; j++) { + if (strcasecmp(mDirs.valueAt(i)->getLeaf().string(), + mDirs.valueAt(j)->getLeaf().string()) == 0) { + SourcePos(mDirs.valueAt(i)->getPrintableSource(), -1).error( + "Directory is case-insensitive equivalent to: %s", + mDirs.valueAt(j)->getPrintableSource().string()); + return UNKNOWN_ERROR; + } + } + + status_t err = mDirs.valueAt(i)->validate(); + if (err != NO_ERROR) { + return err; + } + } + + return NO_ERROR; +} + +void AaptDir::print() const +{ + const size_t ND=getDirs().size(); + size_t i; + for (i=0; i<ND; i++) { + getDirs().valueAt(i)->print(); + } + + const size_t NF=getFiles().size(); + for (i=0; i<NF; i++) { + getFiles().valueAt(i)->print(); + } +} + +String8 AaptDir::getPrintableSource() const +{ + if (mFiles.size() > 0) { + // Arbitrarily pull the first file out of the list as the source dir. + return mFiles.valueAt(0)->getPrintableSource().getPathDir(); + } + if (mDirs.size() > 0) { + // Or arbitrarily pull the first dir out of the list as the source dir. + return mDirs.valueAt(0)->getPrintableSource().getPathDir(); + } + + // Should never hit this case, but to be safe... + return mPath; + +} + +// ========================================================================= +// ========================================================================= +// ========================================================================= + +sp<AaptFile> AaptAssets::addFile( + const String8& filePath, const AaptGroupEntry& entry, + const String8& srcDir, sp<AaptGroup>* outGroup, + const String8& resType) +{ + sp<AaptDir> dir = this; + sp<AaptGroup> group; + sp<AaptFile> file; + String8 root, remain(filePath), partialPath; + while (remain.length() > 0) { + root = remain.walkPath(&remain); + partialPath.appendPath(root); + + const String8 rootStr(root); + + if (remain.length() == 0) { + ssize_t i = dir->getFiles().indexOfKey(rootStr); + if (i >= 0) { + group = dir->getFiles().valueAt(i); + } else { + group = new AaptGroup(rootStr, filePath); + status_t res = dir->addFile(rootStr, group); + if (res != NO_ERROR) { + return NULL; + } + } + file = new AaptFile(srcDir.appendPathCopy(filePath), entry, resType); + status_t res = group->addFile(file); + if (res != NO_ERROR) { + return NULL; + } + break; + + } else { + ssize_t i = dir->getDirs().indexOfKey(rootStr); + if (i >= 0) { + dir = dir->getDirs().valueAt(i); + } else { + sp<AaptDir> subdir = new AaptDir(rootStr, partialPath); + status_t res = dir->addDir(rootStr, subdir); + if (res != NO_ERROR) { + return NULL; + } + dir = subdir; + } + } + } + + mGroupEntries.add(entry); + if (outGroup) *outGroup = group; + return file; +} + +void AaptAssets::addResource(const String8& leafName, const String8& path, + const sp<AaptFile>& file, const String8& resType) +{ + sp<AaptDir> res = AaptDir::makeDir(kResString); + String8 dirname = file->getGroupEntry().toDirName(resType); + sp<AaptDir> subdir = res->makeDir(dirname); + sp<AaptGroup> grr = new AaptGroup(leafName, path); + grr->addFile(file); + + subdir->addFile(leafName, grr); +} + + +ssize_t AaptAssets::slurpFromArgs(Bundle* bundle) +{ + int count; + int totalCount = 0; + FileType type; + const Vector<const char *>& resDirs = bundle->getResourceSourceDirs(); + const size_t dirCount =resDirs.size(); + sp<AaptAssets> current = this; + + const int N = bundle->getFileSpecCount(); + + /* + * If a package manifest was specified, include that first. + */ + if (bundle->getAndroidManifestFile() != NULL) { + // place at root of zip. + String8 srcFile(bundle->getAndroidManifestFile()); + addFile(srcFile.getPathLeaf(), AaptGroupEntry(), srcFile.getPathDir(), + NULL, String8()); + totalCount++; + } + + /* + * If a directory of custom assets was supplied, slurp 'em up. + */ + if (bundle->getAssetSourceDir()) { + const char* assetDir = bundle->getAssetSourceDir(); + + FileType type = getFileType(assetDir); + if (type == kFileTypeNonexistent) { + fprintf(stderr, "ERROR: asset directory '%s' does not exist\n", assetDir); + return UNKNOWN_ERROR; + } + if (type != kFileTypeDirectory) { + fprintf(stderr, "ERROR: '%s' is not a directory\n", assetDir); + return UNKNOWN_ERROR; + } + + String8 assetRoot(assetDir); + sp<AaptDir> assetAaptDir = makeDir(String8(kAssetDir)); + AaptGroupEntry group; + count = assetAaptDir->slurpFullTree(bundle, assetRoot, group, + String8()); + if (count < 0) { + totalCount = count; + goto bail; + } + if (count > 0) { + mGroupEntries.add(group); + } + totalCount += count; + + if (bundle->getVerbose()) + printf("Found %d custom asset file%s in %s\n", + count, (count==1) ? "" : "s", assetDir); + } + + /* + * If a directory of resource-specific assets was supplied, slurp 'em up. + */ + for (size_t i=0; i<dirCount; i++) { + const char *res = resDirs[i]; + if (res) { + type = getFileType(res); + if (type == kFileTypeNonexistent) { + fprintf(stderr, "ERROR: resource directory '%s' does not exist\n", res); + return UNKNOWN_ERROR; + } + if (type == kFileTypeDirectory) { + if (i>0) { + sp<AaptAssets> nextOverlay = new AaptAssets(); + current->setOverlay(nextOverlay); + current = nextOverlay; + } + count = current->slurpResourceTree(bundle, String8(res)); + + if (count < 0) { + totalCount = count; + goto bail; + } + totalCount += count; + } + else { + fprintf(stderr, "ERROR: '%s' is not a directory\n", res); + return UNKNOWN_ERROR; + } + } + + } + /* + * Now do any additional raw files. + */ + for (int arg=0; arg<N; arg++) { + const char* assetDir = bundle->getFileSpecEntry(arg); + + FileType type = getFileType(assetDir); + if (type == kFileTypeNonexistent) { + fprintf(stderr, "ERROR: input directory '%s' does not exist\n", assetDir); + return UNKNOWN_ERROR; + } + if (type != kFileTypeDirectory) { + fprintf(stderr, "ERROR: '%s' is not a directory\n", assetDir); + return UNKNOWN_ERROR; + } + + String8 assetRoot(assetDir); + + if (bundle->getVerbose()) + printf("Processing raw dir '%s'\n", (const char*) assetDir); + + /* + * Do a recursive traversal of subdir tree. We don't make any + * guarantees about ordering, so we're okay with an inorder search + * using whatever order the OS happens to hand back to us. + */ + count = slurpFullTree(bundle, assetRoot, AaptGroupEntry(), String8()); + if (count < 0) { + /* failure; report error and remove archive */ + totalCount = count; + goto bail; + } + totalCount += count; + + if (bundle->getVerbose()) + printf("Found %d asset file%s in %s\n", + count, (count==1) ? "" : "s", assetDir); + } + + count = validate(); + if (count != NO_ERROR) { + totalCount = count; + goto bail; + } + + +bail: + return totalCount; +} + +ssize_t AaptAssets::slurpFullTree(Bundle* bundle, const String8& srcDir, + const AaptGroupEntry& kind, + const String8& resType) +{ + ssize_t res = AaptDir::slurpFullTree(bundle, srcDir, kind, resType); + if (res > 0) { + mGroupEntries.add(kind); + } + + return res; +} + +ssize_t AaptAssets::slurpResourceTree(Bundle* bundle, const String8& srcDir) +{ + ssize_t err = 0; + + DIR* dir = opendir(srcDir.string()); + if (dir == NULL) { + fprintf(stderr, "ERROR: opendir(%s): %s\n", srcDir.string(), strerror(errno)); + return UNKNOWN_ERROR; + } + + status_t count = 0; + + /* + * Run through the directory, looking for dirs that match the + * expected pattern. + */ + while (1) { + struct dirent* entry = readdir(dir); + if (entry == NULL) { + break; + } + + if (isHidden(srcDir.string(), entry->d_name)) { + continue; + } + + String8 subdirName(srcDir); + subdirName.appendPath(entry->d_name); + + AaptGroupEntry group; + String8 resType; + bool b = group.initFromDirName(entry->d_name, &resType); + if (!b) { + fprintf(stderr, "invalid resource directory name: %s/%s\n", srcDir.string(), + entry->d_name); + err = -1; + continue; + } + + FileType type = getFileType(subdirName.string()); + + if (type == kFileTypeDirectory) { + sp<AaptDir> dir = makeDir(String8(entry->d_name)); + ssize_t res = dir->slurpFullTree(bundle, subdirName, group, + resType); + if (res < 0) { + count = res; + goto bail; + } + if (res > 0) { + mGroupEntries.add(group); + count += res; + } + + mDirs.add(dir); + } else { + if (bundle->getVerbose()) { + fprintf(stderr, " (ignoring file '%s')\n", subdirName.string()); + } + } + } + +bail: + closedir(dir); + dir = NULL; + + if (err != 0) { + return err; + } + return count; +} + +ssize_t +AaptAssets::slurpResourceZip(Bundle* bundle, const char* filename) +{ + int count = 0; + SortedVector<AaptGroupEntry> entries; + + ZipFile* zip = new ZipFile; + status_t err = zip->open(filename, ZipFile::kOpenReadOnly); + if (err != NO_ERROR) { + fprintf(stderr, "error opening zip file %s\n", filename); + count = err; + delete zip; + return -1; + } + + const int N = zip->getNumEntries(); + for (int i=0; i<N; i++) { + ZipEntry* entry = zip->getEntryByIndex(i); + if (entry->getDeleted()) { + continue; + } + + String8 entryName(entry->getFileName()); + + String8 dirName = entryName.getPathDir(); + sp<AaptDir> dir = dirName == "" ? this : makeDir(dirName); + + String8 resType; + AaptGroupEntry kind; + + String8 remain; + if (entryName.walkPath(&remain) == kResourceDir) { + // these are the resources, pull their type out of the directory name + kind.initFromDirName(remain.walkPath().string(), &resType); + } else { + // these are untyped and don't have an AaptGroupEntry + } + if (entries.indexOf(kind) < 0) { + entries.add(kind); + mGroupEntries.add(kind); + } + + // use the one from the zip file if they both exist. + dir->removeFile(entryName.getPathLeaf()); + + sp<AaptFile> file = new AaptFile(entryName, kind, resType); + status_t err = dir->addLeafFile(entryName.getPathLeaf(), file); + if (err != NO_ERROR) { + fprintf(stderr, "err=%s entryName=%s\n", strerror(err), entryName.string()); + count = err; + goto bail; + } + file->setCompressionMethod(entry->getCompressionMethod()); + +#if 0 + if (entryName == "AndroidManifest.xml") { + printf("AndroidManifest.xml\n"); + } + printf("\n\nfile: %s\n", entryName.string()); +#endif + + size_t len = entry->getUncompressedLen(); + void* data = zip->uncompress(entry); + void* buf = file->editData(len); + memcpy(buf, data, len); + +#if 0 + const int OFF = 0; + const unsigned char* p = (unsigned char*)data; + const unsigned char* end = p+len; + p += OFF; + for (int i=0; i<32 && p < end; i++) { + printf("0x%03x ", i*0x10 + OFF); + for (int j=0; j<0x10 && p < end; j++) { + printf(" %02x", *p); + p++; + } + printf("\n"); + } +#endif + + free(data); + + count++; + } + +bail: + delete zip; + return count; +} + +sp<AaptSymbols> AaptAssets::getSymbolsFor(const String8& name) +{ + sp<AaptSymbols> sym = mSymbols.valueFor(name); + if (sym == NULL) { + sym = new AaptSymbols(); + mSymbols.add(name, sym); + } + return sym; +} + +status_t AaptAssets::buildIncludedResources(Bundle* bundle) +{ + if (!mHaveIncludedAssets) { + // Add in all includes. + const Vector<const char*>& incl = bundle->getPackageIncludes(); + const size_t N=incl.size(); + for (size_t i=0; i<N; i++) { + if (bundle->getVerbose()) + printf("Including resources from package: %s\n", incl[i]); + if (!mIncludedAssets.addAssetPath(String8(incl[i]), NULL)) { + fprintf(stderr, "ERROR: Asset package include '%s' not found.\n", + incl[i]); + return UNKNOWN_ERROR; + } + } + mHaveIncludedAssets = true; + } + + return NO_ERROR; +} + +status_t AaptAssets::addIncludedResources(const sp<AaptFile>& file) +{ + const ResTable& res = getIncludedResources(); + // XXX dirty! + return const_cast<ResTable&>(res).add(file->getData(), file->getSize(), NULL); +} + +const ResTable& AaptAssets::getIncludedResources() const +{ + return mIncludedAssets.getResources(false); +} + +void AaptAssets::print() const +{ + printf("Locale/Vendor pairs:\n"); + const size_t N=mGroupEntries.size(); + for (size_t i=0; i<N; i++) { + printf(" %s/%s\n", + mGroupEntries.itemAt(i).locale.string(), + mGroupEntries.itemAt(i).vendor.string()); + } + + printf("\nFiles:\n"); + AaptDir::print(); +} + +bool +valid_symbol_name(const String8& symbol) +{ + static char const * const KEYWORDS[] = { + "abstract", "assert", "boolean", "break", + "byte", "case", "catch", "char", "class", "const", "continue", + "default", "do", "double", "else", "enum", "extends", "final", + "finally", "float", "for", "goto", "if", "implements", "import", + "instanceof", "int", "interface", "long", "native", "new", "package", + "private", "protected", "public", "return", "short", "static", + "strictfp", "super", "switch", "synchronized", "this", "throw", + "throws", "transient", "try", "void", "volatile", "while", + "true", "false", "null", + NULL + }; + const char*const* k = KEYWORDS; + const char*const s = symbol.string(); + while (*k) { + if (0 == strcmp(s, *k)) { + return false; + } + k++; + } + return true; +} diff --git a/tools/aapt/AaptAssets.h b/tools/aapt/AaptAssets.h new file mode 100644 index 0000000..01c8140 --- /dev/null +++ b/tools/aapt/AaptAssets.h @@ -0,0 +1,519 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Information about assets being operated on. +// +#ifndef __AAPT_ASSETS_H +#define __AAPT_ASSETS_H + +#include <stdlib.h> +#include <utils/AssetManager.h> +#include <utils/KeyedVector.h> +#include <utils/String8.h> +#include <utils/ResourceTypes.h> +#include <utils/SortedVector.h> +#include <utils/String8.h> +#include <utils/Vector.h> +#include <utils/RefBase.h> +#include <utils/ZipFile.h> + +#include "Bundle.h" +#include "SourcePos.h" + +using namespace android; + +bool valid_symbol_name(const String8& str); + +enum { + AXIS_NONE = 0, + AXIS_MCC = 1, + AXIS_MNC, + AXIS_LANGUAGE, + AXIS_REGION, + AXIS_ORIENTATION, + AXIS_DENSITY, + AXIS_TOUCHSCREEN, + AXIS_KEYSHIDDEN, + AXIS_KEYBOARD, + AXIS_NAVIGATION, + AXIS_SCREENSIZE, + AXIS_VERSION +}; + +/** + * This structure contains a specific variation of a single file out + * of all the variations it can have that we can have. + */ +struct AaptGroupEntry +{ +public: + AaptGroupEntry() { } + AaptGroupEntry(const String8& _locale, const String8& _vendor) + : locale(_locale), vendor(_vendor) { } + + String8 mcc; + String8 mnc; + String8 locale; + String8 vendor; + String8 orientation; + String8 density; + String8 touchscreen; + String8 keysHidden; + String8 keyboard; + String8 navigation; + String8 screenSize; + String8 version; + + bool initFromDirName(const char* dir, String8* resType); + + static status_t parseNamePart(const String8& part, int* axis, uint32_t* value); + + static bool getMccName(const char* name, ResTable_config* out = NULL); + static bool getMncName(const char* name, ResTable_config* out = NULL); + static bool getLocaleName(const char* name, ResTable_config* out = NULL); + static bool getOrientationName(const char* name, ResTable_config* out = NULL); + static bool getDensityName(const char* name, ResTable_config* out = NULL); + static bool getTouchscreenName(const char* name, ResTable_config* out = NULL); + static bool getKeysHiddenName(const char* name, ResTable_config* out = NULL); + static bool getKeyboardName(const char* name, ResTable_config* out = NULL); + static bool getNavigationName(const char* name, ResTable_config* out = NULL); + static bool getScreenSizeName(const char* name, ResTable_config* out = NULL); + static bool getVersionName(const char* name, ResTable_config* out = NULL); + + int compare(const AaptGroupEntry& o) const; + + ResTable_config toParams() const; + + inline bool operator<(const AaptGroupEntry& o) const { return compare(o) < 0; } + inline bool operator<=(const AaptGroupEntry& o) const { return compare(o) <= 0; } + inline bool operator==(const AaptGroupEntry& o) const { return compare(o) == 0; } + inline bool operator!=(const AaptGroupEntry& o) const { return compare(o) != 0; } + inline bool operator>=(const AaptGroupEntry& o) const { return compare(o) >= 0; } + inline bool operator>(const AaptGroupEntry& o) const { return compare(o) > 0; } + + String8 toString() const; + String8 toDirName(const String8& resType) const; +}; + +inline int compare_type(const AaptGroupEntry& lhs, const AaptGroupEntry& rhs) +{ + return lhs.compare(rhs); +} + +inline int strictly_order_type(const AaptGroupEntry& lhs, const AaptGroupEntry& rhs) +{ + return compare_type(lhs, rhs) < 0; +} + +class AaptGroup; + +/** + * A single asset file we know about. + */ +class AaptFile : public RefBase +{ +public: + AaptFile(const String8& sourceFile, const AaptGroupEntry& groupEntry, + const String8& resType) + : mGroupEntry(groupEntry) + , mResourceType(resType) + , mSourceFile(sourceFile) + , mData(NULL) + , mDataSize(0) + , mBufferSize(0) + , mCompression(ZipEntry::kCompressStored) + { + //printf("new AaptFile created %s\n", (const char*)sourceFile); + } + virtual ~AaptFile() { } + + const String8& getPath() const { return mPath; } + const AaptGroupEntry& getGroupEntry() const { return mGroupEntry; } + + // Data API. If there is data attached to the file, + // getSourceFile() is not used. + bool hasData() const { return mData != NULL; } + const void* getData() const { return mData; } + size_t getSize() const { return mDataSize; } + void* editData(size_t size); + void* editData(size_t* outSize = NULL); + void* padData(size_t wordSize); + status_t writeData(const void* data, size_t size); + void clearData(); + + const String8& getResourceType() const { return mResourceType; } + + // File API. If the file does not hold raw data, this is + // a full path to a file on the filesystem that holds its data. + const String8& getSourceFile() const { return mSourceFile; } + + String8 getPrintableSource() const; + + // Desired compression method, as per utils/ZipEntry.h. For example, + // no compression is ZipEntry::kCompressStored. + int getCompressionMethod() const { return mCompression; } + void setCompressionMethod(int c) { mCompression = c; } +private: + friend class AaptGroup; + + String8 mPath; + AaptGroupEntry mGroupEntry; + String8 mResourceType; + String8 mSourceFile; + void* mData; + size_t mDataSize; + size_t mBufferSize; + int mCompression; +}; + +/** + * A group of related files (the same file, with different + * vendor/locale variations). + */ +class AaptGroup : public RefBase +{ +public: + AaptGroup(const String8& leaf, const String8& path) + : mLeaf(leaf), mPath(path) { } + virtual ~AaptGroup() { } + + const String8& getLeaf() const { return mLeaf; } + + // Returns the relative path after the AaptGroupEntry dirs. + const String8& getPath() const { return mPath; } + + const DefaultKeyedVector<AaptGroupEntry, sp<AaptFile> >& getFiles() const + { return mFiles; } + + status_t addFile(const sp<AaptFile>& file); + void removeFile(size_t index); + + void print() const; + + String8 getPrintableSource() const; + +private: + String8 mLeaf; + String8 mPath; + + DefaultKeyedVector<AaptGroupEntry, sp<AaptFile> > mFiles; +}; + +/** + * A single directory of assets, which can contain for files and other + * sub-directories. + */ +class AaptDir : public RefBase +{ +public: + AaptDir(const String8& leaf, const String8& path) + : mLeaf(leaf), mPath(path) { } + virtual ~AaptDir() { } + + const String8& getLeaf() const { return mLeaf; } + + const String8& getPath() const { return mPath; } + + const DefaultKeyedVector<String8, sp<AaptGroup> >& getFiles() const { return mFiles; } + const DefaultKeyedVector<String8, sp<AaptDir> >& getDirs() const { return mDirs; } + + status_t addFile(const String8& name, const sp<AaptGroup>& file); + status_t addDir(const String8& name, const sp<AaptDir>& dir); + + sp<AaptDir> makeDir(const String8& name); + + void removeFile(const String8& name); + void removeDir(const String8& name); + + status_t renameFile(const sp<AaptFile>& file, const String8& newName); + + status_t addLeafFile(const String8& leafName, + const sp<AaptFile>& file); + + virtual ssize_t slurpFullTree(Bundle* bundle, + const String8& srcDir, + const AaptGroupEntry& kind, + const String8& resType); + + /* + * Perform some sanity checks on the names of files and directories here. + * In particular: + * - Check for illegal chars in filenames. + * - Check filename length. + * - Check for presence of ".gz" and non-".gz" copies of same file. + * - Check for multiple files whose names match in a case-insensitive + * fashion (problematic for some systems). + * + * Comparing names against all other names is O(n^2). We could speed + * it up some by sorting the entries and being smarter about what we + * compare against, but I'm not expecting to have enough files in a + * single directory to make a noticeable difference in speed. + * + * Note that sorting here is not enough to guarantee that the package + * contents are sorted -- subsequent updates can rearrange things. + */ + status_t validate() const; + + void print() const; + + String8 getPrintableSource() const; + +private: + String8 mLeaf; + String8 mPath; + + DefaultKeyedVector<String8, sp<AaptGroup> > mFiles; + DefaultKeyedVector<String8, sp<AaptDir> > mDirs; +}; + +/** + * All information we know about a particular symbol. + */ +class AaptSymbolEntry +{ +public: + AaptSymbolEntry() + : isPublic(false), typeCode(TYPE_UNKNOWN) + { + } + AaptSymbolEntry(const String8& _name) + : name(_name), isPublic(false), typeCode(TYPE_UNKNOWN) + { + } + AaptSymbolEntry(const AaptSymbolEntry& o) + : name(o.name), sourcePos(o.sourcePos), isPublic(o.isPublic) + , comment(o.comment), typeComment(o.typeComment) + , typeCode(o.typeCode), int32Val(o.int32Val), stringVal(o.stringVal) + { + } + AaptSymbolEntry operator=(const AaptSymbolEntry& o) + { + sourcePos = o.sourcePos; + isPublic = o.isPublic; + comment = o.comment; + typeComment = o.typeComment; + typeCode = o.typeCode; + int32Val = o.int32Val; + stringVal = o.stringVal; + return *this; + } + + const String8 name; + + SourcePos sourcePos; + bool isPublic; + + String16 comment; + String16 typeComment; + + enum { + TYPE_UNKNOWN = 0, + TYPE_INT32, + TYPE_STRING + }; + + int typeCode; + + // Value. May be one of these. + int32_t int32Val; + String8 stringVal; +}; + +/** + * A group of related symbols (such as indices into a string block) + * that have been generated from the assets. + */ +class AaptSymbols : public RefBase +{ +public: + AaptSymbols() { } + virtual ~AaptSymbols() { } + + status_t addSymbol(const String8& name, int32_t value, const SourcePos& pos) { + if (!check_valid_symbol_name(name, pos, "symbol")) { + return BAD_VALUE; + } + AaptSymbolEntry& sym = edit_symbol(name, &pos); + sym.typeCode = AaptSymbolEntry::TYPE_INT32; + sym.int32Val = value; + return NO_ERROR; + } + + status_t addStringSymbol(const String8& name, const String8& value, + const SourcePos& pos) { + if (!check_valid_symbol_name(name, pos, "symbol")) { + return BAD_VALUE; + } + AaptSymbolEntry& sym = edit_symbol(name, &pos); + sym.typeCode = AaptSymbolEntry::TYPE_STRING; + sym.stringVal = value; + return NO_ERROR; + } + + status_t makeSymbolPublic(const String8& name, const SourcePos& pos) { + if (!check_valid_symbol_name(name, pos, "symbol")) { + return BAD_VALUE; + } + AaptSymbolEntry& sym = edit_symbol(name, &pos); + sym.isPublic = true; + return NO_ERROR; + } + + void appendComment(const String8& name, const String16& comment, const SourcePos& pos) { + if (comment.size() <= 0) { + return; + } + AaptSymbolEntry& sym = edit_symbol(name, &pos); + if (sym.comment.size() == 0) { + sym.comment = comment; + } else { + sym.comment.append(String16("\n")); + sym.comment.append(comment); + } + } + + void appendTypeComment(const String8& name, const String16& comment) { + if (comment.size() <= 0) { + return; + } + AaptSymbolEntry& sym = edit_symbol(name, NULL); + if (sym.typeComment.size() == 0) { + sym.typeComment = comment; + } else { + sym.typeComment.append(String16("\n")); + sym.typeComment.append(comment); + } + } + + sp<AaptSymbols> addNestedSymbol(const String8& name, const SourcePos& pos) { + if (!check_valid_symbol_name(name, pos, "nested symbol")) { + return NULL; + } + + sp<AaptSymbols> sym = mNestedSymbols.valueFor(name); + if (sym == NULL) { + sym = new AaptSymbols(); + mNestedSymbols.add(name, sym); + } + + return sym; + } + + const KeyedVector<String8, AaptSymbolEntry>& getSymbols() const + { return mSymbols; } + const DefaultKeyedVector<String8, sp<AaptSymbols> >& getNestedSymbols() const + { return mNestedSymbols; } + + const String16& getComment(const String8& name) const + { return get_symbol(name).comment; } + const String16& getTypeComment(const String8& name) const + { return get_symbol(name).typeComment; } + +private: + bool check_valid_symbol_name(const String8& symbol, const SourcePos& pos, const char* label) { + if (valid_symbol_name(symbol)) { + return true; + } + pos.error("invalid %s: '%s'\n", label, symbol.string()); + return false; + } + AaptSymbolEntry& edit_symbol(const String8& symbol, const SourcePos* pos) { + ssize_t i = mSymbols.indexOfKey(symbol); + if (i < 0) { + i = mSymbols.add(symbol, AaptSymbolEntry(symbol)); + } + AaptSymbolEntry& sym = mSymbols.editValueAt(i); + if (pos != NULL && sym.sourcePos.line < 0) { + sym.sourcePos = *pos; + } + return sym; + } + const AaptSymbolEntry& get_symbol(const String8& symbol) const { + ssize_t i = mSymbols.indexOfKey(symbol); + if (i >= 0) { + return mSymbols.valueAt(i); + } + return mDefSymbol; + } + + KeyedVector<String8, AaptSymbolEntry> mSymbols; + DefaultKeyedVector<String8, sp<AaptSymbols> > mNestedSymbols; + AaptSymbolEntry mDefSymbol; +}; + +class ResourceTypeSet; + +/** + * Asset hierarchy being operated on. + */ +class AaptAssets : public AaptDir +{ +public: + AaptAssets() : AaptDir(String8(), String8()), mHaveIncludedAssets(false) { } + virtual ~AaptAssets() { } + + const String8& getPackage() const { return mPackage; } + void setPackage(const String8& package) { mPackage = package; mSymbolsPrivatePackage = package; } + + const SortedVector<AaptGroupEntry>& getGroupEntries() const { return mGroupEntries; } + + sp<AaptFile> addFile(const String8& filePath, + const AaptGroupEntry& entry, + const String8& srcDir, + sp<AaptGroup>* outGroup, + const String8& resType); + + void addResource(const String8& leafName, + const String8& path, + const sp<AaptFile>& file, + const String8& resType); + + ssize_t slurpFromArgs(Bundle* bundle); + + virtual ssize_t slurpFullTree(Bundle* bundle, + const String8& srcDir, + const AaptGroupEntry& kind, + const String8& resType); + + ssize_t slurpResourceTree(Bundle* bundle, const String8& srcDir); + ssize_t slurpResourceZip(Bundle* bundle, const char* filename); + + sp<AaptSymbols> getSymbolsFor(const String8& name); + + const DefaultKeyedVector<String8, sp<AaptSymbols> >& getSymbols() const { return mSymbols; } + + String8 getSymbolsPrivatePackage() const { return mSymbolsPrivatePackage; } + void setSymbolsPrivatePackage(const String8& pkg) { mSymbolsPrivatePackage = pkg; } + + status_t buildIncludedResources(Bundle* bundle); + status_t addIncludedResources(const sp<AaptFile>& file); + const ResTable& getIncludedResources() const; + + void print() const; + + inline const Vector<sp<AaptDir> >& resDirs() { return mDirs; } + + inline sp<AaptAssets> getOverlay() { return mOverlay; } + inline void setOverlay(sp<AaptAssets>& overlay) { mOverlay = overlay; } + + inline KeyedVector<String8, sp<ResourceTypeSet> >* getResources() { return mRes; } + inline void + setResources(KeyedVector<String8, sp<ResourceTypeSet> >* res) { mRes = res; } + +private: + String8 mPackage; + SortedVector<AaptGroupEntry> mGroupEntries; + DefaultKeyedVector<String8, sp<AaptSymbols> > mSymbols; + String8 mSymbolsPrivatePackage; + + Vector<sp<AaptDir> > mDirs; + + bool mHaveIncludedAssets; + AssetManager mIncludedAssets; + + sp<AaptAssets> mOverlay; + KeyedVector<String8, sp<ResourceTypeSet> >* mRes; +}; + +#endif // __AAPT_ASSETS_H + diff --git a/tools/aapt/Android.mk b/tools/aapt/Android.mk new file mode 100644 index 0000000..fdc859c --- /dev/null +++ b/tools/aapt/Android.mk @@ -0,0 +1,52 @@ +# +# Copyright 2006 The Android Open Source Project +# +# Android Asset Packaging Tool +# + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + AaptAssets.cpp \ + Command.cpp \ + Main.cpp \ + Package.cpp \ + StringPool.cpp \ + XMLNode.cpp \ + ResourceTable.cpp \ + Images.cpp \ + Resource.cpp \ + SourcePos.cpp + +LOCAL_CFLAGS += -Wno-format-y2k + +LOCAL_C_INCLUDES += external/expat/lib +LOCAL_C_INCLUDES += external/libpng +LOCAL_C_INCLUDES += external/zlib +LOCAL_C_INCLUDES += build/libs/host/include + +#LOCAL_WHOLE_STATIC_LIBRARIES := +LOCAL_STATIC_LIBRARIES := \ + libhost \ + libutils \ + libcutils \ + libexpat \ + libpng + +LOCAL_LDLIBS := -lz + +ifeq ($(HOST_OS),linux) +LOCAL_LDLIBS += -lrt +endif + +ifeq ($(HOST_OS),windows) +ifeq ($(strip $(USE_CYGWIN),),) +LOCAL_LDLIBS += -lws2_32 +endif +endif + +LOCAL_MODULE := aapt + +include $(BUILD_HOST_EXECUTABLE) + diff --git a/tools/aapt/Bundle.h b/tools/aapt/Bundle.h new file mode 100644 index 0000000..2d8471b --- /dev/null +++ b/tools/aapt/Bundle.h @@ -0,0 +1,164 @@ +// +// Copyright 2006 The Android Open Source Project +// +// State bundle. Used to pass around stuff like command-line args. +// +#ifndef __BUNDLE_H +#define __BUNDLE_H + +#include <stdlib.h> +#include <utils.h> // android +#include <utils/String8.h> +#include <utils/Vector.h> + +/* + * Things we can do. + */ +typedef enum Command { + kCommandUnknown = 0, + kCommandVersion, + kCommandList, + kCommandDump, + kCommandAdd, + kCommandRemove, + kCommandPackage, +} Command; + +/* + * Bundle of goodies, including everything specified on the command line. + */ +class Bundle { +public: + Bundle(void) + : mCmd(kCommandUnknown), mVerbose(false), mAndroidList(false), + mForce(false), mGrayscaleTolerance(0), mMakePackageDirs(false), + mUpdate(false), mExtending(false), + mRequireLocalization(false), mPseudolocalize(false), + mCompressionMethod(0), mOutputAPKFile(NULL), + mAssetSourceDir(NULL), + mAndroidManifestFile(NULL), mPublicOutputFile(NULL), + mRClassDir(NULL), mResourceIntermediatesDir(NULL), + mArgc(0), mArgv(NULL) + {} + ~Bundle(void) {} + + /* + * Set the command value. Returns "false" if it was previously set. + */ + Command getCommand(void) const { return mCmd; } + void setCommand(Command cmd) { mCmd = cmd; } + + /* + * Command modifiers. Not all modifiers are appropriate for all + * commands. + */ + bool getVerbose(void) const { return mVerbose; } + void setVerbose(bool val) { mVerbose = val; } + bool getAndroidList(void) const { return mAndroidList; } + void setAndroidList(bool val) { mAndroidList = val; } + bool getForce(void) const { return mForce; } + void setForce(bool val) { mForce = val; } + void setGrayscaleTolerance(int val) { mGrayscaleTolerance = val; } + int getGrayscaleTolerance() { return mGrayscaleTolerance; } + bool getMakePackageDirs(void) const { return mMakePackageDirs; } + void setMakePackageDirs(bool val) { mMakePackageDirs = val; } + bool getUpdate(void) const { return mUpdate; } + void setUpdate(bool val) { mUpdate = val; } + bool getExtending(void) const { return mExtending; } + void setExtending(bool val) { mExtending = val; } + bool getRequireLocalization(void) const { return mRequireLocalization; } + void setRequireLocalization(bool val) { mRequireLocalization = val; } + bool getPseudolocalize(void) const { return mPseudolocalize; } + void setPseudolocalize(bool val) { mPseudolocalize = val; } + int getCompressionMethod(void) const { return mCompressionMethod; } + void setCompressionMethod(int val) { mCompressionMethod = val; } + const char* getOutputAPKFile() const { return mOutputAPKFile; } + void setOutputAPKFile(const char* val) { mOutputAPKFile = val; } + + /* + * Input options. + */ + const char* getAssetSourceDir() const { return mAssetSourceDir; } + void setAssetSourceDir(const char* dir) { mAssetSourceDir = dir; } + const android::Vector<const char*>& getResourceSourceDirs() const { return mResourceSourceDirs; } + void addResourceSourceDir(const char* dir) { mResourceSourceDirs.insertAt(dir,0); } + const char* getAndroidManifestFile() const { return mAndroidManifestFile; } + void setAndroidManifestFile(const char* file) { mAndroidManifestFile = file; } + const char* getPublicOutputFile() const { return mPublicOutputFile; } + void setPublicOutputFile(const char* file) { mPublicOutputFile = file; } + const char* getRClassDir() const { return mRClassDir; } + void setRClassDir(const char* dir) { mRClassDir = dir; } + const char* getConfigurations() const { return mConfigurations.size() > 0 ? mConfigurations.string() : NULL; } + void addConfigurations(const char* val) { if (mConfigurations.size() > 0) { mConfigurations.append(","); mConfigurations.append(val); } else { mConfigurations = val; } } + const char* getResourceIntermediatesDir() const { return mResourceIntermediatesDir; } + void setResourceIntermediatesDir(const char* dir) { mResourceIntermediatesDir = dir; } + const android::Vector<const char*>& getPackageIncludes() const { return mPackageIncludes; } + void addPackageInclude(const char* file) { mPackageIncludes.add(file); } + const android::Vector<const char*>& getJarFiles() const { return mJarFiles; } + void addJarFile(const char* file) { mJarFiles.add(file); } + const android::Vector<const char*>& getNoCompressExtensions() const { return mNoCompressExtensions; } + void addNoCompressExtension(const char* ext) { mNoCompressExtensions.add(ext); } + + /* + * Set and get the file specification. + * + * Note this does NOT make a copy of argv. + */ + void setFileSpec(char* const argv[], int argc) { + mArgc = argc; + mArgv = argv; + } + int getFileSpecCount(void) const { return mArgc; } + const char* getFileSpecEntry(int idx) const { return mArgv[idx]; } + void eatArgs(int n) { + if (n > mArgc) n = mArgc; + mArgv += n; + mArgc -= n; + } + +#if 0 + /* + * Package count. Nothing to do with anything else here; this is + * just a convenient place to stuff it so we don't have to pass it + * around everywhere. + */ + int getPackageCount(void) const { return mPackageCount; } + void setPackageCount(int val) { mPackageCount = val; } +#endif + +private: + /* commands & modifiers */ + Command mCmd; + bool mVerbose; + bool mAndroidList; + bool mForce; + int mGrayscaleTolerance; + bool mMakePackageDirs; + bool mUpdate; + bool mExtending; + bool mRequireLocalization; + bool mPseudolocalize; + int mCompressionMethod; + const char* mOutputAPKFile; + const char* mAssetSourceDir; + const char* mAndroidManifestFile; + const char* mPublicOutputFile; + const char* mRClassDir; + const char* mResourceIntermediatesDir; + android::String8 mConfigurations; + android::Vector<const char*> mPackageIncludes; + android::Vector<const char*> mJarFiles; + android::Vector<const char*> mNoCompressExtensions; + android::Vector<const char*> mResourceSourceDirs; + + /* file specification */ + int mArgc; + char* const* mArgv; + +#if 0 + /* misc stuff */ + int mPackageCount; +#endif +}; + +#endif // __BUNDLE_H diff --git a/tools/aapt/Command.cpp b/tools/aapt/Command.cpp new file mode 100644 index 0000000..bff0423 --- /dev/null +++ b/tools/aapt/Command.cpp @@ -0,0 +1,841 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Android Asset Packaging Tool main entry point. +// +#include "Main.h" +#include "Bundle.h" +#include "ResourceTable.h" +#include "XMLNode.h" + +#include <utils.h> +#include <utils/ZipFile.h> + +#include <fcntl.h> +#include <errno.h> + +using namespace android; + +/* + * Show version info. All the cool kids do it. + */ +int doVersion(Bundle* bundle) +{ + if (bundle->getFileSpecCount() != 0) + printf("(ignoring extra arguments)\n"); + printf("Android Asset Packaging Tool, v0.2\n"); + + return 0; +} + + +/* + * Open the file read only. The call fails if the file doesn't exist. + * + * Returns NULL on failure. + */ +ZipFile* openReadOnly(const char* fileName) +{ + ZipFile* zip; + status_t result; + + zip = new ZipFile; + result = zip->open(fileName, ZipFile::kOpenReadOnly); + if (result != NO_ERROR) { + if (result == NAME_NOT_FOUND) + fprintf(stderr, "ERROR: '%s' not found\n", fileName); + else if (result == PERMISSION_DENIED) + fprintf(stderr, "ERROR: '%s' access denied\n", fileName); + else + fprintf(stderr, "ERROR: failed opening '%s' as Zip file\n", + fileName); + delete zip; + return NULL; + } + + return zip; +} + +/* + * Open the file read-write. The file will be created if it doesn't + * already exist and "okayToCreate" is set. + * + * Returns NULL on failure. + */ +ZipFile* openReadWrite(const char* fileName, bool okayToCreate) +{ + ZipFile* zip = NULL; + status_t result; + int flags; + + flags = ZipFile::kOpenReadWrite; + if (okayToCreate) + flags |= ZipFile::kOpenCreate; + + zip = new ZipFile; + result = zip->open(fileName, flags); + if (result != NO_ERROR) { + delete zip; + zip = NULL; + goto bail; + } + +bail: + return zip; +} + + +/* + * Return a short string describing the compression method. + */ +const char* compressionName(int method) +{ + if (method == ZipEntry::kCompressStored) + return "Stored"; + else if (method == ZipEntry::kCompressDeflated) + return "Deflated"; + else + return "Unknown"; +} + +/* + * Return the percent reduction in size (0% == no compression). + */ +int calcPercent(long uncompressedLen, long compressedLen) +{ + if (!uncompressedLen) + return 0; + else + return (int) (100.0 - (compressedLen * 100.0) / uncompressedLen + 0.5); +} + +/* + * Handle the "list" command, which can be a simple file dump or + * a verbose listing. + * + * The verbose listing closely matches the output of the Info-ZIP "unzip" + * command. + */ +int doList(Bundle* bundle) +{ + int result = 1; + ZipFile* zip = NULL; + const ZipEntry* entry; + long totalUncLen, totalCompLen; + const char* zipFileName; + + if (bundle->getFileSpecCount() != 1) { + fprintf(stderr, "ERROR: specify zip file name (only)\n"); + goto bail; + } + zipFileName = bundle->getFileSpecEntry(0); + + zip = openReadOnly(zipFileName); + if (zip == NULL) + goto bail; + + int count, i; + + if (bundle->getVerbose()) { + printf("Archive: %s\n", zipFileName); + printf( + " Length Method Size Ratio Date Time CRC-32 Name\n"); + printf( + "-------- ------ ------- ----- ---- ---- ------ ----\n"); + } + + totalUncLen = totalCompLen = 0; + + count = zip->getNumEntries(); + for (i = 0; i < count; i++) { + entry = zip->getEntryByIndex(i); + if (bundle->getVerbose()) { + char dateBuf[32]; + time_t when; + + when = entry->getModWhen(); + strftime(dateBuf, sizeof(dateBuf), "%m-%d-%y %H:%M", + localtime(&when)); + + printf("%8ld %-7.7s %7ld %3d%% %s %08lx %s\n", + (long) entry->getUncompressedLen(), + compressionName(entry->getCompressionMethod()), + (long) entry->getCompressedLen(), + calcPercent(entry->getUncompressedLen(), + entry->getCompressedLen()), + dateBuf, + entry->getCRC32(), + entry->getFileName()); + } else { + printf("%s\n", entry->getFileName()); + } + + totalUncLen += entry->getUncompressedLen(); + totalCompLen += entry->getCompressedLen(); + } + + if (bundle->getVerbose()) { + printf( + "-------- ------- --- -------\n"); + printf("%8ld %7ld %2d%% %d files\n", + totalUncLen, + totalCompLen, + calcPercent(totalUncLen, totalCompLen), + zip->getNumEntries()); + } + + if (bundle->getAndroidList()) { + AssetManager assets; + if (!assets.addAssetPath(String8(zipFileName), NULL)) { + fprintf(stderr, "ERROR: list -a failed because assets could not be loaded\n"); + goto bail; + } + + const ResTable& res = assets.getResources(false); + if (&res == NULL) { + printf("\nNo resource table found.\n"); + } else { + printf("\nResource table:\n"); + res.print(); + } + + Asset* manifestAsset = assets.openNonAsset("AndroidManifest.xml", + Asset::ACCESS_BUFFER); + if (manifestAsset == NULL) { + printf("\nNo AndroidManifest.xml found.\n"); + } else { + printf("\nAndroid manifest:\n"); + ResXMLTree tree; + tree.setTo(manifestAsset->getBuffer(true), + manifestAsset->getLength()); + printXMLBlock(&tree); + } + delete manifestAsset; + } + + result = 0; + +bail: + delete zip; + return result; +} + +static ssize_t indexOfAttribute(const ResXMLTree& tree, uint32_t attrRes) +{ + size_t N = tree.getAttributeCount(); + for (size_t i=0; i<N; i++) { + if (tree.getAttributeNameResID(i) == attrRes) { + return (ssize_t)i; + } + } + return -1; +} + +static String8 getAttribute(const ResXMLTree& tree, const char* ns, + const char* attr, String8* outError) +{ + ssize_t idx = tree.indexOfAttribute(ns, attr); + if (idx < 0) { + return String8(); + } + Res_value value; + if (tree.getAttributeValue(idx, &value) != NO_ERROR) { + if (value.dataType != Res_value::TYPE_STRING) { + if (outError != NULL) *outError = "attribute is not a string value"; + return String8(); + } + } + size_t len; + const uint16_t* str = tree.getAttributeStringValue(idx, &len); + return str ? String8(str, len) : String8(); +} + +static String8 getAttribute(const ResXMLTree& tree, uint32_t attrRes, String8* outError) +{ + ssize_t idx = indexOfAttribute(tree, attrRes); + if (idx < 0) { + return String8(); + } + Res_value value; + if (tree.getAttributeValue(idx, &value) != NO_ERROR) { + if (value.dataType != Res_value::TYPE_STRING) { + if (outError != NULL) *outError = "attribute is not a string value"; + return String8(); + } + } + size_t len; + const uint16_t* str = tree.getAttributeStringValue(idx, &len); + return str ? String8(str, len) : String8(); +} + +static int32_t getIntegerAttribute(const ResXMLTree& tree, uint32_t attrRes, String8* outError) +{ + ssize_t idx = indexOfAttribute(tree, attrRes); + if (idx < 0) { + return -1; + } + Res_value value; + if (tree.getAttributeValue(idx, &value) != NO_ERROR) { + if (value.dataType != Res_value::TYPE_INT_DEC) { + if (outError != NULL) *outError = "attribute is not an integer value"; + return -1; + } + } + return value.data; +} + +static String8 getResolvedAttribute(const ResTable* resTable, const ResXMLTree& tree, + uint32_t attrRes, String8* outError) +{ + ssize_t idx = indexOfAttribute(tree, attrRes); + if (idx < 0) { + return String8(); + } + Res_value value; + if (tree.getAttributeValue(idx, &value) != NO_ERROR) { + if (value.dataType == Res_value::TYPE_STRING) { + size_t len; + const uint16_t* str = tree.getAttributeStringValue(idx, &len); + return str ? String8(str, len) : String8(); + } + resTable->resolveReference(&value, 0); + if (value.dataType != Res_value::TYPE_STRING) { + if (outError != NULL) *outError = "attribute is not a string value"; + return String8(); + } + } + size_t len; + const Res_value* value2 = &value; + const char16_t* str = const_cast<ResTable*>(resTable)->valueToString(value2, 0, NULL, &len); + return str ? String8(str, len) : String8(); +} + +// These are attribute resource constants for the platform, as found +// in android.R.attr +enum { + NAME_ATTR = 0x01010003, + VERSION_CODE_ATTR = 0x0101021b, + VERSION_NAME_ATTR = 0x0101021c, + LABEL_ATTR = 0x01010001, + ICON_ATTR = 0x01010002, +}; + +/* + * Handle the "dump" command, to extract select data from an archive. + */ +int doDump(Bundle* bundle) +{ + status_t result = UNKNOWN_ERROR; + Asset* asset = NULL; + + if (bundle->getFileSpecCount() < 1) { + fprintf(stderr, "ERROR: no dump option specified\n"); + return 1; + } + + if (bundle->getFileSpecCount() < 2) { + fprintf(stderr, "ERROR: no dump file specified\n"); + return 1; + } + + const char* option = bundle->getFileSpecEntry(0); + const char* filename = bundle->getFileSpecEntry(1); + + AssetManager assets; + if (!assets.addAssetPath(String8(filename), NULL)) { + fprintf(stderr, "ERROR: dump failed because assets could not be loaded\n"); + return 1; + } + + const ResTable& res = assets.getResources(false); + if (&res == NULL) { + fprintf(stderr, "ERROR: dump failed because no resource table was found\n"); + goto bail; + } + + if (strcmp("resources", option) == 0) { + res.print(); + + } else if (strcmp("xmltree", option) == 0) { + if (bundle->getFileSpecCount() < 3) { + fprintf(stderr, "ERROR: no dump xmltree resource file specified\n"); + goto bail; + } + + for (int i=2; i<bundle->getFileSpecCount(); i++) { + const char* resname = bundle->getFileSpecEntry(i); + ResXMLTree tree; + asset = assets.openNonAsset(resname, Asset::ACCESS_BUFFER); + if (asset == NULL) { + fprintf(stderr, "ERROR: dump failed because resource %p found\n", resname); + goto bail; + } + + if (tree.setTo(asset->getBuffer(true), + asset->getLength()) != NO_ERROR) { + fprintf(stderr, "ERROR: Resource %s is corrupt\n", resname); + goto bail; + } + tree.restart(); + printXMLBlock(&tree); + delete asset; + asset = NULL; + } + + } else if (strcmp("xmlstrings", option) == 0) { + if (bundle->getFileSpecCount() < 3) { + fprintf(stderr, "ERROR: no dump xmltree resource file specified\n"); + goto bail; + } + + for (int i=2; i<bundle->getFileSpecCount(); i++) { + const char* resname = bundle->getFileSpecEntry(i); + ResXMLTree tree; + asset = assets.openNonAsset(resname, Asset::ACCESS_BUFFER); + if (asset == NULL) { + fprintf(stderr, "ERROR: dump failed because resource %p found\n", resname); + goto bail; + } + + if (tree.setTo(asset->getBuffer(true), + asset->getLength()) != NO_ERROR) { + fprintf(stderr, "ERROR: Resource %s is corrupt\n", resname); + goto bail; + } + printStringPool(&tree.getStrings()); + delete asset; + asset = NULL; + } + + } else { + ResXMLTree tree; + asset = assets.openNonAsset("AndroidManifest.xml", + Asset::ACCESS_BUFFER); + if (asset == NULL) { + fprintf(stderr, "ERROR: dump failed because no AndroidManifest.xml found\n"); + goto bail; + } + + if (tree.setTo(asset->getBuffer(true), + asset->getLength()) != NO_ERROR) { + fprintf(stderr, "ERROR: AndroidManifest.xml is corrupt\n"); + goto bail; + } + tree.restart(); + + if (strcmp("permissions", option) == 0) { + size_t len; + ResXMLTree::event_code_t code; + int depth = 0; + while ((code=tree.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::END_TAG) { + depth--; + continue; + } + if (code != ResXMLTree::START_TAG) { + continue; + } + depth++; + String8 tag(tree.getElementName(&len)); + //printf("Depth %d tag %s\n", depth, tag.string()); + if (depth == 1) { + if (tag != "manifest") { + fprintf(stderr, "ERROR: manifest does not start with <manifest> tag\n"); + goto bail; + } + String8 pkg = getAttribute(tree, NULL, "package", NULL); + printf("package: %s\n", pkg.string()); + } else if (depth == 2 && tag == "permission") { + String8 error; + String8 name = getAttribute(tree, NAME_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR: %s\n", error.string()); + goto bail; + } + printf("permission: %s\n", name.string()); + } else if (depth == 2 && tag == "uses-permission") { + String8 error; + String8 name = getAttribute(tree, NAME_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR: %s\n", error.string()); + goto bail; + } + printf("uses-permission: %s\n", name.string()); + } + } + } else if (strcmp("badging", option) == 0) { + size_t len; + ResXMLTree::event_code_t code; + int depth = 0; + String8 error; + bool withinActivity = false; + bool isMainActivity = false; + bool isLauncherActivity = false; + String8 activityName; + String8 activityLabel; + String8 activityIcon; + while ((code=tree.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::END_TAG) { + depth--; + continue; + } + if (code != ResXMLTree::START_TAG) { + continue; + } + depth++; + String8 tag(tree.getElementName(&len)); + //printf("Depth %d tag %s\n", depth, tag.string()); + if (depth == 1) { + if (tag != "manifest") { + fprintf(stderr, "ERROR: manifest does not start with <manifest> tag\n"); + goto bail; + } + String8 pkg = getAttribute(tree, NULL, "package", NULL); + printf("package: name='%s' ", pkg.string()); + int32_t versionCode = getIntegerAttribute(tree, VERSION_CODE_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:versionCode' attribute: %s\n", error.string()); + goto bail; + } + if (versionCode > 0) { + printf("versionCode='%d' ", versionCode); + } else { + printf("versionCode='' "); + } + String8 versionName = getAttribute(tree, VERSION_NAME_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:versionName' attribute: %s\n", error.string()); + goto bail; + } + printf("versionName='%s'\n", versionName.string()); + } else if (depth == 2 && tag == "application") { + String8 label = getResolvedAttribute(&res, tree, LABEL_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:label' attribute: %s\n", error.string()); + goto bail; + } + printf("application: label='%s' ", label.string()); + + String8 icon = getResolvedAttribute(&res, tree, ICON_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:icon' attribute: %s\n", error.string()); + goto bail; + } + printf("icon='%s'\n", icon.string()); + } else if (depth == 3 && tag == "activity") { + withinActivity = true; + //printf("LOG: withinActivity==true\n"); + + activityName = getAttribute(tree, NAME_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:name' attribute: %s\n", error.string()); + goto bail; + } + + activityLabel = getResolvedAttribute(&res, tree, LABEL_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:label' attribute: %s\n", error.string()); + goto bail; + } + + activityIcon = getResolvedAttribute(&res, tree, ICON_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:icon' attribute: %s\n", error.string()); + goto bail; + } + } else if (depth == 5 && withinActivity) { + if (tag == "action") { + //printf("LOG: action tag\n"); + String8 action = getAttribute(tree, NAME_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:name' attribute: %s\n", error.string()); + goto bail; + } + if (action == "android.intent.action.MAIN") { + isMainActivity = true; + //printf("LOG: isMainActivity==true\n"); + } + } else if (tag == "category") { + String8 category = getAttribute(tree, NAME_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'name' attribute: %s\n", error.string()); + goto bail; + } + if (category == "android.intent.category.LAUNCHER") { + isLauncherActivity = true; + //printf("LOG: isLauncherActivity==true\n"); + } + } + } + + if (depth < 3) { + //if (withinActivity) printf("LOG: withinActivity==false\n"); + withinActivity = false; + } + + if (depth < 5) { + //if (isMainActivity) printf("LOG: isMainActivity==false\n"); + //if (isLauncherActivity) printf("LOG: isLauncherActivity==false\n"); + isMainActivity = false; + isLauncherActivity = false; + } + + if (withinActivity && isMainActivity && isLauncherActivity) { + printf("launchable activity: name='%s' label='%s' icon='%s'\n", + activityName.string(), activityLabel.string(), + activityIcon.string()); + } + } + printf("locales:"); + Vector<String8> locales; + res.getLocales(&locales); + const size_t N = locales.size(); + for (size_t i=0; i<N; i++) { + const char* localeStr = locales[i].string(); + if (localeStr == NULL || strlen(localeStr) == 0) { + localeStr = "--_--"; + } + printf(" '%s'", localeStr); + } + printf("\n"); + } else if (strcmp("configurations", option) == 0) { + Vector<ResTable_config> configs; + res.getConfigurations(&configs); + const size_t N = configs.size(); + for (size_t i=0; i<N; i++) { + printf("%s\n", configs[i].toString().string()); + } + } else { + fprintf(stderr, "ERROR: unknown dump option '%s'\n", option); + goto bail; + } + } + + result = NO_ERROR; + +bail: + if (asset) { + delete asset; + } + return (result != NO_ERROR); +} + + +/* + * Handle the "add" command, which wants to add files to a new or + * pre-existing archive. + */ +int doAdd(Bundle* bundle) +{ + ZipFile* zip = NULL; + status_t result = UNKNOWN_ERROR; + const char* zipFileName; + + if (bundle->getUpdate()) { + /* avoid confusion */ + fprintf(stderr, "ERROR: can't use '-u' with add\n"); + goto bail; + } + + if (bundle->getFileSpecCount() < 1) { + fprintf(stderr, "ERROR: must specify zip file name\n"); + goto bail; + } + zipFileName = bundle->getFileSpecEntry(0); + + if (bundle->getFileSpecCount() < 2) { + fprintf(stderr, "NOTE: nothing to do\n"); + goto bail; + } + + zip = openReadWrite(zipFileName, true); + if (zip == NULL) { + fprintf(stderr, "ERROR: failed opening/creating '%s' as Zip file\n", zipFileName); + goto bail; + } + + for (int i = 1; i < bundle->getFileSpecCount(); i++) { + const char* fileName = bundle->getFileSpecEntry(i); + + if (strcasecmp(String8(fileName).getPathExtension().string(), ".gz") == 0) { + printf(" '%s'... (from gzip)\n", fileName); + result = zip->addGzip(fileName, String8(fileName).getBasePath().string(), NULL); + } else { + printf(" '%s'...\n", fileName); + result = zip->add(fileName, bundle->getCompressionMethod(), NULL); + } + if (result != NO_ERROR) { + fprintf(stderr, "Unable to add '%s' to '%s'", bundle->getFileSpecEntry(i), zipFileName); + if (result == NAME_NOT_FOUND) + fprintf(stderr, ": file not found\n"); + else if (result == ALREADY_EXISTS) + fprintf(stderr, ": already exists in archive\n"); + else + fprintf(stderr, "\n"); + goto bail; + } + } + + result = NO_ERROR; + +bail: + delete zip; + return (result != NO_ERROR); +} + + +/* + * Delete files from an existing archive. + */ +int doRemove(Bundle* bundle) +{ + ZipFile* zip = NULL; + status_t result = UNKNOWN_ERROR; + const char* zipFileName; + + if (bundle->getFileSpecCount() < 1) { + fprintf(stderr, "ERROR: must specify zip file name\n"); + goto bail; + } + zipFileName = bundle->getFileSpecEntry(0); + + if (bundle->getFileSpecCount() < 2) { + fprintf(stderr, "NOTE: nothing to do\n"); + goto bail; + } + + zip = openReadWrite(zipFileName, false); + if (zip == NULL) { + fprintf(stderr, "ERROR: failed opening Zip archive '%s'\n", + zipFileName); + goto bail; + } + + for (int i = 1; i < bundle->getFileSpecCount(); i++) { + const char* fileName = bundle->getFileSpecEntry(i); + ZipEntry* entry; + + entry = zip->getEntryByName(fileName); + if (entry == NULL) { + printf(" '%s' NOT FOUND\n", fileName); + continue; + } + + result = zip->remove(entry); + + if (result != NO_ERROR) { + fprintf(stderr, "Unable to delete '%s' from '%s'\n", + bundle->getFileSpecEntry(i), zipFileName); + goto bail; + } + } + + /* update the archive */ + zip->flush(); + +bail: + delete zip; + return (result != NO_ERROR); +} + + +/* + * Package up an asset directory and associated application files. + */ +int doPackage(Bundle* bundle) +{ + const char* outputAPKFile; + int retVal = 1; + status_t err; + sp<AaptAssets> assets; + int N; + + // -c zz_ZZ means do pseudolocalization + ResourceFilter filter; + err = filter.parse(bundle->getConfigurations()); + if (err != NO_ERROR) { + goto bail; + } + if (filter.containsPseudo()) { + bundle->setPseudolocalize(true); + } + + N = bundle->getFileSpecCount(); + if (N < 1 && bundle->getResourceSourceDirs().size() == 0 && bundle->getJarFiles().size() == 0 + && bundle->getAndroidManifestFile() == NULL && bundle->getAssetSourceDir() == NULL) { + fprintf(stderr, "ERROR: no input files\n"); + goto bail; + } + + outputAPKFile = bundle->getOutputAPKFile(); + + // Make sure the filenames provided exist and are of the appropriate type. + if (outputAPKFile) { + FileType type; + type = getFileType(outputAPKFile); + if (type != kFileTypeNonexistent && type != kFileTypeRegular) { + fprintf(stderr, + "ERROR: output file '%s' exists but is not regular file\n", + outputAPKFile); + goto bail; + } + } + + // Load the assets. + assets = new AaptAssets(); + err = assets->slurpFromArgs(bundle); + if (err < 0) { + goto bail; + } + + if (bundle->getVerbose()) { + assets->print(); + } + + // If they asked for any files that need to be compiled, do so. + if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) { + err = buildResources(bundle, assets); + if (err != 0) { + goto bail; + } + } + + // At this point we've read everything and processed everything. From here + // on out it's just writing output files. + if (SourcePos::hasErrors()) { + goto bail; + } + + // Write out R.java constants + if (assets->getPackage() == assets->getSymbolsPrivatePackage()) { + err = writeResourceSymbols(bundle, assets, assets->getPackage(), true); + if (err < 0) { + goto bail; + } + } else { + err = writeResourceSymbols(bundle, assets, assets->getPackage(), false); + if (err < 0) { + goto bail; + } + err = writeResourceSymbols(bundle, assets, assets->getSymbolsPrivatePackage(), true); + if (err < 0) { + goto bail; + } + } + + // Write the apk + if (outputAPKFile) { + err = writeAPK(bundle, assets, String8(outputAPKFile)); + if (err != NO_ERROR) { + fprintf(stderr, "ERROR: packaging of '%s' failed\n", outputAPKFile); + goto bail; + } + } + + retVal = 0; +bail: + if (SourcePos::hasErrors()) { + SourcePos::printErrors(stderr); + } + return retVal; +} diff --git a/tools/aapt/Images.cpp b/tools/aapt/Images.cpp new file mode 100644 index 0000000..edb12ea --- /dev/null +++ b/tools/aapt/Images.cpp @@ -0,0 +1,1082 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#define PNG_INTERNAL + +#include "Images.h" + +#include <utils/ResourceTypes.h> +#include <utils/ByteOrder.h> + +#include <png.h> + +#define NOISY(x) //x + +static void +png_write_aapt_file(png_structp png_ptr, png_bytep data, png_size_t length) +{ + status_t err = ((AaptFile*)png_ptr->io_ptr)->writeData(data, length); + if (err != NO_ERROR) { + png_error(png_ptr, "Write Error"); + } +} + + +static void +png_flush_aapt_file(png_structp png_ptr) +{ +} + +// This holds an image as 8bpp RGBA. +struct image_info +{ + image_info() : rows(NULL), is9Patch(false), allocRows(NULL) { } + ~image_info() { + if (rows && rows != allocRows) { + free(rows); + } + if (allocRows) { + for (int i=0; i<(int)allocHeight; i++) { + free(allocRows[i]); + } + free(allocRows); + } + } + + png_uint_32 width; + png_uint_32 height; + png_bytepp rows; + + // 9-patch info. + bool is9Patch; + Res_png_9patch info9Patch; + + png_uint_32 allocHeight; + png_bytepp allocRows; +}; + +static void read_png(const char* imageName, + png_structp read_ptr, png_infop read_info, + image_info* outImageInfo) +{ + int color_type; + int bit_depth, interlace_type, compression_type; + int i; + + png_read_info(read_ptr, read_info); + + png_get_IHDR(read_ptr, read_info, &outImageInfo->width, + &outImageInfo->height, &bit_depth, &color_type, + &interlace_type, &compression_type, NULL); + + //printf("Image %s:\n", imageName); + //printf("color_type=%d, bit_depth=%d, interlace_type=%d, compression_type=%d\n", + // color_type, bit_depth, interlace_type, compression_type); + + if (color_type == PNG_COLOR_TYPE_PALETTE) + png_set_palette_to_rgb(read_ptr); + + if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8) + png_set_gray_1_2_4_to_8(read_ptr); + + if (png_get_valid(read_ptr, read_info, PNG_INFO_tRNS)) { + //printf("Has PNG_INFO_tRNS!\n"); + png_set_tRNS_to_alpha(read_ptr); + } + + if (bit_depth == 16) + png_set_strip_16(read_ptr); + + if ((color_type&PNG_COLOR_MASK_ALPHA) == 0) + png_set_add_alpha(read_ptr, 0xFF, PNG_FILLER_AFTER); + + if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) + png_set_gray_to_rgb(read_ptr); + + png_read_update_info(read_ptr, read_info); + + outImageInfo->rows = (png_bytepp)malloc( + outImageInfo->height * png_sizeof(png_bytep)); + outImageInfo->allocHeight = outImageInfo->height; + outImageInfo->allocRows = outImageInfo->rows; + + png_set_rows(read_ptr, read_info, outImageInfo->rows); + + for (i = 0; i < (int)outImageInfo->height; i++) + { + outImageInfo->rows[i] = (png_bytep) + malloc(png_get_rowbytes(read_ptr, read_info)); + } + + png_read_image(read_ptr, outImageInfo->rows); + + png_read_end(read_ptr, read_info); + + NOISY(printf("Image %s: w=%d, h=%d, d=%d, colors=%d, inter=%d, comp=%d\n", + imageName, + (int)outImageInfo->width, (int)outImageInfo->height, + bit_depth, color_type, + interlace_type, compression_type)); + + png_get_IHDR(read_ptr, read_info, &outImageInfo->width, + &outImageInfo->height, &bit_depth, &color_type, + &interlace_type, &compression_type, NULL); +} + +static bool is_tick(png_bytep p, bool transparent, const char** outError) +{ + if (transparent) { + if (p[3] == 0) { + return false; + } + if (p[3] != 0xff) { + *outError = "Frame pixels must be either solid or transparent (not intermediate alphas)"; + return false; + } + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in transparent frame must be black"; + } + return true; + } + + if (p[3] != 0xFF) { + *outError = "White frame must be a solid color (no alpha)"; + } + if (p[0] == 0xFF && p[1] == 0xFF && p[2] == 0xFF) { + return false; + } + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in white frame must be black"; + return false; + } + return true; +} + +enum { + TICK_START, + TICK_INSIDE_1, + TICK_OUTSIDE_1 +}; + +static status_t get_horizontal_ticks( + png_bytep row, int width, bool transparent, bool required, + int32_t* outLeft, int32_t* outRight, const char** outError, + uint8_t* outDivs, bool multipleAllowed) +{ + int i; + *outLeft = *outRight = -1; + int state = TICK_START; + bool found = false; + + for (i=1; i<width-1; i++) { + if (is_tick(row+i*4, transparent, outError)) { + if (state == TICK_START || + (state == TICK_OUTSIDE_1 && multipleAllowed)) { + *outLeft = i-1; + *outRight = width-2; + found = true; + if (outDivs != NULL) { + *outDivs += 2; + } + state = TICK_INSIDE_1; + } else if (state == TICK_OUTSIDE_1) { + *outError = "Can't have more than one marked region along edge"; + *outLeft = i; + return UNKNOWN_ERROR; + } + } else if (*outError == NULL) { + if (state == TICK_INSIDE_1) { + // We're done with this div. Move on to the next. + *outRight = i-1; + outRight += 2; + outLeft += 2; + state = TICK_OUTSIDE_1; + } + } else { + *outLeft = i; + return UNKNOWN_ERROR; + } + } + + if (required && !found) { + *outError = "No marked region found along edge"; + *outLeft = -1; + return UNKNOWN_ERROR; + } + + return NO_ERROR; +} + +static status_t get_vertical_ticks( + png_bytepp rows, int offset, int height, bool transparent, bool required, + int32_t* outTop, int32_t* outBottom, const char** outError, + uint8_t* outDivs, bool multipleAllowed) +{ + int i; + *outTop = *outBottom = -1; + int state = TICK_START; + bool found = false; + + for (i=1; i<height-1; i++) { + if (is_tick(rows[i]+offset, transparent, outError)) { + if (state == TICK_START || + (state == TICK_OUTSIDE_1 && multipleAllowed)) { + *outTop = i-1; + *outBottom = height-2; + found = true; + if (outDivs != NULL) { + *outDivs += 2; + } + state = TICK_INSIDE_1; + } else if (state == TICK_OUTSIDE_1) { + *outError = "Can't have more than one marked region along edge"; + *outTop = i; + return UNKNOWN_ERROR; + } + } else if (*outError == NULL) { + if (state == TICK_INSIDE_1) { + // We're done with this div. Move on to the next. + *outBottom = i-1; + outTop += 2; + outBottom += 2; + state = TICK_OUTSIDE_1; + } + } else { + *outTop = i; + return UNKNOWN_ERROR; + } + } + + if (required && !found) { + *outError = "No marked region found along edge"; + *outTop = -1; + return UNKNOWN_ERROR; + } + + return NO_ERROR; +} + +static uint32_t get_color( + png_bytepp rows, int left, int top, int right, int bottom) +{ + png_bytep color = rows[top] + left*4; + + if (left > right || top > bottom) { + return Res_png_9patch::TRANSPARENT_COLOR; + } + + while (top <= bottom) { + for (int i = left; i <= right; i++) { + png_bytep p = rows[top]+i*4; + if (color[3] == 0) { + if (p[3] != 0) { + return Res_png_9patch::NO_COLOR; + } + } else if (p[0] != color[0] || p[1] != color[1] + || p[2] != color[2] || p[3] != color[3]) { + return Res_png_9patch::NO_COLOR; + } + } + top++; + } + + if (color[3] == 0) { + return Res_png_9patch::TRANSPARENT_COLOR; + } + return (color[3]<<24) | (color[0]<<16) | (color[1]<<8) | color[2]; +} + +static void select_patch( + int which, int front, int back, int size, int* start, int* end) +{ + switch (which) { + case 0: + *start = 0; + *end = front-1; + break; + case 1: + *start = front; + *end = back-1; + break; + case 2: + *start = back; + *end = size-1; + break; + } +} + +static uint32_t get_color(image_info* image, int hpatch, int vpatch) +{ + int left, right, top, bottom; + select_patch( + hpatch, image->info9Patch.xDivs[0], image->info9Patch.xDivs[1], + image->width, &left, &right); + select_patch( + vpatch, image->info9Patch.yDivs[0], image->info9Patch.yDivs[1], + image->height, &top, &bottom); + //printf("Selecting h=%d v=%d: (%d,%d)-(%d,%d)\n", + // hpatch, vpatch, left, top, right, bottom); + const uint32_t c = get_color(image->rows, left, top, right, bottom); + NOISY(printf("Color in (%d,%d)-(%d,%d): #%08x\n", left, top, right, bottom, c)); + return c; +} + +static status_t do_9patch(const char* imageName, image_info* image) +{ + image->is9Patch = true; + + int W = image->width; + int H = image->height; + int i, j; + + int maxSizeXDivs = (W / 2 + 1) * sizeof(int32_t); + int maxSizeYDivs = (H / 2 + 1) * sizeof(int32_t); + int32_t* xDivs = (int32_t*) malloc(maxSizeXDivs); + int32_t* yDivs = (int32_t*) malloc(maxSizeYDivs); + uint8_t numXDivs = 0; + uint8_t numYDivs = 0; + int8_t numColors; + int numRows; + int numCols; + int top; + int left; + int right; + int bottom; + memset(xDivs, -1, maxSizeXDivs); + memset(yDivs, -1, maxSizeYDivs); + image->info9Patch.paddingLeft = image->info9Patch.paddingRight = + image->info9Patch.paddingTop = image->info9Patch.paddingBottom = -1; + + png_bytep p = image->rows[0]; + bool transparent = p[3] == 0; + bool hasColor = false; + + const char* errorMsg = NULL; + int errorPixel = -1; + const char* errorEdge = ""; + + int colorIndex = 0; + + // Validate size... + if (W < 3 || H < 3) { + errorMsg = "Image must be at least 3x3 (1x1 without frame) pixels"; + goto getout; + } + + // Validate frame... + if (!transparent && + (p[0] != 0xFF || p[1] != 0xFF || p[2] != 0xFF || p[3] != 0xFF)) { + errorMsg = "Must have one-pixel frame that is either transparent or white"; + goto getout; + } + + // Find left and right of sizing areas... + if (get_horizontal_ticks(p, W, transparent, true, &xDivs[0], + &xDivs[1], &errorMsg, &numXDivs, true) != NO_ERROR) { + errorPixel = xDivs[0]; + errorEdge = "top"; + goto getout; + } + + // Find top and bottom of sizing areas... + if (get_vertical_ticks(image->rows, 0, H, transparent, true, &yDivs[0], + &yDivs[1], &errorMsg, &numYDivs, true) != NO_ERROR) { + errorPixel = yDivs[0]; + errorEdge = "left"; + goto getout; + } + + // Find left and right of padding area... + if (get_horizontal_ticks(image->rows[H-1], W, transparent, false, &image->info9Patch.paddingLeft, + &image->info9Patch.paddingRight, &errorMsg, NULL, false) != NO_ERROR) { + errorPixel = image->info9Patch.paddingLeft; + errorEdge = "bottom"; + goto getout; + } + + // Find top and bottom of padding area... + if (get_vertical_ticks(image->rows, (W-1)*4, H, transparent, false, &image->info9Patch.paddingTop, + &image->info9Patch.paddingBottom, &errorMsg, NULL, false) != NO_ERROR) { + errorPixel = image->info9Patch.paddingTop; + errorEdge = "right"; + goto getout; + } + + // Copy patch data into image + image->info9Patch.numXDivs = numXDivs; + image->info9Patch.numYDivs = numYDivs; + image->info9Patch.xDivs = xDivs; + image->info9Patch.yDivs = yDivs; + + // If padding is not yet specified, take values from size. + if (image->info9Patch.paddingLeft < 0) { + image->info9Patch.paddingLeft = xDivs[0]; + image->info9Patch.paddingRight = W - 2 - xDivs[1]; + } else { + // Adjust value to be correct! + image->info9Patch.paddingRight = W - 2 - image->info9Patch.paddingRight; + } + if (image->info9Patch.paddingTop < 0) { + image->info9Patch.paddingTop = yDivs[0]; + image->info9Patch.paddingBottom = H - 2 - yDivs[1]; + } else { + // Adjust value to be correct! + image->info9Patch.paddingBottom = H - 2 - image->info9Patch.paddingBottom; + } + + NOISY(printf("Size ticks for %s: x0=%d, x1=%d, y0=%d, y1=%d\n", imageName, + image->info9Patch.xDivs[0], image->info9Patch.xDivs[1], + image->info9Patch.yDivs[0], image->info9Patch.yDivs[1])); + NOISY(printf("padding ticks for %s: l=%d, r=%d, t=%d, b=%d\n", imageName, + image->info9Patch.paddingLeft, image->info9Patch.paddingRight, + image->info9Patch.paddingTop, image->info9Patch.paddingBottom)); + + // Remove frame from image. + image->rows = (png_bytepp)malloc((H-2) * png_sizeof(png_bytep)); + for (i=0; i<(H-2); i++) { + image->rows[i] = image->allocRows[i+1]; + memmove(image->rows[i], image->rows[i]+4, (W-2)*4); + } + image->width -= 2; + W = image->width; + image->height -= 2; + H = image->height; + + // Figure out the number of rows and columns in the N-patch + numCols = numXDivs + 1; + if (xDivs[0] == 0) { // Column 1 is strechable + numCols--; + } + if (xDivs[numXDivs - 1] == W) { + numCols--; + } + numRows = numYDivs + 1; + if (yDivs[0] == 0) { // Row 1 is strechable + numRows--; + } + if (yDivs[numYDivs - 1] == H) { + numRows--; + } + numColors = numRows * numCols; + image->info9Patch.numColors = numColors; + image->info9Patch.colors = (uint32_t*)malloc(numColors * sizeof(uint32_t)); + + // Fill in color information for each patch. + + uint32_t c; + top = 0; + + // The first row always starts with the top being at y=0 and the bottom + // being either yDivs[1] (if yDivs[0]=0) of yDivs[0]. In the former case + // the first row is stretchable along the Y axis, otherwise it is fixed. + // The last row always ends with the bottom being bitmap.height and the top + // being either yDivs[numYDivs-2] (if yDivs[numYDivs-1]=bitmap.height) or + // yDivs[numYDivs-1]. In the former case the last row is stretchable along + // the Y axis, otherwise it is fixed. + // + // The first and last columns are similarly treated with respect to the X + // axis. + // + // The above is to help explain some of the special casing that goes on the + // code below. + + // The initial yDiv and whether the first row is considered stretchable or + // not depends on whether yDiv[0] was zero or not. + for (j = (yDivs[0] == 0 ? 1 : 0); + j <= numYDivs && top < H; + j++) { + if (j == numYDivs) { + bottom = H; + } else { + bottom = yDivs[j]; + } + left = 0; + // The initial xDiv and whether the first column is considered + // stretchable or not depends on whether xDiv[0] was zero or not. + for (i = xDivs[0] == 0 ? 1 : 0; + i <= numXDivs && left < W; + i++) { + if (i == numXDivs) { + right = W; + } else { + right = xDivs[i]; + } + c = get_color(image->rows, left, top, right - 1, bottom - 1); + image->info9Patch.colors[colorIndex++] = c; + NOISY(if (c != Res_png_9patch::NO_COLOR) hasColor = true); + left = right; + } + top = bottom; + } + + assert(colorIndex == numColors); + + for (i=0; i<numColors; i++) { + if (hasColor) { + if (i == 0) printf("Colors in %s:\n ", imageName); + printf(" #%08x", image->info9Patch.colors[i]); + if (i == numColors - 1) printf("\n"); + } + } + + image->is9Patch = true; + image->info9Patch.deviceToFile(); + +getout: + if (errorMsg) { + fprintf(stderr, + "ERROR: 9-patch image %s malformed.\n" + " %s.\n", imageName, errorMsg); + if (errorPixel >= 0) { + fprintf(stderr, + " Found at pixel #%d along %s edge.\n", errorPixel, errorEdge); + } else { + fprintf(stderr, + " Found along %s edge.\n", errorEdge); + } + return UNKNOWN_ERROR; + } + return NO_ERROR; +} + +static void checkNinePatchSerialization(Res_png_9patch* inPatch, void * data) +{ + if (sizeof(void*) != sizeof(int32_t)) { + // can't deserialize on a non-32 bit system + return; + } + size_t patchSize = inPatch->serializedSize(); + void * newData = malloc(patchSize); + memcpy(newData, data, patchSize); + Res_png_9patch* outPatch = inPatch->deserialize(newData); + // deserialization is done in place, so outPatch == newData + assert(outPatch == newData); + assert(outPatch->numXDivs == inPatch->numXDivs); + assert(outPatch->numYDivs == inPatch->numYDivs); + assert(outPatch->paddingLeft == inPatch->paddingLeft); + assert(outPatch->paddingRight == inPatch->paddingRight); + assert(outPatch->paddingTop == inPatch->paddingTop); + assert(outPatch->paddingBottom == inPatch->paddingBottom); + for (int i = 0; i < outPatch->numXDivs; i++) { + assert(outPatch->xDivs[i] == inPatch->xDivs[i]); + } + for (int i = 0; i < outPatch->numYDivs; i++) { + assert(outPatch->yDivs[i] == inPatch->yDivs[i]); + } + for (int i = 0; i < outPatch->numColors; i++) { + assert(outPatch->colors[i] == inPatch->colors[i]); + } + free(newData); +} + +static bool patch_equals(Res_png_9patch& patch1, Res_png_9patch& patch2) { + if (!(patch1.numXDivs == patch2.numXDivs && + patch1.numYDivs == patch2.numYDivs && + patch1.numColors == patch2.numColors && + patch1.paddingLeft == patch2.paddingLeft && + patch1.paddingRight == patch2.paddingRight && + patch1.paddingTop == patch2.paddingTop && + patch1.paddingBottom == patch2.paddingBottom)) { + return false; + } + for (int i = 0; i < patch1.numColors; i++) { + if (patch1.colors[i] != patch2.colors[i]) { + return false; + } + } + for (int i = 0; i < patch1.numXDivs; i++) { + if (patch1.xDivs[i] != patch2.xDivs[i]) { + return false; + } + } + for (int i = 0; i < patch1.numYDivs; i++) { + if (patch1.yDivs[i] != patch2.yDivs[i]) { + return false; + } + } + return true; +} + +static void dump_image(int w, int h, png_bytepp rows, int color_type) +{ + int i, j, rr, gg, bb, aa; + + int bpp; + if (color_type == PNG_COLOR_TYPE_PALETTE || color_type == PNG_COLOR_TYPE_GRAY) { + bpp = 1; + } else if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { + bpp = 2; + } else if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA) { + // We use a padding byte even when there is no alpha + bpp = 4; + } else { + printf("Unknown color type %d.\n", color_type); + } + + for (j = 0; j < h; j++) { + png_bytep row = rows[j]; + for (i = 0; i < w; i++) { + rr = row[0]; + gg = row[1]; + bb = row[2]; + aa = row[3]; + row += bpp; + + if (i == 0) { + printf("Row %d:", j); + } + switch (bpp) { + case 1: + printf(" (%d)", rr); + break; + case 2: + printf(" (%d %d", rr, gg); + break; + case 3: + printf(" (%d %d %d)", rr, gg, bb); + break; + case 4: + printf(" (%d %d %d %d)", rr, gg, bb, aa); + break; + } + if (i == (w - 1)) { + NOISY(printf("\n")); + } + } + } +} + +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define ABS(a) ((a)<0?-(a):(a)) + +static void analyze_image(const char *imageName, image_info &imageInfo, int grayscaleTolerance, + png_colorp rgbPalette, png_bytep alphaPalette, + int *paletteEntries, bool *hasTransparency, int *colorType, + png_bytepp outRows) +{ + int w = imageInfo.width; + int h = imageInfo.height; + int i, j, rr, gg, bb, aa, idx; + uint32_t colors[256], col; + int num_colors = 0; + int maxGrayDeviation = 0; + + bool isOpaque = true; + bool isPalette = true; + bool isGrayscale = true; + + // Scan the entire image and determine if: + // 1. Every pixel has R == G == B (grayscale) + // 2. Every pixel has A == 255 (opaque) + // 3. There are no more than 256 distinct RGBA colors + + // NOISY(printf("Initial image data:\n")); + // dump_image(w, h, imageInfo.rows, PNG_COLOR_TYPE_RGB_ALPHA); + + for (j = 0; j < h; j++) { + png_bytep row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + rr = *row++; + gg = *row++; + bb = *row++; + aa = *row++; + + int odev = maxGrayDeviation; + maxGrayDeviation = MAX(ABS(rr - gg), maxGrayDeviation); + maxGrayDeviation = MAX(ABS(gg - bb), maxGrayDeviation); + maxGrayDeviation = MAX(ABS(bb - rr), maxGrayDeviation); + if (maxGrayDeviation > odev) { + NOISY(printf("New max dev. = %d at pixel (%d, %d) = (%d %d %d %d)\n", + maxGrayDeviation, i, j, rr, gg, bb, aa)); + } + + // Check if image is really grayscale + if (isGrayscale) { + if (rr != gg || rr != bb) { + NOISY(printf("Found a non-gray pixel at %d, %d = (%d %d %d %d)\n", + i, j, rr, gg, bb, aa)); + isGrayscale = false; + } + } + + // Check if image is really opaque + if (isOpaque) { + if (aa != 0xff) { + NOISY(printf("Found a non-opaque pixel at %d, %d = (%d %d %d %d)\n", + i, j, rr, gg, bb, aa)); + isOpaque = false; + } + } + + // Check if image is really <= 256 colors + if (isPalette) { + col = (uint32_t) ((rr << 24) | (gg << 16) | (bb << 8) | aa); + bool match = false; + for (idx = 0; idx < num_colors; idx++) { + if (colors[idx] == col) { + match = true; + break; + } + } + + // Write the palette index for the pixel to outRows optimistically + // We might overwrite it later if we decide to encode as gray or + // gray + alpha + *out++ = idx; + if (!match) { + if (num_colors == 256) { + NOISY(printf("Found 257th color at %d, %d\n", i, j)); + isPalette = false; + } else { + colors[num_colors++] = col; + } + } + } + } + } + + *paletteEntries = 0; + *hasTransparency = !isOpaque; + int bpp = isOpaque ? 3 : 4; + int paletteSize = w * h + bpp * num_colors; + + NOISY(printf("isGrayscale = %s\n", isGrayscale ? "true" : "false")); + NOISY(printf("isOpaque = %s\n", isOpaque ? "true" : "false")); + NOISY(printf("isPalette = %s\n", isPalette ? "true" : "false")); + NOISY(printf("Size w/ palette = %d, gray+alpha = %d, rgb(a) = %d\n", + paletteSize, 2 * w * h, bpp * w * h)); + NOISY(printf("Max gray deviation = %d, tolerance = %d\n", maxGrayDeviation, grayscaleTolerance)); + + // Choose the best color type for the image. + // 1. Opaque gray - use COLOR_TYPE_GRAY at 1 byte/pixel + // 2. Gray + alpha - use COLOR_TYPE_PALETTE if the number of distinct combinations + // is sufficiently small, otherwise use COLOR_TYPE_GRAY_ALPHA + // 3. RGB(A) - use COLOR_TYPE_PALETTE if the number of distinct colors is sufficiently + // small, otherwise use COLOR_TYPE_RGB{_ALPHA} + if (isGrayscale) { + if (isOpaque) { + *colorType = PNG_COLOR_TYPE_GRAY; // 1 byte/pixel + } else { + // Use a simple heuristic to determine whether using a palette will + // save space versus using gray + alpha for each pixel. + // This doesn't take into account chunk overhead, filtering, LZ + // compression, etc. + if (isPalette && (paletteSize < 2 * w * h)) { + *colorType = PNG_COLOR_TYPE_PALETTE; // 1 byte/pixel + 4 bytes/color + } else { + *colorType = PNG_COLOR_TYPE_GRAY_ALPHA; // 2 bytes per pixel + } + } + } else if (isPalette && (paletteSize < bpp * w * h)) { + *colorType = PNG_COLOR_TYPE_PALETTE; + } else { + if (maxGrayDeviation <= grayscaleTolerance) { + printf("%s: forcing image to gray (max deviation = %d)\n", imageName, maxGrayDeviation); + *colorType = isOpaque ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_GRAY_ALPHA; + } else { + *colorType = isOpaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA; + } + } + + // Perform postprocessing of the image or palette data based on the final + // color type chosen + + if (*colorType == PNG_COLOR_TYPE_PALETTE) { + // Create separate RGB and Alpha palettes and set the number of colors + *paletteEntries = num_colors; + + // Create the RGB and alpha palettes + for (int idx = 0; idx < num_colors; idx++) { + col = colors[idx]; + rgbPalette[idx].red = (png_byte) ((col >> 24) & 0xff); + rgbPalette[idx].green = (png_byte) ((col >> 16) & 0xff); + rgbPalette[idx].blue = (png_byte) ((col >> 8) & 0xff); + alphaPalette[idx] = (png_byte) (col & 0xff); + } + } else if (*colorType == PNG_COLOR_TYPE_GRAY || *colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { + // If the image is gray or gray + alpha, compact the pixels into outRows + for (j = 0; j < h; j++) { + png_bytep row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + rr = *row++; + gg = *row++; + bb = *row++; + aa = *row++; + + if (isGrayscale) { + *out++ = rr; + } else { + *out++ = (png_byte) (rr * 0.2126f + gg * 0.7152f + bb * 0.0722f); + } + if (!isOpaque) { + *out++ = aa; + } + } + } + } +} + + +static void write_png(const char* imageName, + png_structp write_ptr, png_infop write_info, + image_info& imageInfo, int grayscaleTolerance) +{ + bool optimize = true; + png_uint_32 width, height; + int color_type; + int bit_depth, interlace_type, compression_type; + int i; + + png_unknown_chunk unknowns[1]; + + png_bytepp outRows = (png_bytepp) malloc((int) imageInfo.height * png_sizeof(png_bytep)); + if (outRows == (png_bytepp) 0) { + printf("Can't allocate output buffer!\n"); + exit(1); + } + for (i = 0; i < (int) imageInfo.height; i++) { + outRows[i] = (png_bytep) malloc(2 * (int) imageInfo.width); + if (outRows[i] == (png_bytep) 0) { + printf("Can't allocate output buffer!\n"); + exit(1); + } + } + + png_set_compression_level(write_ptr, Z_BEST_COMPRESSION); + + NOISY(printf("Writing image %s: w = %d, h = %d\n", imageName, + (int) imageInfo.width, (int) imageInfo.height)); + + png_color rgbPalette[256]; + png_byte alphaPalette[256]; + bool hasTransparency; + int paletteEntries; + + analyze_image(imageName, imageInfo, grayscaleTolerance, rgbPalette, alphaPalette, + &paletteEntries, &hasTransparency, &color_type, outRows); + + // If the image is a 9-patch, we need to preserve it as a ARGB file to make + // sure the pixels will not be pre-dithered/clamped until we decide they are + if (imageInfo.is9Patch && (color_type == PNG_COLOR_TYPE_RGB || + color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)) { + color_type = PNG_COLOR_TYPE_RGB_ALPHA; + } + + switch (color_type) { + case PNG_COLOR_TYPE_PALETTE: + NOISY(printf("Image %s has %d colors%s, using PNG_COLOR_TYPE_PALETTE\n", + imageName, paletteEntries, + hasTransparency ? " (with alpha)" : "")); + break; + case PNG_COLOR_TYPE_GRAY: + NOISY(printf("Image %s is opaque gray, using PNG_COLOR_TYPE_GRAY\n", imageName)); + break; + case PNG_COLOR_TYPE_GRAY_ALPHA: + NOISY(printf("Image %s is gray + alpha, using PNG_COLOR_TYPE_GRAY_ALPHA\n", imageName)); + break; + case PNG_COLOR_TYPE_RGB: + NOISY(printf("Image %s is opaque RGB, using PNG_COLOR_TYPE_RGB\n", imageName)); + break; + case PNG_COLOR_TYPE_RGB_ALPHA: + NOISY(printf("Image %s is RGB + alpha, using PNG_COLOR_TYPE_RGB_ALPHA\n", imageName)); + break; + } + + png_set_IHDR(write_ptr, write_info, imageInfo.width, imageInfo.height, + 8, color_type, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + if (color_type == PNG_COLOR_TYPE_PALETTE) { + png_set_PLTE(write_ptr, write_info, rgbPalette, paletteEntries); + if (hasTransparency) { + png_set_tRNS(write_ptr, write_info, alphaPalette, paletteEntries, (png_color_16p) 0); + } + png_set_filter(write_ptr, 0, PNG_NO_FILTERS); + } else { + png_set_filter(write_ptr, 0, PNG_ALL_FILTERS); + } + + if (imageInfo.is9Patch) { + NOISY(printf("Adding 9-patch info...\n")); + strcpy((char*)unknowns[0].name, "npTc"); + unknowns[0].data = (png_byte*)imageInfo.info9Patch.serialize(); + unknowns[0].size = imageInfo.info9Patch.serializedSize(); + // TODO: remove the check below when everything works + checkNinePatchSerialization(&imageInfo.info9Patch, unknowns[0].data); + png_set_keep_unknown_chunks(write_ptr, PNG_HANDLE_CHUNK_ALWAYS, + (png_byte*)"npTc", 1); + png_set_unknown_chunks(write_ptr, write_info, unknowns, 1); + // XXX I can't get this to work without forcibly changing + // the location to what I want... which apparently is supposed + // to be a private API, but everything else I have tried results + // in the location being set to what I -last- wrote so I never + // get written. :p + png_set_unknown_chunk_location(write_ptr, write_info, 0, PNG_HAVE_PLTE); + } + + png_write_info(write_ptr, write_info); + + png_bytepp rows; + if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA) { + png_set_filler(write_ptr, 0, PNG_FILLER_AFTER); + rows = imageInfo.rows; + } else { + rows = outRows; + } + png_write_image(write_ptr, rows); + +// NOISY(printf("Final image data:\n")); +// dump_image(imageInfo.width, imageInfo.height, rows, color_type); + + png_write_end(write_ptr, write_info); + + for (i = 0; i < (int) imageInfo.height; i++) { + free(outRows[i]); + } + free(outRows); + + png_get_IHDR(write_ptr, write_info, &width, &height, + &bit_depth, &color_type, &interlace_type, + &compression_type, NULL); + + NOISY(printf("Image written: w=%d, h=%d, d=%d, colors=%d, inter=%d, comp=%d\n", + (int)width, (int)height, bit_depth, color_type, interlace_type, + compression_type)); +} + +status_t preProcessImage(Bundle* bundle, const sp<AaptAssets>& assets, + const sp<AaptFile>& file, String8* outNewLeafName) +{ + String8 ext(file->getPath().getPathExtension()); + + // We currently only process PNG images. + if (strcmp(ext.string(), ".png") != 0) { + return NO_ERROR; + } + + // Example of renaming a file: + //*outNewLeafName = file->getPath().getBasePath().getFileName(); + //outNewLeafName->append(".nupng"); + + String8 printableName(file->getPrintableSource()); + + png_structp read_ptr = NULL; + png_infop read_info = NULL; + FILE* fp; + + image_info imageInfo; + + png_structp write_ptr = NULL; + png_infop write_info = NULL; + + status_t error = UNKNOWN_ERROR; + + const size_t nameLen = file->getPath().length(); + + fp = fopen(file->getSourceFile().string(), "rb"); + if (fp == NULL) { + fprintf(stderr, "%s: ERROR: Unable to open PNG file\n", printableName.string()); + goto bail; + } + + read_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, (png_error_ptr)NULL, + (png_error_ptr)NULL); + if (!read_ptr) { + goto bail; + } + + read_info = png_create_info_struct(read_ptr); + if (!read_info) { + goto bail; + } + + if (setjmp(png_jmpbuf(read_ptr))) { + goto bail; + } + + png_init_io(read_ptr, fp); + + read_png(printableName.string(), read_ptr, read_info, &imageInfo); + + if (nameLen > 6) { + const char* name = file->getPath().string(); + if (name[nameLen-5] == '9' && name[nameLen-6] == '.') { + if (do_9patch(printableName.string(), &imageInfo) != NO_ERROR) { + goto bail; + } + } + } + + write_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, (png_error_ptr)NULL, + (png_error_ptr)NULL); + if (!write_ptr) + { + goto bail; + } + + write_info = png_create_info_struct(write_ptr); + if (!write_info) + { + goto bail; + } + + png_set_write_fn(write_ptr, (void*)file.get(), + png_write_aapt_file, png_flush_aapt_file); + + if (setjmp(png_jmpbuf(write_ptr))) + { + goto bail; + } + + write_png(printableName.string(), write_ptr, write_info, imageInfo, + bundle->getGrayscaleTolerance()); + + error = NO_ERROR; + + if (bundle->getVerbose()) { + fseek(fp, 0, SEEK_END); + size_t oldSize = (size_t)ftell(fp); + size_t newSize = file->getSize(); + float factor = ((float)newSize)/oldSize; + int percent = (int)(factor*100); + printf(" (processed image %s: %d%% size of source)\n", printableName.string(), percent); + } + +bail: + if (read_ptr) { + png_destroy_read_struct(&read_ptr, &read_info, (png_infopp)NULL); + } + if (fp) { + fclose(fp); + } + if (write_ptr) { + png_destroy_write_struct(&write_ptr, &write_info); + } + + if (error != NO_ERROR) { + fprintf(stderr, "ERROR: Failure processing PNG image %s\n", + file->getPrintableSource().string()); + } + return error; +} + + + +status_t postProcessImage(const sp<AaptAssets>& assets, + ResourceTable* table, const sp<AaptFile>& file) +{ + String8 ext(file->getPath().getPathExtension()); + + // At this point, now that we have all the resource data, all we need to + // do is compile XML files. + if (strcmp(ext.string(), ".xml") == 0) { + return compileXmlFile(assets, file, table); + } + + return NO_ERROR; +} diff --git a/tools/aapt/Images.h b/tools/aapt/Images.h new file mode 100644 index 0000000..168e22f --- /dev/null +++ b/tools/aapt/Images.h @@ -0,0 +1,18 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#ifndef IMAGES_H +#define IMAGES_H + +#include "ResourceTable.h" + +status_t preProcessImage(Bundle* bundle, const sp<AaptAssets>& assets, + const sp<AaptFile>& file, String8* outNewLeafName); + +status_t postProcessImage(const sp<AaptAssets>& assets, + ResourceTable* table, const sp<AaptFile>& file); + +#endif diff --git a/tools/aapt/Main.cpp b/tools/aapt/Main.cpp new file mode 100644 index 0000000..71b1a3c --- /dev/null +++ b/tools/aapt/Main.cpp @@ -0,0 +1,369 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Android Asset Packaging Tool main entry point. +// +#include "Main.h" +#include "Bundle.h" + +#include <utils.h> +#include <utils/ZipFile.h> + +#include <stdlib.h> +#include <getopt.h> +#include <assert.h> + +using namespace android; + +static const char* gProgName = "aapt"; + +/* + * When running under Cygwin on Windows, this will convert slash-based + * paths into back-slash-based ones. Otherwise the ApptAssets file comparisons + * fail later as they use back-slash separators under Windows. + * + * This operates in-place on the path string. + */ +void convertPath(char *path) { + if (path != NULL && OS_PATH_SEPARATOR != '/') { + for (; *path; path++) { + if (*path == '/') { + *path = OS_PATH_SEPARATOR; + } + } + } +} + +/* + * Print usage info. + */ +void usage(void) +{ + fprintf(stderr, "Android Asset Packaging Tool\n\n"); + fprintf(stderr, "Usage:\n"); + fprintf(stderr, + " %s l[ist] [-v] [-a] file.{zip,jar,apk}\n" + " List contents of Zip-compatible archive.\n\n", gProgName); + fprintf(stderr, + " %s d[ump] WHAT file.{apk} [asset [asset ...]]\n" + " badging Print the label and icon for the app declared in APK.\n" + " permissions Print the permissions from the APK.\n" + " resources Print the resource table from the APK.\n" + " configurations Print the configurations in the APK.\n" + " xmltree Print the compiled xmls in the given assets.\n" + " xmlstrings Print the strings of the given compiled xml assets.\n\n", gProgName); + fprintf(stderr, + " %s p[ackage] [-d][-f][-m][-u][-v][-x][-z][-M AndroidManifest.xml] \\\n" + " [-0 extension [-0 extension ...]] \\\n" + " [-g tolerance] \\\n" + " [-j jarfile] \\\n" + " [-I base-package [-I base-package ...]] \\\n" + " [-A asset-source-dir] [-P public-definitions-file] \\\n" + " [-S resource-sources [-S resource-sources ...]] " + " [-F apk-file] [-J R-file-dir] \\\n" + " [raw-files-dir [raw-files-dir] ...]\n" + "\n" + " Package the android resources. It will read assets and resources that are\n" + " supplied with the -M -A -S or raw-files-dir arguments. The -J -P -F and -R\n" + " options control which files are output.\n\n" + , gProgName); + fprintf(stderr, + " %s r[emove] [-v] file.{zip,jar,apk} file1 [file2 ...]\n" + " Delete specified files from Zip-compatible archive.\n\n", + gProgName); + fprintf(stderr, + " %s a[dd] [-v] file.{zip,jar,apk} file1 [file2 ...]\n" + " Add specified files to Zip-compatible archive.\n\n", gProgName); + fprintf(stderr, + " %s v[ersion]\n" + " Print program version.\n\n", gProgName); + fprintf(stderr, + " Modifiers:\n" + " -a print Android-specific data (resources, manifest) when listing\n" + " -c specify which configurations to include. The default is all\n" + " configurations. The value of the parameter should be a comma\n" + " separated list of configuration values. Locales should be specified\n" + " as either a language or language-region pair. Some examples:\n" + " en\n" + " port,en\n" + " port,land,en_US\n" + " If you put the special locale, zz_ZZ on the list, it will perform\n" + " pseudolocalization on the default locale, modifying all of the\n" + " strings so you can look for strings that missed the\n" + " internationalization process. For example:\n" + " port,land,zz_ZZ\n" + " -d one or more device assets to include, separated by commas\n" + " -f force overwrite of existing files\n" + " -g specify a pixel tolerance to force images to grayscale, default 0\n" + " -j specify a jar or zip file containing classes to include\n" + " -m make package directories under location specified by -J\n" +#if 0 + " -p pseudolocalize the default configuration\n" +#endif + " -u update existing packages (add new, replace older, remove deleted files)\n" + " -v verbose output\n" + " -x create extending (non-application) resource IDs\n" + " -z require localization of resource attributes marked with\n" + " localization=\"suggested\"\n" + " -A additional directory in which to find raw asset files\n" + " -F specify the apk file to output\n" + " -I add an existing package to base include set\n" + " -J specify where to output R.java resource constant definitions\n" + " -M specify full path to AndroidManifest.xml to include in zip\n" + " -P specify where to output public resource definitions\n" + " -S directory in which to find resources. Multiple directories will be scanned" + " and the first match found (left to right) will take precedence." + " -0 specifies an additional extension for which such files will not\n" + " be stored compressed in the .apk. An empty string means to not\n" + " compress any files at all.\n"); +} + +/* + * Dispatch the command. + */ +int handleCommand(Bundle* bundle) +{ + //printf("--- command %d (verbose=%d force=%d):\n", + // bundle->getCommand(), bundle->getVerbose(), bundle->getForce()); + //for (int i = 0; i < bundle->getFileSpecCount(); i++) + // printf(" %d: '%s'\n", i, bundle->getFileSpecEntry(i)); + + switch (bundle->getCommand()) { + case kCommandVersion: return doVersion(bundle); + case kCommandList: return doList(bundle); + case kCommandDump: return doDump(bundle); + case kCommandAdd: return doAdd(bundle); + case kCommandRemove: return doRemove(bundle); + case kCommandPackage: return doPackage(bundle); + default: + fprintf(stderr, "%s: requested command not yet supported\n", gProgName); + return 1; + } +} + +/* + * Parse args. + */ +int main(int argc, char* const argv[]) +{ + char *prog = argv[0]; + Bundle bundle; + bool wantUsage = false; + int result = 1; // pessimistically assume an error. + int tolerance = 0; + + /* default to compression */ + bundle.setCompressionMethod(ZipEntry::kCompressDeflated); + + if (argc < 2) { + wantUsage = true; + goto bail; + } + + if (argv[1][0] == 'v') + bundle.setCommand(kCommandVersion); + else if (argv[1][0] == 'd') + bundle.setCommand(kCommandDump); + else if (argv[1][0] == 'l') + bundle.setCommand(kCommandList); + else if (argv[1][0] == 'a') + bundle.setCommand(kCommandAdd); + else if (argv[1][0] == 'r') + bundle.setCommand(kCommandRemove); + else if (argv[1][0] == 'p') + bundle.setCommand(kCommandPackage); + else { + fprintf(stderr, "ERROR: Unknown command '%s'\n", argv[1]); + wantUsage = true; + goto bail; + } + argc -= 2; + argv += 2; + + /* + * Pull out flags. We support "-fv" and "-f -v". + */ + while (argc && argv[0][0] == '-') { + /* flag(s) found */ + const char* cp = argv[0] +1; + + while (*cp != '\0') { + switch (*cp) { + case 'v': + bundle.setVerbose(true); + break; + case 'a': + bundle.setAndroidList(true); + break; + case 'c': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-c' option\n"); + wantUsage = true; + goto bail; + } + bundle.addConfigurations(argv[0]); + break; + case 'f': + bundle.setForce(true); + break; + case 'g': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-g' option\n"); + wantUsage = true; + goto bail; + } + tolerance = atoi(argv[0]); + bundle.setGrayscaleTolerance(tolerance); + printf("%s: Images with deviation <= %d will be forced to grayscale.\n", prog, tolerance); + break; + case 'm': + bundle.setMakePackageDirs(true); + break; +#if 0 + case 'p': + bundle.setPseudolocalize(true); + break; +#endif + case 'u': + bundle.setUpdate(true); + break; + case 'x': + bundle.setExtending(true); + break; + case 'z': + bundle.setRequireLocalization(true); + break; + case 'j': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-j' option\n"); + wantUsage = true; + goto bail; + } + convertPath(argv[0]); + bundle.addJarFile(argv[0]); + break; + case 'A': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-A' option\n"); + wantUsage = true; + goto bail; + } + convertPath(argv[0]); + bundle.setAssetSourceDir(argv[0]); + break; + case 'I': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-I' option\n"); + wantUsage = true; + goto bail; + } + convertPath(argv[0]); + bundle.addPackageInclude(argv[0]); + break; + case 'F': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-F' option\n"); + wantUsage = true; + goto bail; + } + convertPath(argv[0]); + bundle.setOutputAPKFile(argv[0]); + break; + case 'J': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-J' option\n"); + wantUsage = true; + goto bail; + } + convertPath(argv[0]); + bundle.setRClassDir(argv[0]); + break; + case 'M': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-M' option\n"); + wantUsage = true; + goto bail; + } + convertPath(argv[0]); + bundle.setAndroidManifestFile(argv[0]); + break; + case 'P': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-P' option\n"); + wantUsage = true; + goto bail; + } + convertPath(argv[0]); + bundle.setPublicOutputFile(argv[0]); + break; + case 'S': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-S' option\n"); + wantUsage = true; + goto bail; + } + convertPath(argv[0]); + bundle.addResourceSourceDir(argv[0]); + break; + case '0': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-e' option\n"); + wantUsage = true; + goto bail; + } + if (argv[0][0] != 0) { + bundle.addNoCompressExtension(argv[0]); + } else { + bundle.setCompressionMethod(ZipEntry::kCompressStored); + } + break; + default: + fprintf(stderr, "ERROR: Unknown flag '-%c'\n", *cp); + wantUsage = true; + goto bail; + } + + cp++; + } + argc--; + argv++; + } + + /* + * We're past the flags. The rest all goes straight in. + */ + bundle.setFileSpec(argv, argc); + + result = handleCommand(&bundle); + +bail: + if (wantUsage) { + usage(); + result = 2; + } + + //printf("--> returning %d\n", result); + return result; +} diff --git a/tools/aapt/Main.h b/tools/aapt/Main.h new file mode 100644 index 0000000..65c0a8a --- /dev/null +++ b/tools/aapt/Main.h @@ -0,0 +1,41 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Some global defines that don't really merit their own header. +// +#ifndef __MAIN_H +#define __MAIN_H + +#include <utils.h> +#include "Bundle.h" +#include "AaptAssets.h" +#include <utils/ZipFile.h> + +extern int doVersion(Bundle* bundle); +extern int doList(Bundle* bundle); +extern int doDump(Bundle* bundle); +extern int doAdd(Bundle* bundle); +extern int doRemove(Bundle* bundle); +extern int doPackage(Bundle* bundle); + +extern int calcPercent(long uncompressedLen, long compressedLen); + +extern android::status_t writeAPK(Bundle* bundle, + const sp<AaptAssets>& assets, + const android::String8& outputFile); + +extern android::status_t buildResources(Bundle* bundle, + const sp<AaptAssets>& assets); + +extern android::status_t writeResourceSymbols(Bundle* bundle, + const sp<AaptAssets>& assets, const String8& pkgName, bool includePrivate); + +extern bool isValidResourceType(const String8& type); + +ssize_t processAssets(Bundle* bundle, ZipFile* zip, const sp<AaptAssets>& assets); + +extern status_t filterResources(Bundle* bundle, const sp<AaptAssets>& assets); + +int dumpResources(Bundle* bundle); + +#endif // __MAIN_H diff --git a/tools/aapt/Package.cpp b/tools/aapt/Package.cpp new file mode 100644 index 0000000..eb7d6f5 --- /dev/null +++ b/tools/aapt/Package.cpp @@ -0,0 +1,464 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Package assets into Zip files. +// +#include "Main.h" +#include "AaptAssets.h" +#include "ResourceTable.h" + +#include <utils.h> +#include <utils/ZipFile.h> + +#include <sys/types.h> +#include <dirent.h> +#include <ctype.h> +#include <errno.h> + +using namespace android; + +static const char* kExcludeExtension = ".EXCLUDE"; + +/* these formats are already compressed, or don't compress well */ +static const char* kNoCompressExt[] = { + ".jpg", ".jpeg", ".png", ".gif", + ".wav", ".mp2", ".mp3", ".ogg", ".aac", + ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet", + ".rtttl", ".imy", ".xmf", ".mp4", ".m4a", + ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2", + ".amr", ".awb", ".wma", ".wmv" +}; + +/* fwd decls, so I can write this downward */ +ssize_t processAssets(Bundle* bundle, ZipFile* zip, const sp<AaptAssets>& assets); +ssize_t processAssets(Bundle* bundle, ZipFile* zip, + const sp<AaptDir>& dir, const AaptGroupEntry& ge); +bool processFile(Bundle* bundle, ZipFile* zip, + const sp<AaptGroup>& group, const sp<AaptFile>& file); +bool okayToCompress(Bundle* bundle, const String8& pathName); +ssize_t processJarFiles(Bundle* bundle, ZipFile* zip); + +/* + * The directory hierarchy looks like this: + * "outputDir" and "assetRoot" are existing directories. + * + * On success, "bundle->numPackages" will be the number of Zip packages + * we created. + */ +status_t writeAPK(Bundle* bundle, const sp<AaptAssets>& assets, + const String8& outputFile) +{ + status_t result = NO_ERROR; + ZipFile* zip = NULL; + int count; + + //bundle->setPackageCount(0); + + /* + * Prep the Zip archive. + * + * If the file already exists, fail unless "update" or "force" is set. + * If "update" is set, update the contents of the existing archive. + * Else, if "force" is set, remove the existing archive. + */ + FileType fileType = getFileType(outputFile.string()); + if (fileType == kFileTypeNonexistent) { + // okay, create it below + } else if (fileType == kFileTypeRegular) { + if (bundle->getUpdate()) { + // okay, open it below + } else if (bundle->getForce()) { + if (unlink(outputFile.string()) != 0) { + fprintf(stderr, "ERROR: unable to remove '%s': %s\n", outputFile.string(), + strerror(errno)); + goto bail; + } + } else { + fprintf(stderr, "ERROR: '%s' exists (use '-f' to force overwrite)\n", + outputFile.string()); + goto bail; + } + } else { + fprintf(stderr, "ERROR: '%s' exists and is not a regular file\n", outputFile.string()); + goto bail; + } + + if (bundle->getVerbose()) { + printf("%s '%s'\n", (fileType == kFileTypeNonexistent) ? "Creating" : "Opening", + outputFile.string()); + } + + status_t status; + zip = new ZipFile; + status = zip->open(outputFile.string(), ZipFile::kOpenReadWrite | ZipFile::kOpenCreate); + if (status != NO_ERROR) { + fprintf(stderr, "ERROR: unable to open '%s' as Zip file for writing\n", + outputFile.string()); + goto bail; + } + + if (bundle->getVerbose()) { + printf("Writing all files...\n"); + } + + count = processAssets(bundle, zip, assets); + if (count < 0) { + fprintf(stderr, "ERROR: unable to process assets while packaging '%s'\n", + outputFile.string()); + result = count; + goto bail; + } + + if (bundle->getVerbose()) { + printf("Generated %d file%s\n", count, (count==1) ? "" : "s"); + } + + count = processJarFiles(bundle, zip); + if (count < 0) { + fprintf(stderr, "ERROR: unable to process jar files while packaging '%s'\n", + outputFile.string()); + result = count; + goto bail; + } + + if (bundle->getVerbose()) + printf("Included %d file%s from jar/zip files.\n", count, (count==1) ? "" : "s"); + + result = NO_ERROR; + + /* + * Check for cruft. We set the "marked" flag on all entries we created + * or decided not to update. If the entry isn't already slated for + * deletion, remove it now. + */ + { + if (bundle->getVerbose()) + printf("Checking for deleted files\n"); + int i, removed = 0; + for (i = 0; i < zip->getNumEntries(); i++) { + ZipEntry* entry = zip->getEntryByIndex(i); + + if (!entry->getMarked() && entry->getDeleted()) { + if (bundle->getVerbose()) { + printf(" (removing crufty '%s')\n", + entry->getFileName()); + } + zip->remove(entry); + removed++; + } + } + if (bundle->getVerbose() && removed > 0) + printf("Removed %d file%s\n", removed, (removed==1) ? "" : "s"); + } + + /* tell Zip lib to process deletions and other pending changes */ + result = zip->flush(); + if (result != NO_ERROR) { + fprintf(stderr, "ERROR: Zip flush failed, archive may be hosed\n"); + goto bail; + } + + /* anything here? */ + if (zip->getNumEntries() == 0) { + if (bundle->getVerbose()) { + printf("Archive is empty -- removing %s\n", outputFile.getPathLeaf().string()); + } + delete zip; // close the file so we can remove it in Win32 + zip = NULL; + if (unlink(outputFile.string()) != 0) { + fprintf(stderr, "WARNING: could not unlink '%s'\n", outputFile.string()); + } + } + + assert(result == NO_ERROR); + +bail: + delete zip; // must close before remove in Win32 + if (result != NO_ERROR) { + if (bundle->getVerbose()) { + printf("Removing %s due to earlier failures\n", outputFile.string()); + } + if (unlink(outputFile.string()) != 0) { + fprintf(stderr, "WARNING: could not unlink '%s'\n", outputFile.string()); + } + } + + if (result == NO_ERROR && bundle->getVerbose()) + printf("Done!\n"); + return result; +} + +ssize_t processAssets(Bundle* bundle, ZipFile* zip, + const sp<AaptAssets>& assets) +{ + ResourceFilter filter; + status_t status = filter.parse(bundle->getConfigurations()); + if (status != NO_ERROR) { + return -1; + } + + ssize_t count = 0; + + const size_t N = assets->getGroupEntries().size(); + for (size_t i=0; i<N; i++) { + const AaptGroupEntry& ge = assets->getGroupEntries()[i]; + if (!filter.match(ge.toParams())) { + continue; + } + ssize_t res = processAssets(bundle, zip, assets, ge); + if (res < 0) { + return res; + } + count += res; + } + + return count; +} + +ssize_t processAssets(Bundle* bundle, ZipFile* zip, + const sp<AaptDir>& dir, const AaptGroupEntry& ge) +{ + ssize_t count = 0; + + const size_t ND = dir->getDirs().size(); + size_t i; + for (i=0; i<ND; i++) { + ssize_t res = processAssets(bundle, zip, dir->getDirs().valueAt(i), ge); + if (res < 0) { + return res; + } + count += res; + } + + const size_t NF = dir->getFiles().size(); + for (i=0; i<NF; i++) { + sp<AaptGroup> gp = dir->getFiles().valueAt(i); + ssize_t fi = gp->getFiles().indexOfKey(ge); + if (fi >= 0) { + sp<AaptFile> fl = gp->getFiles().valueAt(fi); + if (!processFile(bundle, zip, gp, fl)) { + return UNKNOWN_ERROR; + } + count++; + } + } + + return count; +} + +/* + * Process a regular file, adding it to the archive if appropriate. + * + * If we're in "update" mode, and the file already exists in the archive, + * delete the existing entry before adding the new one. + */ +bool processFile(Bundle* bundle, ZipFile* zip, + const sp<AaptGroup>& group, const sp<AaptFile>& file) +{ + const bool hasData = file->hasData(); + + String8 storageName(group->getPath()); + storageName.convertToResPath(); + ZipEntry* entry; + bool fromGzip = false; + status_t result; + + /* + * See if the filename ends in ".EXCLUDE". We can't use + * String8::getPathExtension() because the length of what it considers + * to be an extension is capped. + * + * The Asset Manager doesn't check for ".EXCLUDE" in Zip archives, + * so there's no value in adding them (and it makes life easier on + * the AssetManager lib if we don't). + * + * NOTE: this restriction has been removed. If you're in this code, you + * should clean this up, but I'm in here getting rid of Path Name, and I + * don't want to make other potentially breaking changes --joeo + */ + int fileNameLen = storageName.length(); + int excludeExtensionLen = strlen(kExcludeExtension); + if (fileNameLen > excludeExtensionLen + && (0 == strcmp(storageName.string() + (fileNameLen - excludeExtensionLen), + kExcludeExtension))) { + fprintf(stderr, "WARNING: '%s' not added to Zip\n", storageName.string()); + return true; + } + + if (strcasecmp(storageName.getPathExtension().string(), ".gz") == 0) { + fromGzip = true; + storageName = storageName.getBasePath(); + } + + if (bundle->getUpdate()) { + entry = zip->getEntryByName(storageName.string()); + if (entry != NULL) { + /* file already exists in archive; there can be only one */ + if (entry->getMarked()) { + fprintf(stderr, + "ERROR: '%s' exists twice (check for with & w/o '.gz'?)\n", + file->getPrintableSource().string()); + return false; + } + if (!hasData) { + const String8& srcName = file->getSourceFile(); + time_t fileModWhen; + fileModWhen = getFileModDate(srcName.string()); + if (fileModWhen == (time_t) -1) { // file existence tested earlier, + return false; // not expecting an error here + } + + if (fileModWhen > entry->getModWhen()) { + // mark as deleted so add() will succeed + if (bundle->getVerbose()) { + printf(" (removing old '%s')\n", storageName.string()); + } + + zip->remove(entry); + } else { + // version in archive is newer + if (bundle->getVerbose()) { + printf(" (not updating '%s')\n", storageName.string()); + } + entry->setMarked(true); + return true; + } + } else { + // Generated files are always replaced. + zip->remove(entry); + } + } + } + + //android_setMinPriority(NULL, ANDROID_LOG_VERBOSE); + + if (fromGzip) { + result = zip->addGzip(file->getSourceFile().string(), storageName.string(), &entry); + } else if (!hasData) { + /* don't compress certain files, e.g. PNGs */ + int compressionMethod = bundle->getCompressionMethod(); + if (!okayToCompress(bundle, storageName)) { + compressionMethod = ZipEntry::kCompressStored; + } + result = zip->add(file->getSourceFile().string(), storageName.string(), compressionMethod, + &entry); + } else { + result = zip->add(file->getData(), file->getSize(), storageName.string(), + file->getCompressionMethod(), &entry); + } + if (result == NO_ERROR) { + if (bundle->getVerbose()) { + printf(" '%s'%s", storageName.string(), fromGzip ? " (from .gz)" : ""); + if (entry->getCompressionMethod() == ZipEntry::kCompressStored) { + printf(" (not compressed)\n"); + } else { + printf(" (compressed %d%%)\n", calcPercent(entry->getUncompressedLen(), + entry->getCompressedLen())); + } + } + entry->setMarked(true); + } else { + if (result == ALREADY_EXISTS) { + fprintf(stderr, " Unable to add '%s': file already in archive (try '-u'?)\n", + file->getPrintableSource().string()); + } else { + fprintf(stderr, " Unable to add '%s': Zip add failed\n", + file->getPrintableSource().string()); + } + return false; + } + + return true; +} + +/* + * Determine whether or not we want to try to compress this file based + * on the file extension. + */ +bool okayToCompress(Bundle* bundle, const String8& pathName) +{ + String8 ext = pathName.getPathExtension(); + int i; + + if (ext.length() == 0) + return true; + + for (i = 0; i < NELEM(kNoCompressExt); i++) { + if (strcasecmp(ext.string(), kNoCompressExt[i]) == 0) + return false; + } + + const android::Vector<const char*>& others(bundle->getNoCompressExtensions()); + for (i = 0; i < (int)others.size(); i++) { + const char* str = others[i]; + int pos = pathName.length() - strlen(str); + if (pos < 0) { + continue; + } + const char* path = pathName.string(); + if (strcasecmp(path + pos, str) == 0) { + return false; + } + } + + return true; +} + +bool endsWith(const char* haystack, const char* needle) +{ + size_t a = strlen(haystack); + size_t b = strlen(needle); + if (a < b) return false; + return strcasecmp(haystack+(a-b), needle) == 0; +} + +ssize_t processJarFile(ZipFile* jar, ZipFile* out) +{ + status_t err; + size_t N = jar->getNumEntries(); + size_t count = 0; + for (size_t i=0; i<N; i++) { + ZipEntry* entry = jar->getEntryByIndex(i); + const char* storageName = entry->getFileName(); + if (endsWith(storageName, ".class")) { + int compressionMethod = entry->getCompressionMethod(); + size_t size = entry->getUncompressedLen(); + const void* data = jar->uncompress(entry); + if (data == NULL) { + fprintf(stderr, "ERROR: unable to uncompress entry '%s'\n", + storageName); + return -1; + } + out->add(data, size, storageName, compressionMethod, NULL); + free((void*)data); + } + count++; + } + return count; +} + +ssize_t processJarFiles(Bundle* bundle, ZipFile* zip) +{ + ssize_t err; + ssize_t count = 0; + const android::Vector<const char*>& jars = bundle->getJarFiles(); + + size_t N = jars.size(); + for (size_t i=0; i<N; i++) { + ZipFile jar; + err = jar.open(jars[i], ZipFile::kOpenReadOnly); + if (err != 0) { + fprintf(stderr, "ERROR: unable to open '%s' as a zip file: %zd\n", + jars[i], err); + return err; + } + err += processJarFile(&jar, zip); + if (err < 0) { + fprintf(stderr, "ERROR: unable to process '%s'\n", jars[i]); + return err; + } + count += err; + } + + return count; +} diff --git a/tools/aapt/Resource.cpp b/tools/aapt/Resource.cpp new file mode 100644 index 0000000..b2bd9ff --- /dev/null +++ b/tools/aapt/Resource.cpp @@ -0,0 +1,1524 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// +#include "Main.h" +#include "AaptAssets.h" +#include "StringPool.h" +#include "XMLNode.h" +#include "ResourceTable.h" +#include "Images.h" + +#define NOISY(x) // x + +// ========================================================================== +// ========================================================================== +// ========================================================================== + +class PackageInfo +{ +public: + PackageInfo() + { + } + ~PackageInfo() + { + } + + status_t parsePackage(const sp<AaptGroup>& grp); +}; + +// ========================================================================== +// ========================================================================== +// ========================================================================== + +static String8 parseResourceName(const String8& leaf) +{ + const char* firstDot = strchr(leaf.string(), '.'); + const char* str = leaf.string(); + + if (firstDot) { + return String8(str, firstDot-str); + } else { + return String8(str); + } +} + +class ResourceTypeSet : public RefBase, + public KeyedVector<String8,sp<AaptGroup> > +{ +public: + ResourceTypeSet(); +}; + +ResourceTypeSet::ResourceTypeSet() + :RefBase(), + KeyedVector<String8,sp<AaptGroup> >() +{ +} + +class ResourceDirIterator +{ +public: + ResourceDirIterator(const sp<ResourceTypeSet>& set, const String8& resType) + : mResType(resType), mSet(set), mSetPos(0), mGroupPos(0) + { + } + + inline const sp<AaptGroup>& getGroup() const { return mGroup; } + inline const sp<AaptFile>& getFile() const { return mFile; } + + inline const String8& getBaseName() const { return mBaseName; } + inline const String8& getLeafName() const { return mLeafName; } + inline String8 getPath() const { return mPath; } + inline const ResTable_config& getParams() const { return mParams; } + + enum { + EOD = 1 + }; + + ssize_t next() + { + while (true) { + sp<AaptGroup> group; + sp<AaptFile> file; + + // Try to get next file in this current group. + if (mGroup != NULL && mGroupPos < mGroup->getFiles().size()) { + group = mGroup; + file = group->getFiles().valueAt(mGroupPos++); + + // Try to get the next group/file in this directory + } else if (mSetPos < mSet->size()) { + mGroup = group = mSet->valueAt(mSetPos++); + if (group->getFiles().size() < 1) { + continue; + } + file = group->getFiles().valueAt(0); + mGroupPos = 1; + + // All done! + } else { + return EOD; + } + + mFile = file; + + String8 leaf(group->getLeaf()); + mLeafName = String8(leaf); + mParams = file->getGroupEntry().toParams(); + NOISY(printf("Dir %s: mcc=%d mnc=%d lang=%c%c cnt=%c%c orient=%d density=%d touch=%d key=%d inp=%d nav=%d\n", + group->getPath().string(), mParams.mcc, mParams.mnc, + mParams.language[0] ? mParams.language[0] : '-', + mParams.language[1] ? mParams.language[1] : '-', + mParams.country[0] ? mParams.country[0] : '-', + mParams.country[1] ? mParams.country[1] : '-', + mParams.orientation, + mParams.density, mParams.touchscreen, mParams.keyboard, + mParams.inputFlags, mParams.navigation)); + mPath = "res"; + mPath.appendPath(file->getGroupEntry().toDirName(mResType)); + mPath.appendPath(leaf); + mBaseName = parseResourceName(leaf); + if (mBaseName == "") { + fprintf(stderr, "Error: malformed resource filename %s\n", + file->getPrintableSource().string()); + return UNKNOWN_ERROR; + } + + NOISY(printf("file name=%s\n", mBaseName.string())); + + return NO_ERROR; + } + } + +private: + String8 mResType; + + const sp<ResourceTypeSet> mSet; + size_t mSetPos; + + sp<AaptGroup> mGroup; + size_t mGroupPos; + + sp<AaptFile> mFile; + String8 mBaseName; + String8 mLeafName; + String8 mPath; + ResTable_config mParams; +}; + +// ========================================================================== +// ========================================================================== +// ========================================================================== + +bool isValidResourceType(const String8& type) +{ + return type == "anim" || type == "drawable" || type == "layout" + || type == "values" || type == "xml" || type == "raw" + || type == "color" || type == "menu"; +} + +static sp<AaptFile> getResourceFile(const sp<AaptAssets>& assets, bool makeIfNecessary=true) +{ + sp<AaptGroup> group = assets->getFiles().valueFor(String8("resources.arsc")); + sp<AaptFile> file; + if (group != NULL) { + file = group->getFiles().valueFor(AaptGroupEntry()); + if (file != NULL) { + return file; + } + } + + if (!makeIfNecessary) { + return NULL; + } + return assets->addFile(String8("resources.arsc"), AaptGroupEntry(), String8(), + NULL, String8()); +} + +static status_t parsePackage(const sp<AaptAssets>& assets, const sp<AaptGroup>& grp) +{ + if (grp->getFiles().size() != 1) { + fprintf(stderr, "WARNING: Multiple AndroidManifest.xml files found, using %s\n", + grp->getFiles().valueAt(0)->getPrintableSource().string()); + } + + sp<AaptFile> file = grp->getFiles().valueAt(0); + + ResXMLTree block; + status_t err = parseXMLResource(file, &block); + if (err != NO_ERROR) { + return err; + } + //printXMLBlock(&block); + + ResXMLTree::event_code_t code; + while ((code=block.next()) != ResXMLTree::START_TAG + && code != ResXMLTree::END_DOCUMENT + && code != ResXMLTree::BAD_DOCUMENT) { + } + + size_t len; + if (code != ResXMLTree::START_TAG) { + fprintf(stderr, "%s:%d: No start tag found\n", + file->getPrintableSource().string(), block.getLineNumber()); + return UNKNOWN_ERROR; + } + if (strcmp16(block.getElementName(&len), String16("manifest").string()) != 0) { + fprintf(stderr, "%s:%d: Invalid start tag %s, expected <manifest>\n", + file->getPrintableSource().string(), block.getLineNumber(), + String8(block.getElementName(&len)).string()); + return UNKNOWN_ERROR; + } + + ssize_t nameIndex = block.indexOfAttribute(NULL, "package"); + if (nameIndex < 0) { + fprintf(stderr, "%s:%d: <manifest> does not have package attribute.\n", + file->getPrintableSource().string(), block.getLineNumber()); + return UNKNOWN_ERROR; + } + + assets->setPackage(String8(block.getAttributeStringValue(nameIndex, &len))); + + return NO_ERROR; +} + +// ========================================================================== +// ========================================================================== +// ========================================================================== + +static status_t makeFileResources(Bundle* bundle, const sp<AaptAssets>& assets, + ResourceTable* table, + const sp<ResourceTypeSet>& set, + const char* resType) +{ + String8 type8(resType); + String16 type16(resType); + + bool hasErrors = false; + + ResourceDirIterator it(set, String8(resType)); + ssize_t res; + while ((res=it.next()) == NO_ERROR) { + if (bundle->getVerbose()) { + printf(" (new resource id %s from %s)\n", + it.getBaseName().string(), it.getFile()->getPrintableSource().string()); + } + String16 baseName(it.getBaseName()); + const char16_t* str = baseName.string(); + const char16_t* const end = str + baseName.size(); + while (str < end) { + if (!((*str >= 'a' && *str <= 'z') + || (*str >= '0' && *str <= '9') + || *str == '_' || *str == '.')) { + fprintf(stderr, "%s: Invalid file name: must contain only [a-z0-9_.]\n", + it.getPath().string()); + hasErrors = true; + } + str++; + } + String8 resPath = it.getPath(); + resPath.convertToResPath(); + table->addEntry(SourcePos(it.getPath(), 0), String16(assets->getPackage()), + type16, + baseName, + String16(resPath), + NULL, + &it.getParams()); + assets->addResource(it.getLeafName(), resPath, it.getFile(), type8); + } + + return hasErrors ? UNKNOWN_ERROR : NO_ERROR; +} + +static status_t preProcessImages(Bundle* bundle, const sp<AaptAssets>& assets, + const sp<ResourceTypeSet>& set) +{ + ResourceDirIterator it(set, String8("drawable")); + Vector<sp<AaptFile> > newNameFiles; + Vector<String8> newNamePaths; + ssize_t res; + while ((res=it.next()) == NO_ERROR) { + res = preProcessImage(bundle, assets, it.getFile(), NULL); + if (res != NO_ERROR) { + return res; + } + } + + return NO_ERROR; +} + +status_t postProcessImages(const sp<AaptAssets>& assets, + ResourceTable* table, + const sp<ResourceTypeSet>& set) +{ + ResourceDirIterator it(set, String8("drawable")); + ssize_t res; + while ((res=it.next()) == NO_ERROR) { + res = postProcessImage(assets, table, it.getFile()); + if (res != NO_ERROR) { + return res; + } + } + + return res < NO_ERROR ? res : (status_t)NO_ERROR; +} + +static void collect_files(const sp<AaptDir>& dir, + KeyedVector<String8, sp<ResourceTypeSet> >* resources) +{ + const DefaultKeyedVector<String8, sp<AaptGroup> >& groups = dir->getFiles(); + int N = groups.size(); + for (int i=0; i<N; i++) { + String8 leafName = groups.keyAt(i); + const sp<AaptGroup>& group = groups.valueAt(i); + + const DefaultKeyedVector<AaptGroupEntry, sp<AaptFile> >& files + = group->getFiles(); + + if (files.size() == 0) { + continue; + } + + String8 resType = files.valueAt(0)->getResourceType(); + + ssize_t index = resources->indexOfKey(resType); + + if (index < 0) { + sp<ResourceTypeSet> set = new ResourceTypeSet(); + set->add(leafName, group); + resources->add(resType, set); + } else { + sp<ResourceTypeSet> set = resources->valueAt(index); + index = set->indexOfKey(leafName); + if (index < 0) { + set->add(leafName, group); + } else { + sp<AaptGroup> existingGroup = set->valueAt(index); + int M = files.size(); + for (int j=0; j<M; j++) { + existingGroup->addFile(files.valueAt(j)); + } + } + } + } +} + +static void collect_files(const sp<AaptAssets>& ass, + KeyedVector<String8, sp<ResourceTypeSet> >* resources) +{ + const Vector<sp<AaptDir> >& dirs = ass->resDirs(); + int N = dirs.size(); + + for (int i=0; i<N; i++) { + sp<AaptDir> d = dirs.itemAt(i); + collect_files(d, resources); + + // don't try to include the res dir + ass->removeDir(d->getLeaf()); + } +} + +enum { + ATTR_OKAY = -1, + ATTR_NOT_FOUND = -2, + ATTR_LEADING_SPACES = -3, + ATTR_TRAILING_SPACES = -4 +}; +static int validateAttr(const String8& path, const ResXMLParser& parser, + const char* ns, const char* attr, const char* validChars, bool required) +{ + size_t len; + + ssize_t index = parser.indexOfAttribute(ns, attr); + const uint16_t* str; + if (index >= 0 && (str=parser.getAttributeStringValue(index, &len)) != NULL) { + if (validChars) { + for (size_t i=0; i<len; i++) { + uint16_t c = str[i]; + const char* p = validChars; + bool okay = false; + while (*p) { + if (c == *p) { + okay = true; + break; + } + p++; + } + if (!okay) { + fprintf(stderr, "%s:%d: Tag <%s> attribute %s has invalid character '%c'.\n", + path.string(), parser.getLineNumber(), + String8(parser.getElementName(&len)).string(), attr, (char)str[i]); + return (int)i; + } + } + } + if (*str == ' ') { + fprintf(stderr, "%s:%d: Tag <%s> attribute %s can not start with a space.\n", + path.string(), parser.getLineNumber(), + String8(parser.getElementName(&len)).string(), attr); + return ATTR_LEADING_SPACES; + } + if (str[len-1] == ' ') { + fprintf(stderr, "%s:%d: Tag <%s> attribute %s can not end with a space.\n", + path.string(), parser.getLineNumber(), + String8(parser.getElementName(&len)).string(), attr); + return ATTR_TRAILING_SPACES; + } + return ATTR_OKAY; + } + if (required) { + fprintf(stderr, "%s:%d: Tag <%s> missing required attribute %s.\n", + path.string(), parser.getLineNumber(), + String8(parser.getElementName(&len)).string(), attr); + return ATTR_NOT_FOUND; + } + return ATTR_OKAY; +} + +static void checkForIds(const String8& path, ResXMLParser& parser) +{ + ResXMLTree::event_code_t code; + while ((code=parser.next()) != ResXMLTree::END_DOCUMENT + && code > ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::START_TAG) { + ssize_t index = parser.indexOfAttribute(NULL, "id"); + if (index >= 0) { + fprintf(stderr, "%s:%d: WARNING: found plain 'id' attribute; did you mean the new 'android:id' name?\n", + path.string(), parser.getLineNumber()); + } + } + } +} + +static void applyFileOverlay(const sp<AaptAssets>& assets, + const sp<ResourceTypeSet>& baseSet, + const char *resType) +{ + // Replace any base level files in this category with any found from the overlay + // Also add any found only in the overlay. + sp<AaptAssets> overlay = assets->getOverlay(); + String8 resTypeString(resType); + + // work through the linked list of overlays + while (overlay.get()) { + KeyedVector<String8, sp<ResourceTypeSet> >* overlayRes = overlay->getResources(); + + // get the overlay resources of the requested type + ssize_t index = overlayRes->indexOfKey(resTypeString); + if (index >= 0) { + sp<ResourceTypeSet> overlaySet = overlayRes->valueAt(index); + + // for each of the resources, check for a match in the previously built + // non-overlay "baseset". + size_t overlayCount = overlaySet->size(); + for (size_t overlayIndex=0; overlayIndex<overlayCount; overlayIndex++) { + size_t baseIndex = baseSet->indexOfKey(overlaySet->keyAt(overlayIndex)); + if (baseIndex != UNKNOWN_ERROR) { + // look for same flavor. For a given file (strings.xml, for example) + // there may be a locale specific or other flavors - we want to match + // the same flavor. + sp<AaptGroup> overlayGroup = overlaySet->valueAt(overlayIndex); + sp<AaptGroup> baseGroup = baseSet->valueAt(baseIndex); + + DefaultKeyedVector<AaptGroupEntry, sp<AaptFile> > baseFiles = + baseGroup->getFiles(); + DefaultKeyedVector<AaptGroupEntry, sp<AaptFile> > overlayFiles = + overlayGroup->getFiles(); + size_t overlayGroupSize = overlayFiles.size(); + for (size_t overlayGroupIndex = 0; + overlayGroupIndex<overlayGroupSize; + overlayGroupIndex++) { + size_t baseFileIndex = + baseFiles.indexOfKey(overlayFiles.keyAt(overlayGroupIndex)); + if(baseFileIndex < UNKNOWN_ERROR) { + baseGroup->removeFile(baseFileIndex); + } else { + // didn't find a match fall through and add it.. + } + baseGroup->addFile(overlayFiles.valueAt(overlayGroupIndex)); + } + } else { + // this group doesn't exist (a file that's only in the overlay) + // add it + baseSet->add(overlaySet->keyAt(overlayIndex), + overlaySet->valueAt(overlayIndex)); + } + } + // this overlay didn't have resources for this type + } + // try next overlay + overlay = overlay->getOverlay(); + } + return; +} + +#define ASSIGN_IT(n) \ + do { \ + ssize_t index = resources->indexOfKey(String8(#n)); \ + if (index >= 0) { \ + n ## s = resources->valueAt(index); \ + } \ + } while (0) + +status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets) +{ + // First, look for a package file to parse. This is required to + // be able to generate the resource information. + sp<AaptGroup> androidManifestFile = + assets->getFiles().valueFor(String8("AndroidManifest.xml")); + if (androidManifestFile == NULL) { + fprintf(stderr, "ERROR: No AndroidManifest.xml file found.\n"); + return UNKNOWN_ERROR; + } + + status_t err = parsePackage(assets, androidManifestFile); + if (err != NO_ERROR) { + return err; + } + + NOISY(printf("Creating resources for package %s\n", + assets->getPackage().string())); + + ResourceTable table(bundle, String16(assets->getPackage())); + err = table.addIncludedResources(bundle, assets); + if (err != NO_ERROR) { + return err; + } + + NOISY(printf("Found %d included resource packages\n", (int)table.size())); + + // -------------------------------------------------------------- + // First, gather all resource information. + // -------------------------------------------------------------- + + // resType -> leafName -> group + KeyedVector<String8, sp<ResourceTypeSet> > *resources = + new KeyedVector<String8, sp<ResourceTypeSet> >; + collect_files(assets, resources); + + sp<ResourceTypeSet> drawables; + sp<ResourceTypeSet> layouts; + sp<ResourceTypeSet> anims; + sp<ResourceTypeSet> xmls; + sp<ResourceTypeSet> raws; + sp<ResourceTypeSet> colors; + sp<ResourceTypeSet> menus; + + ASSIGN_IT(drawable); + ASSIGN_IT(layout); + ASSIGN_IT(anim); + ASSIGN_IT(xml); + ASSIGN_IT(raw); + ASSIGN_IT(color); + ASSIGN_IT(menu); + + assets->setResources(resources); + // now go through any resource overlays and collect their files + sp<AaptAssets> current = assets->getOverlay(); + while(current.get()) { + KeyedVector<String8, sp<ResourceTypeSet> > *resources = + new KeyedVector<String8, sp<ResourceTypeSet> >; + current->setResources(resources); + collect_files(current, resources); + current = current->getOverlay(); + } + // apply the overlay files to the base set + applyFileOverlay(assets, drawables, "drawable"); + applyFileOverlay(assets, layouts, "layout"); + applyFileOverlay(assets, anims, "anim"); + applyFileOverlay(assets, xmls, "xml"); + applyFileOverlay(assets, raws, "raw"); + applyFileOverlay(assets, colors, "color"); + applyFileOverlay(assets, menus, "menu"); + + bool hasErrors = false; + + if (drawables != NULL) { + err = preProcessImages(bundle, assets, drawables); + if (err == NO_ERROR) { + err = makeFileResources(bundle, assets, &table, drawables, "drawable"); + if (err != NO_ERROR) { + hasErrors = true; + } + } else { + hasErrors = true; + } + } + + if (layouts != NULL) { + err = makeFileResources(bundle, assets, &table, layouts, "layout"); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (anims != NULL) { + err = makeFileResources(bundle, assets, &table, anims, "anim"); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (xmls != NULL) { + err = makeFileResources(bundle, assets, &table, xmls, "xml"); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (raws != NULL) { + err = makeFileResources(bundle, assets, &table, raws, "raw"); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + // compile resources + current = assets; + while(current.get()) { + KeyedVector<String8, sp<ResourceTypeSet> > *resources = + current->getResources(); + + ssize_t index = resources->indexOfKey(String8("values")); + if (index >= 0) { + ResourceDirIterator it(resources->valueAt(index), String8("values")); + ssize_t res; + while ((res=it.next()) == NO_ERROR) { + sp<AaptFile> file = it.getFile(); + res = compileResourceFile(bundle, assets, file, it.getParams(), + (current!=assets), &table); + if (res != NO_ERROR) { + hasErrors = true; + } + } + } + current = current->getOverlay(); + } + + if (colors != NULL) { + err = makeFileResources(bundle, assets, &table, colors, "color"); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (menus != NULL) { + err = makeFileResources(bundle, assets, &table, menus, "menu"); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + // -------------------------------------------------------------------- + // Assignment of resource IDs and initial generation of resource table. + // -------------------------------------------------------------------- + + if (table.hasResources()) { + sp<AaptFile> resFile(getResourceFile(assets)); + if (resFile == NULL) { + fprintf(stderr, "Error: unable to generate entry for resource data\n"); + return UNKNOWN_ERROR; + } + + err = table.assignResourceIds(); + if (err < NO_ERROR) { + return err; + } + } + + // -------------------------------------------------------------- + // Finally, we can now we can compile XML files, which may reference + // resources. + // -------------------------------------------------------------- + + if (layouts != NULL) { + ResourceDirIterator it(layouts, String8("layout")); + while ((err=it.next()) == NO_ERROR) { + String8 src = it.getFile()->getPrintableSource(); + err = compileXmlFile(assets, it.getFile(), &table); + if (err == NO_ERROR) { + ResXMLTree block; + block.setTo(it.getFile()->getData(), it.getFile()->getSize(), true); + checkForIds(src, block); + } else { + hasErrors = true; + } + } + + if (err < NO_ERROR) { + hasErrors = true; + } + err = NO_ERROR; + } + + if (anims != NULL) { + ResourceDirIterator it(anims, String8("anim")); + while ((err=it.next()) == NO_ERROR) { + err = compileXmlFile(assets, it.getFile(), &table); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (err < NO_ERROR) { + hasErrors = true; + } + err = NO_ERROR; + } + + if (xmls != NULL) { + ResourceDirIterator it(xmls, String8("xml")); + while ((err=it.next()) == NO_ERROR) { + err = compileXmlFile(assets, it.getFile(), &table); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (err < NO_ERROR) { + hasErrors = true; + } + err = NO_ERROR; + } + + if (drawables != NULL) { + err = postProcessImages(assets, &table, drawables); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (colors != NULL) { + ResourceDirIterator it(colors, String8("color")); + while ((err=it.next()) == NO_ERROR) { + err = compileXmlFile(assets, it.getFile(), &table); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (err < NO_ERROR) { + hasErrors = true; + } + err = NO_ERROR; + } + + if (menus != NULL) { + ResourceDirIterator it(menus, String8("menu")); + while ((err=it.next()) == NO_ERROR) { + String8 src = it.getFile()->getPrintableSource(); + err = compileXmlFile(assets, it.getFile(), &table); + if (err != NO_ERROR) { + hasErrors = true; + } + ResXMLTree block; + block.setTo(it.getFile()->getData(), it.getFile()->getSize(), true); + checkForIds(src, block); + } + + if (err < NO_ERROR) { + hasErrors = true; + } + err = NO_ERROR; + } + + const sp<AaptFile> manifestFile(androidManifestFile->getFiles().valueAt(0)); + String8 manifestPath(manifestFile->getPrintableSource()); + + // Perform a basic validation of the manifest file. This time we + // parse it with the comments intact, so that we can use them to + // generate java docs... so we are not going to write this one + // back out to the final manifest data. + err = compileXmlFile(assets, manifestFile, &table, + XML_COMPILE_ASSIGN_ATTRIBUTE_IDS + | XML_COMPILE_STRIP_WHITESPACE | XML_COMPILE_STRIP_RAW_VALUES); + if (err < NO_ERROR) { + return err; + } + ResXMLTree block; + block.setTo(manifestFile->getData(), manifestFile->getSize(), true); + String16 manifest16("manifest"); + String16 permission16("permission"); + String16 permission_group16("permission-group"); + String16 uses_permission16("uses-permission"); + String16 instrumentation16("instrumentation"); + String16 application16("application"); + String16 provider16("provider"); + String16 service16("service"); + String16 receiver16("receiver"); + String16 activity16("activity"); + String16 action16("action"); + String16 category16("category"); + String16 data16("scheme"); + const char* packageIdentChars = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ._0123456789"; + const char* packageIdentCharsWithTheStupid = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ._0123456789-"; + const char* classIdentChars = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ._0123456789$"; + const char* processIdentChars = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ._0123456789:"; + const char* authoritiesIdentChars = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ._0123456789-:;"; + const char* typeIdentChars = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ._0123456789:-/*+"; + const char* schemeIdentChars = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ._0123456789-"; + ResXMLTree::event_code_t code; + sp<AaptSymbols> permissionSymbols; + sp<AaptSymbols> permissionGroupSymbols; + while ((code=block.next()) != ResXMLTree::END_DOCUMENT + && code > ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::START_TAG) { + size_t len; + if (block.getElementNamespace(&len) != NULL) { + continue; + } + if (strcmp16(block.getElementName(&len), manifest16.string()) == 0) { + if (validateAttr(manifestPath, block, NULL, "package", + packageIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + } else if (strcmp16(block.getElementName(&len), permission16.string()) == 0 + || strcmp16(block.getElementName(&len), permission_group16.string()) == 0) { + const bool isGroup = strcmp16(block.getElementName(&len), + permission_group16.string()) == 0; + if (validateAttr(manifestPath, block, RESOURCES_ANDROID_NAMESPACE, "name", + isGroup ? packageIdentCharsWithTheStupid + : packageIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + SourcePos srcPos(manifestPath, block.getLineNumber()); + sp<AaptSymbols> syms; + if (!isGroup) { + syms = permissionSymbols; + if (syms == NULL) { + sp<AaptSymbols> symbols = + assets->getSymbolsFor(String8("Manifest")); + syms = permissionSymbols = symbols->addNestedSymbol( + String8("permission"), srcPos); + } + } else { + syms = permissionGroupSymbols; + if (syms == NULL) { + sp<AaptSymbols> symbols = + assets->getSymbolsFor(String8("Manifest")); + syms = permissionGroupSymbols = symbols->addNestedSymbol( + String8("permission_group"), srcPos); + } + } + size_t len; + ssize_t index = block.indexOfAttribute(RESOURCES_ANDROID_NAMESPACE, "name"); + const uint16_t* id = block.getAttributeStringValue(index, &len); + if (id == NULL) { + fprintf(stderr, "%s:%d: missing name attribute in element <%s>.\n", + manifestPath.string(), block.getLineNumber(), + String8(block.getElementName(&len)).string()); + hasErrors = true; + break; + } + String8 idStr(id); + char* p = idStr.lockBuffer(idStr.size()); + char* e = p + idStr.size(); + bool begins_with_digit = true; // init to true so an empty string fails + while (e > p) { + e--; + if (*e >= '0' && *e <= '9') { + begins_with_digit = true; + continue; + } + if ((*e >= 'a' && *e <= 'z') || + (*e >= 'A' && *e <= 'Z') || + (*e == '_')) { + begins_with_digit = false; + continue; + } + if (isGroup && (*e == '-')) { + *e = '_'; + begins_with_digit = false; + continue; + } + e++; + break; + } + idStr.unlockBuffer(); + // verify that we stopped because we hit a period or + // the beginning of the string, and that the + // identifier didn't begin with a digit. + if (begins_with_digit || (e != p && *(e-1) != '.')) { + fprintf(stderr, + "%s:%d: Permission name <%s> is not a valid Java symbol\n", + manifestPath.string(), block.getLineNumber(), idStr.string()); + hasErrors = true; + } + syms->addStringSymbol(String8(e), idStr, srcPos); + const uint16_t* cmt = block.getComment(&len); + if (cmt != NULL && *cmt != 0) { + //printf("Comment of %s: %s\n", String8(e).string(), + // String8(cmt).string()); + syms->appendComment(String8(e), String16(cmt), srcPos); + } else { + //printf("No comment for %s\n", String8(e).string()); + } + syms->makeSymbolPublic(String8(e), srcPos); + } else if (strcmp16(block.getElementName(&len), uses_permission16.string()) == 0) { + if (validateAttr(manifestPath, block, RESOURCES_ANDROID_NAMESPACE, "name", + packageIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + } else if (strcmp16(block.getElementName(&len), instrumentation16.string()) == 0) { + if (validateAttr(manifestPath, block, RESOURCES_ANDROID_NAMESPACE, "name", + classIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "targetPackage", + packageIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + } else if (strcmp16(block.getElementName(&len), application16.string()) == 0) { + if (validateAttr(manifestPath, block, RESOURCES_ANDROID_NAMESPACE, "name", + classIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "permission", + packageIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "process", + processIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "taskAffinity", + processIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + } else if (strcmp16(block.getElementName(&len), provider16.string()) == 0) { + if (validateAttr(manifestPath, block, RESOURCES_ANDROID_NAMESPACE, "name", + classIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "authorities", + authoritiesIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "permission", + packageIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "process", + processIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + } else if (strcmp16(block.getElementName(&len), service16.string()) == 0 + || strcmp16(block.getElementName(&len), receiver16.string()) == 0 + || strcmp16(block.getElementName(&len), activity16.string()) == 0) { + if (validateAttr(manifestPath, block, RESOURCES_ANDROID_NAMESPACE, "name", + classIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "permission", + packageIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "process", + processIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "taskAffinity", + processIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + } else if (strcmp16(block.getElementName(&len), action16.string()) == 0 + || strcmp16(block.getElementName(&len), category16.string()) == 0) { + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "name", + packageIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + } else if (strcmp16(block.getElementName(&len), data16.string()) == 0) { + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "mimeType", + typeIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, block, + RESOURCES_ANDROID_NAMESPACE, "scheme", + schemeIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + } + } + } + + if (table.validateLocalizations()) { + hasErrors = true; + } + + if (hasErrors) { + return UNKNOWN_ERROR; + } + + // Generate final compiled manifest file. + manifestFile->clearData(); + err = compileXmlFile(assets, manifestFile, &table); + if (err < NO_ERROR) { + return err; + } + + //block.restart(); + //printXMLBlock(&block); + + // -------------------------------------------------------------- + // Generate the final resource table. + // Re-flatten because we may have added new resource IDs + // -------------------------------------------------------------- + + if (table.hasResources()) { + sp<AaptSymbols> symbols = assets->getSymbolsFor(String8("R")); + err = table.addSymbols(symbols); + if (err < NO_ERROR) { + return err; + } + + sp<AaptFile> resFile(getResourceFile(assets)); + if (resFile == NULL) { + fprintf(stderr, "Error: unable to generate entry for resource data\n"); + return UNKNOWN_ERROR; + } + + err = table.flatten(bundle, resFile); + if (err < NO_ERROR) { + return err; + } + + if (bundle->getPublicOutputFile()) { + FILE* fp = fopen(bundle->getPublicOutputFile(), "w+"); + if (fp == NULL) { + fprintf(stderr, "ERROR: Unable to open public definitions output file %s: %s\n", + (const char*)bundle->getPublicOutputFile(), strerror(errno)); + return UNKNOWN_ERROR; + } + if (bundle->getVerbose()) { + printf(" Writing public definitions to %s.\n", bundle->getPublicOutputFile()); + } + table.writePublicDefinitions(String16(assets->getPackage()), fp); + } + + NOISY( + ResTable rt; + rt.add(resFile->getData(), resFile->getSize(), NULL); + printf("Generated resources:\n"); + rt.print(); + ) + + // These resources are now considered to be a part of the included + // resources, for others to reference. + err = assets->addIncludedResources(resFile); + if (err < NO_ERROR) { + fprintf(stderr, "ERROR: Unable to parse generated resources, aborting.\n"); + return err; + } + } + + return err; +} + +static const char* getIndentSpace(int indent) +{ +static const char whitespace[] = +" "; + + return whitespace + sizeof(whitespace) - 1 - indent*4; +} + +static status_t fixupSymbol(String16* inoutSymbol) +{ + inoutSymbol->replaceAll('.', '_'); + inoutSymbol->replaceAll(':', '_'); + return NO_ERROR; +} + +static String16 getAttributeComment(const sp<AaptAssets>& assets, + const String8& name, + String16* outTypeComment = NULL) +{ + sp<AaptSymbols> asym = assets->getSymbolsFor(String8("R")); + if (asym != NULL) { + //printf("Got R symbols!\n"); + asym = asym->getNestedSymbols().valueFor(String8("attr")); + if (asym != NULL) { + //printf("Got attrs symbols! comment %s=%s\n", + // name.string(), String8(asym->getComment(name)).string()); + if (outTypeComment != NULL) { + *outTypeComment = asym->getTypeComment(name); + } + return asym->getComment(name); + } + } + return String16(); +} + +static status_t writeLayoutClasses( + FILE* fp, const sp<AaptAssets>& assets, + const sp<AaptSymbols>& symbols, int indent, bool includePrivate) +{ + const char* indentStr = getIndentSpace(indent); + if (!includePrivate) { + fprintf(fp, "%s/** @doconly */\n", indentStr); + } + fprintf(fp, "%spublic static final class styleable {\n", indentStr); + indent++; + + String16 attr16("attr"); + String16 package16(assets->getPackage()); + + indentStr = getIndentSpace(indent); + bool hasErrors = false; + + size_t i; + size_t N = symbols->getNestedSymbols().size(); + for (i=0; i<N; i++) { + sp<AaptSymbols> nsymbols = symbols->getNestedSymbols().valueAt(i); + String16 nclassName16(symbols->getNestedSymbols().keyAt(i)); + String8 realClassName(nclassName16); + if (fixupSymbol(&nclassName16) != NO_ERROR) { + hasErrors = true; + } + String8 nclassName(nclassName16); + + SortedVector<uint32_t> idents; + Vector<uint32_t> origOrder; + Vector<bool> publicFlags; + + size_t a; + size_t NA = nsymbols->getSymbols().size(); + for (a=0; a<NA; a++) { + const AaptSymbolEntry& sym(nsymbols->getSymbols().valueAt(a)); + int32_t code = sym.typeCode == AaptSymbolEntry::TYPE_INT32 + ? sym.int32Val : 0; + bool isPublic = true; + if (code == 0) { + String16 name16(sym.name); + uint32_t typeSpecFlags; + code = assets->getIncludedResources().identifierForName( + name16.string(), name16.size(), + attr16.string(), attr16.size(), + package16.string(), package16.size(), &typeSpecFlags); + if (code == 0) { + fprintf(stderr, "ERROR: In <declare-styleable> %s, unable to find attribute %s\n", + nclassName.string(), sym.name.string()); + hasErrors = true; + } + isPublic = (typeSpecFlags&ResTable_typeSpec::SPEC_PUBLIC) != 0; + } + idents.add(code); + origOrder.add(code); + publicFlags.add(isPublic); + } + + NA = idents.size(); + + String16 comment = symbols->getComment(realClassName); + fprintf(fp, "%s/** ", indentStr); + if (comment.size() > 0) { + fprintf(fp, "%s\n", String8(comment).string()); + } else { + fprintf(fp, "Attributes that can be used with a %s.\n", nclassName.string()); + } + bool hasTable = false; + for (a=0; a<NA; a++) { + ssize_t pos = idents.indexOf(origOrder.itemAt(a)); + if (pos >= 0) { + if (!hasTable) { + hasTable = true; + fprintf(fp, + "%s <p>Includes the following attributes:</p>\n" + "%s <table border=\"2\" width=\"85%%\" align=\"center\" frame=\"hsides\" rules=\"all\" cellpadding=\"5\">\n" + "%s <colgroup align=\"left\" />\n" + "%s <colgroup align=\"left\" />\n" + "%s <tr><th>Attribute<th>Summary</tr>\n", + indentStr, + indentStr, + indentStr, + indentStr, + indentStr); + } + const AaptSymbolEntry& sym = nsymbols->getSymbols().valueAt(a); + if (!publicFlags.itemAt(a) && !includePrivate) { + continue; + } + String8 name8(sym.name); + String16 comment(sym.comment); + if (comment.size() <= 0) { + comment = getAttributeComment(assets, name8); + } + if (comment.size() > 0) { + const char16_t* p = comment.string(); + while (*p != 0 && *p != '.') { + if (*p == '{') { + while (*p != 0 && *p != '}') { + p++; + } + } else { + p++; + } + } + if (*p == '.') { + p++; + } + comment = String16(comment.string(), p-comment.string()); + } + String16 name(name8); + fixupSymbol(&name); + fprintf(fp, "%s <tr><th><code>{@link #%s_%s %s:%s}</code><td>%s</tr>\n", + indentStr, nclassName.string(), + String8(name).string(), + assets->getPackage().string(), + String8(name).string(), + String8(comment).string()); + } + } + if (hasTable) { + fprintf(fp, "%s </table>\n", indentStr); + } + for (a=0; a<NA; a++) { + ssize_t pos = idents.indexOf(origOrder.itemAt(a)); + if (pos >= 0) { + const AaptSymbolEntry& sym = nsymbols->getSymbols().valueAt(a); + if (!publicFlags.itemAt(a) && !includePrivate) { + continue; + } + String16 name(sym.name); + fixupSymbol(&name); + fprintf(fp, "%s @see #%s_%s\n", + indentStr, nclassName.string(), + String8(name).string()); + } + } + fprintf(fp, "%s */\n", getIndentSpace(indent)); + + fprintf(fp, + "%spublic static final int[] %s = {\n" + "%s", + indentStr, nclassName.string(), + getIndentSpace(indent+1)); + + for (a=0; a<NA; a++) { + if (a != 0) { + if ((a&3) == 0) { + fprintf(fp, ",\n%s", getIndentSpace(indent+1)); + } else { + fprintf(fp, ", "); + } + } + fprintf(fp, "0x%08x", idents[a]); + } + + fprintf(fp, "\n%s};\n", indentStr); + + for (a=0; a<NA; a++) { + ssize_t pos = idents.indexOf(origOrder.itemAt(a)); + if (pos >= 0) { + const AaptSymbolEntry& sym = nsymbols->getSymbols().valueAt(a); + if (!publicFlags.itemAt(a) && !includePrivate) { + continue; + } + String8 name8(sym.name); + String16 comment(sym.comment); + String16 typeComment; + if (comment.size() <= 0) { + comment = getAttributeComment(assets, name8, &typeComment); + } else { + getAttributeComment(assets, name8, &typeComment); + } + String16 name(name8); + if (fixupSymbol(&name) != NO_ERROR) { + hasErrors = true; + } + + uint32_t typeSpecFlags = 0; + String16 name16(sym.name); + assets->getIncludedResources().identifierForName( + name16.string(), name16.size(), + attr16.string(), attr16.size(), + package16.string(), package16.size(), &typeSpecFlags); + //printf("%s:%s/%s: 0x%08x\n", String8(package16).string(), + // String8(attr16).string(), String8(name16).string(), typeSpecFlags); + const bool pub = (typeSpecFlags&ResTable_typeSpec::SPEC_PUBLIC) != 0; + + fprintf(fp, "%s/**\n", indentStr); + if (comment.size() > 0) { + fprintf(fp, "%s <p>\n%s @attr description\n", indentStr, indentStr); + fprintf(fp, "%s %s\n", indentStr, String8(comment).string()); + } else { + fprintf(fp, + "%s <p>This symbol is the offset where the {@link %s.R.attr#%s}\n" + "%s attribute's value can be found in the {@link #%s} array.\n", + indentStr, + pub ? assets->getPackage().string() + : assets->getSymbolsPrivatePackage().string(), + String8(name).string(), + indentStr, nclassName.string()); + } + if (typeComment.size() > 0) { + fprintf(fp, "\n\n%s %s\n", indentStr, String8(typeComment).string()); + } + if (comment.size() > 0) { + if (pub) { + fprintf(fp, + "%s <p>This corresponds to the global attribute" + "%s resource symbol {@link %s.R.attr#%s}.\n", + indentStr, indentStr, + assets->getPackage().string(), + String8(name).string()); + } else { + fprintf(fp, + "%s <p>This is a private symbol.\n", indentStr); + } + } + fprintf(fp, "%s @attr name %s:%s\n", indentStr, + "android", String8(name).string()); + fprintf(fp, "%s*/\n", indentStr); + fprintf(fp, + "%spublic static final int %s_%s = %d;\n", + indentStr, nclassName.string(), + String8(name).string(), (int)pos); + } + } + } + + indent--; + fprintf(fp, "%s};\n", getIndentSpace(indent)); + return hasErrors ? UNKNOWN_ERROR : NO_ERROR; +} + +static status_t writeSymbolClass( + FILE* fp, const sp<AaptAssets>& assets, bool includePrivate, + const sp<AaptSymbols>& symbols, const String8& className, int indent) +{ + fprintf(fp, "%spublic %sfinal class %s {\n", + getIndentSpace(indent), + indent != 0 ? "static " : "", className.string()); + indent++; + + size_t i; + status_t err = NO_ERROR; + + size_t N = symbols->getSymbols().size(); + for (i=0; i<N; i++) { + const AaptSymbolEntry& sym = symbols->getSymbols().valueAt(i); + if (sym.typeCode != AaptSymbolEntry::TYPE_INT32) { + continue; + } + if (!includePrivate && !sym.isPublic) { + continue; + } + String16 name(sym.name); + String8 realName(name); + if (fixupSymbol(&name) != NO_ERROR) { + return UNKNOWN_ERROR; + } + String16 comment(sym.comment); + bool haveComment = false; + if (comment.size() > 0) { + haveComment = true; + fprintf(fp, + "%s/** %s\n", + getIndentSpace(indent), String8(comment).string()); + } else if (sym.isPublic && !includePrivate) { + sym.sourcePos.warning("No comment for public symbol %s:%s/%s", + assets->getPackage().string(), className.string(), + String8(sym.name).string()); + } + String16 typeComment(sym.typeComment); + if (typeComment.size() > 0) { + if (!haveComment) { + haveComment = true; + fprintf(fp, + "%s/** %s\n", + getIndentSpace(indent), String8(typeComment).string()); + } else { + fprintf(fp, + "%s %s\n", + getIndentSpace(indent), String8(typeComment).string()); + } + } + if (haveComment) { + fprintf(fp,"%s */\n", getIndentSpace(indent)); + } + fprintf(fp, "%spublic static final int %s=0x%08x;\n", + getIndentSpace(indent), + String8(name).string(), (int)sym.int32Val); + } + + for (i=0; i<N; i++) { + const AaptSymbolEntry& sym = symbols->getSymbols().valueAt(i); + if (sym.typeCode != AaptSymbolEntry::TYPE_STRING) { + continue; + } + if (!includePrivate && !sym.isPublic) { + continue; + } + String16 name(sym.name); + if (fixupSymbol(&name) != NO_ERROR) { + return UNKNOWN_ERROR; + } + String16 comment(sym.comment); + if (comment.size() > 0) { + fprintf(fp, + "%s/** %s\n" + "%s */\n", + getIndentSpace(indent), String8(comment).string(), + getIndentSpace(indent)); + } else if (sym.isPublic && !includePrivate) { + sym.sourcePos.warning("No comment for public symbol %s:%s/%s", + assets->getPackage().string(), className.string(), + String8(sym.name).string()); + } + fprintf(fp, "%spublic static final String %s=\"%s\";\n", + getIndentSpace(indent), + String8(name).string(), sym.stringVal.string()); + } + + sp<AaptSymbols> styleableSymbols; + + N = symbols->getNestedSymbols().size(); + for (i=0; i<N; i++) { + sp<AaptSymbols> nsymbols = symbols->getNestedSymbols().valueAt(i); + String8 nclassName(symbols->getNestedSymbols().keyAt(i)); + if (nclassName == "styleable") { + styleableSymbols = nsymbols; + } else { + err = writeSymbolClass(fp, assets, includePrivate, nsymbols, nclassName, indent); + } + if (err != NO_ERROR) { + return err; + } + } + + if (styleableSymbols != NULL) { + err = writeLayoutClasses(fp, assets, styleableSymbols, indent, includePrivate); + if (err != NO_ERROR) { + return err; + } + } + + indent--; + fprintf(fp, "%s}\n", getIndentSpace(indent)); + return NO_ERROR; +} + +status_t writeResourceSymbols(Bundle* bundle, const sp<AaptAssets>& assets, + const String8& package, bool includePrivate) +{ + if (!bundle->getRClassDir()) { + return NO_ERROR; + } + + const size_t N = assets->getSymbols().size(); + for (size_t i=0; i<N; i++) { + sp<AaptSymbols> symbols = assets->getSymbols().valueAt(i); + String8 className(assets->getSymbols().keyAt(i)); + String8 dest(bundle->getRClassDir()); + if (bundle->getMakePackageDirs()) { + String8 pkg(package); + const char* last = pkg.string(); + const char* s = last-1; + do { + s++; + if (s > last && (*s == '.' || *s == 0)) { + String8 part(last, s-last); + dest.appendPath(part); +#ifdef HAVE_MS_C_RUNTIME + _mkdir(dest.string()); +#else + mkdir(dest.string(), S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP); +#endif + last = s+1; + } + } while (*s); + } + dest.appendPath(className); + dest.append(".java"); + FILE* fp = fopen(dest.string(), "w+"); + if (fp == NULL) { + fprintf(stderr, "ERROR: Unable to open class file %s: %s\n", + dest.string(), strerror(errno)); + return UNKNOWN_ERROR; + } + if (bundle->getVerbose()) { + printf(" Writing symbols for class %s.\n", className.string()); + } + + fprintf(fp, + "/* AUTO-GENERATED FILE. DO NOT MODIFY.\n" + " *\n" + " * This class was automatically generated by the\n" + " * aapt tool from the resource data it found. It\n" + " * should not be modified by hand.\n" + " */\n" + "\n" + "package %s;\n\n", package.string()); + + status_t err = writeSymbolClass(fp, assets, includePrivate, symbols, className, 0); + if (err != NO_ERROR) { + return err; + } + fclose(fp); + } + + return NO_ERROR; +} diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp new file mode 100644 index 0000000..6f71a1e --- /dev/null +++ b/tools/aapt/ResourceTable.cpp @@ -0,0 +1,3491 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#include "ResourceTable.h" + +#include "XMLNode.h" + +#include <utils/ByteOrder.h> +#include <utils/ResourceTypes.h> +#include <stdarg.h> + +#define NOISY(x) //x + +status_t compileXmlFile(const sp<AaptAssets>& assets, + const sp<AaptFile>& target, + ResourceTable* table, + int options) +{ + sp<XMLNode> root = XMLNode::parse(target); + if (root == NULL) { + return UNKNOWN_ERROR; + } + if ((options&XML_COMPILE_STRIP_WHITESPACE) != 0) { + root->removeWhitespace(true, NULL); + } else if ((options&XML_COMPILE_COMPACT_WHITESPACE) != 0) { + root->removeWhitespace(false, NULL); + } + + bool hasErrors = false; + + if ((options&XML_COMPILE_ASSIGN_ATTRIBUTE_IDS) != 0) { + status_t err = root->assignResourceIds(assets, table); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + status_t err = root->parseValues(assets, table); + if (err != NO_ERROR) { + hasErrors = true; + } + + if (hasErrors) { + return UNKNOWN_ERROR; + } + + NOISY(printf("Input XML Resource:\n")); + NOISY(root->print()); + err = root->flatten(target, + (options&XML_COMPILE_STRIP_COMMENTS) != 0, + (options&XML_COMPILE_STRIP_RAW_VALUES) != 0); + if (err != NO_ERROR) { + return err; + } + + NOISY(printf("Output XML Resource:\n")); + NOISY(ResXMLTree tree; + tree.setTo(target->getData(), target->getSize()); + printXMLBlock(&tree)); + + target->setCompressionMethod(ZipEntry::kCompressDeflated); + + return err; +} + +#undef NOISY +#define NOISY(x) //x + +struct flag_entry +{ + const char16_t* name; + size_t nameLen; + uint32_t value; + const char* description; +}; + +static const char16_t referenceArray[] = + { 'r', 'e', 'f', 'e', 'r', 'e', 'n', 'c', 'e' }; +static const char16_t stringArray[] = + { 's', 't', 'r', 'i', 'n', 'g' }; +static const char16_t integerArray[] = + { 'i', 'n', 't', 'e', 'g', 'e', 'r' }; +static const char16_t booleanArray[] = + { 'b', 'o', 'o', 'l', 'e', 'a', 'n' }; +static const char16_t colorArray[] = + { 'c', 'o', 'l', 'o', 'r' }; +static const char16_t floatArray[] = + { 'f', 'l', 'o', 'a', 't' }; +static const char16_t dimensionArray[] = + { 'd', 'i', 'm', 'e', 'n', 's', 'i', 'o', 'n' }; +static const char16_t fractionArray[] = + { 'f', 'r', 'a', 'c', 't', 'i', 'o', 'n' }; +static const char16_t enumArray[] = + { 'e', 'n', 'u', 'm' }; +static const char16_t flagsArray[] = + { 'f', 'l', 'a', 'g', 's' }; + +static const flag_entry gFormatFlags[] = { + { referenceArray, sizeof(referenceArray)/2, ResTable_map::TYPE_REFERENCE, + "a reference to another resource, in the form \"<code>@[+][<i>package</i>:]<i>type</i>:<i>name</i></code>\"\n" + "or to a theme attribute in the form \"<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>\"."}, + { stringArray, sizeof(stringArray)/2, ResTable_map::TYPE_STRING, + "a string value, using '\\\\;' to escape characters such as '\\\\n' or '\\\\uxxxx' for a unicode character." }, + { integerArray, sizeof(integerArray)/2, ResTable_map::TYPE_INTEGER, + "an integer value, such as \"<code>100</code>\"." }, + { booleanArray, sizeof(booleanArray)/2, ResTable_map::TYPE_BOOLEAN, + "a boolean value, either \"<code>true</code>\" or \"<code>false</code>\"." }, + { colorArray, sizeof(colorArray)/2, ResTable_map::TYPE_COLOR, + "a color value, in the form of \"<code>#<i>rgb</i></code>\", \"<code>#<i>argb</i></code>\",\n" + "\"<code>#<i>rrggbb</i></code>\", or \"<code>#<i>aarrggbb</i></code>\"." }, + { floatArray, sizeof(floatArray)/2, ResTable_map::TYPE_FLOAT, + "a floating point value, such as \"<code>1.2</code>\"."}, + { dimensionArray, sizeof(dimensionArray)/2, ResTable_map::TYPE_DIMENSION, + "a dimension value, which is a floating point number appended with a unit such as \"<code>14.5sp</code>\".\n" + "Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size),\n" + "in (inches), mm (millimeters)." }, + { fractionArray, sizeof(fractionArray)/2, ResTable_map::TYPE_FRACTION, + "a fractional value, which is a floating point number appended with either % or %p, such as \"<code>14.5%</code>\".\n" + "The % suffix always means a percentage of the base size; the optional %p suffix provides a size relative to\n" + "some parent container." }, + { enumArray, sizeof(enumArray)/2, ResTable_map::TYPE_ENUM, NULL }, + { flagsArray, sizeof(flagsArray)/2, ResTable_map::TYPE_FLAGS, NULL }, + { NULL, 0, 0, NULL } +}; + +static const char16_t suggestedArray[] = { 's', 'u', 'g', 'g', 'e', 's', 't', 'e', 'd' }; + +static const flag_entry l10nRequiredFlags[] = { + { suggestedArray, sizeof(suggestedArray)/2, ResTable_map::L10N_SUGGESTED, NULL }, + { NULL, 0, 0, NULL } +}; + +static const char16_t nulStr[] = { 0 }; + +static uint32_t parse_flags(const char16_t* str, size_t len, + const flag_entry* flags, bool* outError = NULL) +{ + while (len > 0 && isspace(*str)) { + str++; + len--; + } + while (len > 0 && isspace(str[len-1])) { + len--; + } + + const char16_t* const end = str + len; + uint32_t value = 0; + + while (str < end) { + const char16_t* div = str; + while (div < end && *div != '|') { + div++; + } + + const flag_entry* cur = flags; + while (cur->name) { + if (strzcmp16(cur->name, cur->nameLen, str, div-str) == 0) { + value |= cur->value; + break; + } + cur++; + } + + if (!cur->name) { + if (outError) *outError = true; + return 0; + } + + str = div < end ? div+1 : div; + } + + if (outError) *outError = false; + return value; +} + +static String16 mayOrMust(int type, int flags) +{ + if ((type&(~flags)) == 0) { + return String16("<p>Must"); + } + + return String16("<p>May"); +} + +static void appendTypeInfo(ResourceTable* outTable, const String16& pkg, + const String16& typeName, const String16& ident, int type, + const flag_entry* flags) +{ + bool hadType = false; + while (flags->name) { + if ((type&flags->value) != 0 && flags->description != NULL) { + String16 fullMsg(mayOrMust(type, flags->value)); + fullMsg.append(String16(" be ")); + fullMsg.append(String16(flags->description)); + outTable->appendTypeComment(pkg, typeName, ident, fullMsg); + hadType = true; + } + flags++; + } + if (hadType && (type&ResTable_map::TYPE_REFERENCE) == 0) { + outTable->appendTypeComment(pkg, typeName, ident, + String16("<p>This may also be a reference to a resource (in the form\n" + "\"<code>@[<i>package</i>:]<i>type</i>:<i>name</i></code>\") or\n" + "theme attribute (in the form\n" + "\"<code>?[<i>package</i>:][<i>type</i>:]<i>name</i></code>\")\n" + "containing a value of this type.")); + } +} + +struct PendingAttribute +{ + const String16 myPackage; + const SourcePos sourcePos; + const bool appendComment; + int32_t type; + String16 ident; + String16 comment; + bool hasErrors; + bool added; + + PendingAttribute(String16 _package, const sp<AaptFile>& in, + ResXMLTree& block, bool _appendComment) + : myPackage(_package) + , sourcePos(in->getPrintableSource(), block.getLineNumber()) + , appendComment(_appendComment) + , type(ResTable_map::TYPE_ANY) + , hasErrors(false) + , added(false) + { + } + + status_t createIfNeeded(ResourceTable* outTable) + { + if (added || hasErrors) { + return NO_ERROR; + } + added = true; + + String16 attr16("attr"); + + if (outTable->hasBagOrEntry(myPackage, attr16, ident)) { + sourcePos.error("Attribute \"%s\" has already been defined\n", + String8(ident).string()); + hasErrors = true; + return UNKNOWN_ERROR; + } + + char numberStr[16]; + sprintf(numberStr, "%d", type); + status_t err = outTable->addBag(sourcePos, myPackage, + attr16, ident, String16(""), + String16("^type"), + String16(numberStr), NULL, NULL); + if (err != NO_ERROR) { + hasErrors = true; + return err; + } + outTable->appendComment(myPackage, attr16, ident, comment, appendComment); + //printf("Attribute %s comment: %s\n", String8(ident).string(), + // String8(comment).string()); + return err; + } +}; + +static status_t compileAttribute(const sp<AaptFile>& in, + ResXMLTree& block, + const String16& myPackage, + ResourceTable* outTable, + String16* outIdent = NULL, + bool inStyleable = false) +{ + PendingAttribute attr(myPackage, in, block, inStyleable); + + const String16 attr16("attr"); + const String16 id16("id"); + + // Attribute type constants. + const String16 enum16("enum"); + const String16 flag16("flag"); + + ResXMLTree::event_code_t code; + size_t len; + status_t err; + + ssize_t identIdx = block.indexOfAttribute(NULL, "name"); + if (identIdx >= 0) { + attr.ident = String16(block.getAttributeStringValue(identIdx, &len)); + if (outIdent) { + *outIdent = attr.ident; + } + } else { + attr.sourcePos.error("A 'name' attribute is required for <attr>\n"); + attr.hasErrors = true; + } + + attr.comment = String16( + block.getComment(&len) ? block.getComment(&len) : nulStr); + + ssize_t typeIdx = block.indexOfAttribute(NULL, "format"); + if (typeIdx >= 0) { + String16 typeStr = String16(block.getAttributeStringValue(typeIdx, &len)); + attr.type = parse_flags(typeStr.string(), typeStr.size(), gFormatFlags); + if (attr.type == 0) { + attr.sourcePos.error("Tag <attr> 'format' attribute value \"%s\" not valid\n", + String8(typeStr).string()); + attr.hasErrors = true; + } + attr.createIfNeeded(outTable); + } else if (!inStyleable) { + // Attribute definitions outside of styleables always define the + // attribute as a generic value. + attr.createIfNeeded(outTable); + } + + //printf("Attribute %s: type=0x%08x\n", String8(attr.ident).string(), attr.type); + + ssize_t minIdx = block.indexOfAttribute(NULL, "min"); + if (minIdx >= 0) { + String16 val = String16(block.getAttributeStringValue(minIdx, &len)); + if (!ResTable::stringToInt(val.string(), val.size(), NULL)) { + attr.sourcePos.error("Tag <attr> 'min' attribute must be a number, not \"%s\"\n", + String8(val).string()); + attr.hasErrors = true; + } + attr.createIfNeeded(outTable); + if (!attr.hasErrors) { + err = outTable->addBag(attr.sourcePos, myPackage, attr16, attr.ident, + String16(""), String16("^min"), String16(val), NULL, NULL); + if (err != NO_ERROR) { + attr.hasErrors = true; + } + } + } + + ssize_t maxIdx = block.indexOfAttribute(NULL, "max"); + if (maxIdx >= 0) { + String16 val = String16(block.getAttributeStringValue(maxIdx, &len)); + if (!ResTable::stringToInt(val.string(), val.size(), NULL)) { + attr.sourcePos.error("Tag <attr> 'max' attribute must be a number, not \"%s\"\n", + String8(val).string()); + attr.hasErrors = true; + } + attr.createIfNeeded(outTable); + if (!attr.hasErrors) { + err = outTable->addBag(attr.sourcePos, myPackage, attr16, attr.ident, + String16(""), String16("^max"), String16(val), NULL, NULL); + attr.hasErrors = true; + } + } + + if ((minIdx >= 0 || maxIdx >= 0) && (attr.type&ResTable_map::TYPE_INTEGER) == 0) { + attr.sourcePos.error("Tag <attr> must have format=integer attribute if using max or min\n"); + attr.hasErrors = true; + } + + ssize_t l10nIdx = block.indexOfAttribute(NULL, "localization"); + if (l10nIdx >= 0) { + const uint16_t* str = block.getAttributeStringValue(l10nIdx, &len); + bool error; + uint32_t l10n_required = parse_flags(str, len, l10nRequiredFlags, &error); + if (error) { + attr.sourcePos.error("Tag <attr> 'localization' attribute value \"%s\" not valid\n", + String8(str).string()); + attr.hasErrors = true; + } + attr.createIfNeeded(outTable); + if (!attr.hasErrors) { + char buf[10]; + sprintf(buf, "%d", l10n_required); + err = outTable->addBag(attr.sourcePos, myPackage, attr16, attr.ident, + String16(""), String16("^l10n"), String16(buf), NULL, NULL); + if (err != NO_ERROR) { + attr.hasErrors = true; + } + } + } + + String16 enumOrFlagsComment; + + while ((code=block.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::START_TAG) { + uint32_t localType = 0; + if (strcmp16(block.getElementName(&len), enum16.string()) == 0) { + localType = ResTable_map::TYPE_ENUM; + } else if (strcmp16(block.getElementName(&len), flag16.string()) == 0) { + localType = ResTable_map::TYPE_FLAGS; + } else { + SourcePos(in->getPrintableSource(), block.getLineNumber()) + .error("Tag <%s> can not appear inside <attr>, only <enum> or <flag>\n", + String8(block.getElementName(&len)).string()); + return UNKNOWN_ERROR; + } + + attr.createIfNeeded(outTable); + + if (attr.type == ResTable_map::TYPE_ANY) { + // No type was explicitly stated, so supplying enum tags + // implicitly creates an enum or flag. + attr.type = 0; + } + + if ((attr.type&(ResTable_map::TYPE_ENUM|ResTable_map::TYPE_FLAGS)) == 0) { + // Wasn't originally specified as an enum, so update its type. + attr.type |= localType; + if (!attr.hasErrors) { + char numberStr[16]; + sprintf(numberStr, "%d", attr.type); + err = outTable->addBag(SourcePos(in->getPrintableSource(), block.getLineNumber()), + myPackage, attr16, attr.ident, String16(""), + String16("^type"), String16(numberStr), NULL, NULL, true); + if (err != NO_ERROR) { + attr.hasErrors = true; + } + } + } else if ((uint32_t)(attr.type&(ResTable_map::TYPE_ENUM|ResTable_map::TYPE_FLAGS)) != localType) { + if (localType == ResTable_map::TYPE_ENUM) { + SourcePos(in->getPrintableSource(), block.getLineNumber()) + .error("<enum> attribute can not be used inside a flags format\n"); + attr.hasErrors = true; + } else { + SourcePos(in->getPrintableSource(), block.getLineNumber()) + .error("<flag> attribute can not be used inside a enum format\n"); + attr.hasErrors = true; + } + } + + String16 itemIdent; + ssize_t itemIdentIdx = block.indexOfAttribute(NULL, "name"); + if (itemIdentIdx >= 0) { + itemIdent = String16(block.getAttributeStringValue(itemIdentIdx, &len)); + } else { + SourcePos(in->getPrintableSource(), block.getLineNumber()) + .error("A 'name' attribute is required for <enum> or <flag>\n"); + attr.hasErrors = true; + } + + String16 value; + ssize_t valueIdx = block.indexOfAttribute(NULL, "value"); + if (valueIdx >= 0) { + value = String16(block.getAttributeStringValue(valueIdx, &len)); + } else { + SourcePos(in->getPrintableSource(), block.getLineNumber()) + .error("A 'value' attribute is required for <enum> or <flag>\n"); + attr.hasErrors = true; + } + if (!attr.hasErrors && !ResTable::stringToInt(value.string(), value.size(), NULL)) { + SourcePos(in->getPrintableSource(), block.getLineNumber()) + .error("Tag <enum> or <flag> 'value' attribute must be a number," + " not \"%s\"\n", + String8(value).string()); + attr.hasErrors = true; + } + + // Make sure an id is defined for this enum/flag identifier... + if (!attr.hasErrors && !outTable->hasBagOrEntry(itemIdent, &id16, &myPackage)) { + err = outTable->startBag(SourcePos(in->getPrintableSource(), block.getLineNumber()), + myPackage, id16, itemIdent, String16(), NULL); + if (err != NO_ERROR) { + attr.hasErrors = true; + } + } + + if (!attr.hasErrors) { + if (enumOrFlagsComment.size() == 0) { + enumOrFlagsComment.append(mayOrMust(attr.type, + ResTable_map::TYPE_ENUM|ResTable_map::TYPE_FLAGS)); + enumOrFlagsComment.append((attr.type&ResTable_map::TYPE_ENUM) + ? String16(" be one of the following constant values.") + : String16(" be one or more (separated by '|') of the following constant values.")); + enumOrFlagsComment.append(String16("</p>\n<table border=\"2\" width=\"85%\" align=\"center\" frame=\"hsides\" rules=\"all\" cellpadding=\"5\">\n" + "<colgroup align=\"left\" />\n" + "<colgroup align=\"left\" />\n" + "<colgroup align=\"left\" />\n" + "<tr><th>Constant<th>Value<th>Description</tr>")); + } + + enumOrFlagsComment.append(String16("\n<tr><th><code>")); + enumOrFlagsComment.append(itemIdent); + enumOrFlagsComment.append(String16("</code><td>")); + enumOrFlagsComment.append(value); + enumOrFlagsComment.append(String16("<td>")); + if (block.getComment(&len)) { + enumOrFlagsComment.append(String16(block.getComment(&len))); + } + enumOrFlagsComment.append(String16("</tr>")); + + err = outTable->addBag(SourcePos(in->getPrintableSource(), block.getLineNumber()), + myPackage, + attr16, attr.ident, String16(""), + itemIdent, value, NULL, NULL, false, true); + if (err != NO_ERROR) { + attr.hasErrors = true; + } + } + } else if (code == ResXMLTree::END_TAG) { + if (strcmp16(block.getElementName(&len), attr16.string()) == 0) { + break; + } + if ((attr.type&ResTable_map::TYPE_ENUM) != 0) { + if (strcmp16(block.getElementName(&len), enum16.string()) != 0) { + SourcePos(in->getPrintableSource(), block.getLineNumber()) + .error("Found tag </%s> where </enum> is expected\n", + String8(block.getElementName(&len)).string()); + return UNKNOWN_ERROR; + } + } else { + if (strcmp16(block.getElementName(&len), flag16.string()) != 0) { + SourcePos(in->getPrintableSource(), block.getLineNumber()) + .error("Found tag </%s> where </flag> is expected\n", + String8(block.getElementName(&len)).string()); + return UNKNOWN_ERROR; + } + } + } + } + + if (!attr.hasErrors && attr.added) { + appendTypeInfo(outTable, myPackage, attr16, attr.ident, attr.type, gFormatFlags); + } + + if (!attr.hasErrors && enumOrFlagsComment.size() > 0) { + enumOrFlagsComment.append(String16("\n</table>")); + outTable->appendTypeComment(myPackage, attr16, attr.ident, enumOrFlagsComment); + } + + + return NO_ERROR; +} + +bool localeIsDefined(const ResTable_config& config) +{ + return config.locale == 0; +} + +status_t parseAndAddBag(Bundle* bundle, + const sp<AaptFile>& in, + ResXMLTree* block, + const ResTable_config& config, + const String16& myPackage, + const String16& curType, + const String16& ident, + const String16& parentIdent, + const String16& itemIdent, + int32_t curFormat, + bool pseudolocalize, + const bool overwrite, + ResourceTable* outTable) +{ + status_t err; + const String16 item16("item"); + + String16 str; + Vector<StringPool::entry_style_span> spans; + err = parseStyledString(bundle, in->getPrintableSource().string(), + block, item16, &str, &spans, + pseudolocalize); + if (err != NO_ERROR) { + return err; + } + + NOISY(printf("Adding resource bag entry l=%c%c c=%c%c orien=%d d=%d " + " pid=%s, bag=%s, id=%s: %s\n", + config.language[0], config.language[1], + config.country[0], config.country[1], + config.orientation, config.density, + String8(parentIdent).string(), + String8(ident).string(), + String8(itemIdent).string(), + String8(str).string())); + + err = outTable->addBag(SourcePos(in->getPrintableSource(), block->getLineNumber()), + myPackage, curType, ident, parentIdent, itemIdent, str, + &spans, &config, overwrite, false, curFormat); + return err; +} + + +status_t parseAndAddEntry(Bundle* bundle, + const sp<AaptFile>& in, + ResXMLTree* block, + const ResTable_config& config, + const String16& myPackage, + const String16& curType, + const String16& ident, + const String16& curTag, + bool curIsStyled, + int32_t curFormat, + bool pseudolocalize, + const bool overwrite, + ResourceTable* outTable) +{ + status_t err; + + String16 str; + Vector<StringPool::entry_style_span> spans; + err = parseStyledString(bundle, in->getPrintableSource().string(), block, + curTag, &str, curIsStyled ? &spans : NULL, + pseudolocalize); + + if (err < NO_ERROR) { + return err; + } + + NOISY(printf("Adding resource entry l=%c%c c=%c%c orien=%d d=%d id=%s: %s\n", + config.language[0], config.language[1], + config.country[0], config.country[1], + config.orientation, config.density, + String8(ident).string(), String8(str).string())); + + err = outTable->addEntry(SourcePos(in->getPrintableSource(), block->getLineNumber()), + myPackage, curType, ident, str, &spans, &config, + false, curFormat, overwrite); + + return err; +} + +status_t compileResourceFile(Bundle* bundle, + const sp<AaptAssets>& assets, + const sp<AaptFile>& in, + const ResTable_config& defParams, + const bool overwrite, + ResourceTable* outTable) +{ + ResXMLTree block; + status_t err = parseXMLResource(in, &block, false, true); + if (err != NO_ERROR) { + return err; + } + + // Top-level tag. + const String16 resources16("resources"); + + // Identifier declaration tags. + const String16 declare_styleable16("declare-styleable"); + const String16 attr16("attr"); + + // Data creation organizational tags. + const String16 string16("string"); + const String16 drawable16("drawable"); + const String16 color16("color"); + const String16 bool16("bool"); + const String16 integer16("integer"); + const String16 dimen16("dimen"); + const String16 fraction16("fraction"); + const String16 style16("style"); + const String16 plurals16("plurals"); + const String16 array16("array"); + const String16 string_array16("string-array"); + const String16 integer_array16("integer-array"); + const String16 public16("public"); + const String16 private_symbols16("private-symbols"); + const String16 skip16("skip"); + const String16 eat_comment16("eat-comment"); + + // Data creation tags. + const String16 bag16("bag"); + const String16 item16("item"); + + // Attribute type constants. + const String16 enum16("enum"); + + // plural values + const String16 other16("other"); + const String16 quantityOther16("^other"); + const String16 zero16("zero"); + const String16 quantityZero16("^zero"); + const String16 one16("one"); + const String16 quantityOne16("^one"); + const String16 two16("two"); + const String16 quantityTwo16("^two"); + const String16 few16("few"); + const String16 quantityFew16("^few"); + const String16 many16("many"); + const String16 quantityMany16("^many"); + + // useful attribute names and special values + const String16 name16("name"); + const String16 translatable16("translatable"); + const String16 false16("false"); + + const String16 myPackage(assets->getPackage()); + + bool hasErrors = false; + + uint32_t nextPublicId = 0; + + ResXMLTree::event_code_t code; + do { + code = block.next(); + } while (code == ResXMLTree::START_NAMESPACE); + + size_t len; + if (code != ResXMLTree::START_TAG) { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "No start tag found\n"); + return UNKNOWN_ERROR; + } + if (strcmp16(block.getElementName(&len), resources16.string()) != 0) { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "Invalid start tag %s\n", String8(block.getElementName(&len)).string()); + return UNKNOWN_ERROR; + } + + ResTable_config curParams(defParams); + + ResTable_config pseudoParams(curParams); + pseudoParams.language[0] = 'z'; + pseudoParams.language[1] = 'z'; + pseudoParams.country[0] = 'Z'; + pseudoParams.country[1] = 'Z'; + + while ((code=block.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::START_TAG) { + const String16* curTag = NULL; + String16 curType; + int32_t curFormat = ResTable_map::TYPE_ANY; + bool curIsBag = false; + bool curIsStyled = false; + bool curIsPseudolocalizable = false; + bool localHasErrors = false; + + if (strcmp16(block.getElementName(&len), skip16.string()) == 0) { + while ((code=block.next()) != ResXMLTree::END_DOCUMENT + && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::END_TAG) { + if (strcmp16(block.getElementName(&len), skip16.string()) == 0) { + break; + } + } + } + continue; + + } else if (strcmp16(block.getElementName(&len), eat_comment16.string()) == 0) { + while ((code=block.next()) != ResXMLTree::END_DOCUMENT + && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::END_TAG) { + if (strcmp16(block.getElementName(&len), eat_comment16.string()) == 0) { + break; + } + } + } + continue; + + } else if (strcmp16(block.getElementName(&len), public16.string()) == 0) { + SourcePos srcPos(in->getPrintableSource(), block.getLineNumber()); + + String16 type; + ssize_t typeIdx = block.indexOfAttribute(NULL, "type"); + if (typeIdx < 0) { + srcPos.error("A 'type' attribute is required for <public>\n"); + hasErrors = localHasErrors = true; + } + type = String16(block.getAttributeStringValue(typeIdx, &len)); + + String16 name; + ssize_t nameIdx = block.indexOfAttribute(NULL, "name"); + if (nameIdx < 0) { + srcPos.error("A 'name' attribute is required for <public>\n"); + hasErrors = localHasErrors = true; + } + name = String16(block.getAttributeStringValue(nameIdx, &len)); + + uint32_t ident = 0; + ssize_t identIdx = block.indexOfAttribute(NULL, "id"); + if (identIdx >= 0) { + const char16_t* identStr = block.getAttributeStringValue(identIdx, &len); + Res_value identValue; + if (!ResTable::stringToInt(identStr, len, &identValue)) { + srcPos.error("Given 'id' attribute is not an integer: %s\n", + String8(block.getAttributeStringValue(identIdx, &len)).string()); + hasErrors = localHasErrors = true; + } else { + ident = identValue.data; + nextPublicId = ident+1; + } + } else if (nextPublicId == 0) { + srcPos.error("No 'id' attribute supplied <public>," + " and no previous id defined in this file.\n"); + hasErrors = localHasErrors = true; + } else if (!localHasErrors) { + ident = nextPublicId; + nextPublicId++; + } + + if (!localHasErrors) { + err = outTable->addPublic(srcPos, myPackage, type, name, ident); + if (err < NO_ERROR) { + hasErrors = localHasErrors = true; + } + } + if (!localHasErrors) { + sp<AaptSymbols> symbols = assets->getSymbolsFor(String8("R")); + if (symbols != NULL) { + symbols = symbols->addNestedSymbol(String8(type), srcPos); + } + if (symbols != NULL) { + symbols->makeSymbolPublic(String8(name), srcPos); + String16 comment( + block.getComment(&len) ? block.getComment(&len) : nulStr); + symbols->appendComment(String8(name), comment, srcPos); + } else { + srcPos.error("Unable to create symbols!\n"); + hasErrors = localHasErrors = true; + } + } + + while ((code=block.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::END_TAG) { + if (strcmp16(block.getElementName(&len), public16.string()) == 0) { + break; + } + } + } + continue; + + } else if (strcmp16(block.getElementName(&len), private_symbols16.string()) == 0) { + String16 pkg; + ssize_t pkgIdx = block.indexOfAttribute(NULL, "package"); + if (pkgIdx < 0) { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "A 'package' attribute is required for <private-symbols>\n"); + hasErrors = localHasErrors = true; + } + pkg = String16(block.getAttributeStringValue(pkgIdx, &len)); + if (!localHasErrors) { + assets->setSymbolsPrivatePackage(String8(pkg)); + } + + while ((code=block.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::END_TAG) { + if (strcmp16(block.getElementName(&len), private_symbols16.string()) == 0) { + break; + } + } + } + continue; + + } else if (strcmp16(block.getElementName(&len), declare_styleable16.string()) == 0) { + SourcePos srcPos(in->getPrintableSource(), block.getLineNumber()); + + String16 ident; + ssize_t identIdx = block.indexOfAttribute(NULL, "name"); + if (identIdx < 0) { + srcPos.error("A 'name' attribute is required for <declare-styleable>\n"); + hasErrors = localHasErrors = true; + } + ident = String16(block.getAttributeStringValue(identIdx, &len)); + + sp<AaptSymbols> symbols = assets->getSymbolsFor(String8("R")); + if (!localHasErrors) { + if (symbols != NULL) { + symbols = symbols->addNestedSymbol(String8("styleable"), srcPos); + } + sp<AaptSymbols> styleSymbols = symbols; + if (symbols != NULL) { + symbols = symbols->addNestedSymbol(String8(ident), srcPos); + } + if (symbols == NULL) { + srcPos.error("Unable to create symbols!\n"); + return UNKNOWN_ERROR; + } + + String16 comment( + block.getComment(&len) ? block.getComment(&len) : nulStr); + styleSymbols->appendComment(String8(ident), comment, srcPos); + } else { + symbols = NULL; + } + + while ((code=block.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::START_TAG) { + if (strcmp16(block.getElementName(&len), skip16.string()) == 0) { + while ((code=block.next()) != ResXMLTree::END_DOCUMENT + && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::END_TAG) { + if (strcmp16(block.getElementName(&len), skip16.string()) == 0) { + break; + } + } + } + continue; + } else if (strcmp16(block.getElementName(&len), eat_comment16.string()) == 0) { + while ((code=block.next()) != ResXMLTree::END_DOCUMENT + && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::END_TAG) { + if (strcmp16(block.getElementName(&len), eat_comment16.string()) == 0) { + break; + } + } + } + continue; + } else if (strcmp16(block.getElementName(&len), attr16.string()) != 0) { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "Tag <%s> can not appear inside <declare-styleable>, only <attr>\n", + String8(block.getElementName(&len)).string()); + return UNKNOWN_ERROR; + } + + String16 comment( + block.getComment(&len) ? block.getComment(&len) : nulStr); + String16 itemIdent; + err = compileAttribute(in, block, myPackage, outTable, &itemIdent, true); + if (err != NO_ERROR) { + hasErrors = localHasErrors = true; + } + + if (symbols != NULL) { + SourcePos srcPos(String8(in->getPrintableSource()), block.getLineNumber()); + symbols->addSymbol(String8(itemIdent), 0, srcPos); + symbols->appendComment(String8(itemIdent), comment, srcPos); + //printf("Attribute %s comment: %s\n", String8(itemIdent).string(), + // String8(comment).string()); + } + } else if (code == ResXMLTree::END_TAG) { + if (strcmp16(block.getElementName(&len), declare_styleable16.string()) == 0) { + break; + } + + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "Found tag </%s> where </attr> is expected\n", + String8(block.getElementName(&len)).string()); + return UNKNOWN_ERROR; + } + } + continue; + + } else if (strcmp16(block.getElementName(&len), attr16.string()) == 0) { + err = compileAttribute(in, block, myPackage, outTable, NULL); + if (err != NO_ERROR) { + hasErrors = true; + } + continue; + + } else if (strcmp16(block.getElementName(&len), item16.string()) == 0) { + curTag = &item16; + ssize_t attri = block.indexOfAttribute(NULL, "type"); + if (attri >= 0) { + curType = String16(block.getAttributeStringValue(attri, &len)); + ssize_t formatIdx = block.indexOfAttribute(NULL, "format"); + if (formatIdx >= 0) { + String16 formatStr = String16(block.getAttributeStringValue( + formatIdx, &len)); + curFormat = parse_flags(formatStr.string(), formatStr.size(), + gFormatFlags); + if (curFormat == 0) { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "Tag <item> 'format' attribute value \"%s\" not valid\n", + String8(formatStr).string()); + hasErrors = localHasErrors = true; + } + } + } else { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "A 'type' attribute is required for <item>\n"); + hasErrors = localHasErrors = true; + } + curIsStyled = true; + } else if (strcmp16(block.getElementName(&len), string16.string()) == 0) { + // Note the existence and locale of every string we process + char rawLocale[16]; + curParams.getLocale(rawLocale); + String8 locale(rawLocale); + String16 name; + String16 translatable; + + size_t n = block.getAttributeCount(); + for (size_t i = 0; i < n; i++) { + size_t length; + const uint16_t* attr = block.getAttributeName(i, &length); + if (strcmp16(attr, name16.string()) == 0) { + name.setTo(block.getAttributeStringValue(i, &length)); + } else if (strcmp16(attr, translatable16.string()) == 0) { + translatable.setTo(block.getAttributeStringValue(i, &length)); + } + } + + if (name.size() > 0) { + if (translatable == false16) { + // Untranslatable strings must only exist in the default [empty] locale + if (locale.size() > 0) { + fprintf(stderr, "aapt: warning: string '%s' in %s marked untranslatable but exists" + " in locale '%s'\n", String8(name).string(), + bundle->getResourceSourceDirs()[0], + locale.string()); + // hasErrors = localHasErrors = true; + } else { + // Intentionally empty block: + // + // Don't add untranslatable strings to the localization table; that + // way if we later see localizations of them, they'll be flagged as + // having no default translation. + } + } else { + outTable->addLocalization(name, locale); + } + } + + curTag = &string16; + curType = string16; + curFormat = ResTable_map::TYPE_REFERENCE|ResTable_map::TYPE_STRING; + curIsStyled = true; + curIsPseudolocalizable = true; + } else if (strcmp16(block.getElementName(&len), drawable16.string()) == 0) { + curTag = &drawable16; + curType = drawable16; + curFormat = ResTable_map::TYPE_REFERENCE|ResTable_map::TYPE_COLOR; + } else if (strcmp16(block.getElementName(&len), color16.string()) == 0) { + curTag = &color16; + curType = color16; + curFormat = ResTable_map::TYPE_REFERENCE|ResTable_map::TYPE_COLOR; + } else if (strcmp16(block.getElementName(&len), bool16.string()) == 0) { + curTag = &bool16; + curType = bool16; + curFormat = ResTable_map::TYPE_REFERENCE|ResTable_map::TYPE_BOOLEAN; + } else if (strcmp16(block.getElementName(&len), integer16.string()) == 0) { + curTag = &integer16; + curType = integer16; + curFormat = ResTable_map::TYPE_REFERENCE|ResTable_map::TYPE_INTEGER; + } else if (strcmp16(block.getElementName(&len), dimen16.string()) == 0) { + curTag = &dimen16; + curType = dimen16; + curFormat = ResTable_map::TYPE_REFERENCE|ResTable_map::TYPE_DIMENSION; + } else if (strcmp16(block.getElementName(&len), fraction16.string()) == 0) { + curTag = &fraction16; + curType = fraction16; + curFormat = ResTable_map::TYPE_REFERENCE|ResTable_map::TYPE_FRACTION; + } else if (strcmp16(block.getElementName(&len), bag16.string()) == 0) { + curTag = &bag16; + curIsBag = true; + ssize_t attri = block.indexOfAttribute(NULL, "type"); + if (attri >= 0) { + curType = String16(block.getAttributeStringValue(attri, &len)); + } else { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "A 'type' attribute is required for <bag>\n"); + hasErrors = localHasErrors = true; + } + } else if (strcmp16(block.getElementName(&len), style16.string()) == 0) { + curTag = &style16; + curType = style16; + curIsBag = true; + } else if (strcmp16(block.getElementName(&len), plurals16.string()) == 0) { + curTag = &plurals16; + curType = plurals16; + curIsBag = true; + } else if (strcmp16(block.getElementName(&len), array16.string()) == 0) { + curTag = &array16; + curType = array16; + curIsBag = true; + ssize_t formatIdx = block.indexOfAttribute(NULL, "format"); + if (formatIdx >= 0) { + String16 formatStr = String16(block.getAttributeStringValue( + formatIdx, &len)); + curFormat = parse_flags(formatStr.string(), formatStr.size(), + gFormatFlags); + if (curFormat == 0) { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "Tag <array> 'format' attribute value \"%s\" not valid\n", + String8(formatStr).string()); + hasErrors = localHasErrors = true; + } + } + } else if (strcmp16(block.getElementName(&len), string_array16.string()) == 0) { + curTag = &string_array16; + curType = array16; + curFormat = ResTable_map::TYPE_REFERENCE|ResTable_map::TYPE_STRING; + curIsBag = true; + curIsPseudolocalizable = true; + } else if (strcmp16(block.getElementName(&len), integer_array16.string()) == 0) { + curTag = &integer_array16; + curType = array16; + curFormat = ResTable_map::TYPE_REFERENCE|ResTable_map::TYPE_INTEGER; + curIsBag = true; + } else { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "Found tag %s where item is expected\n", + String8(block.getElementName(&len)).string()); + return UNKNOWN_ERROR; + } + + String16 ident; + ssize_t identIdx = block.indexOfAttribute(NULL, "name"); + if (identIdx >= 0) { + ident = String16(block.getAttributeStringValue(identIdx, &len)); + } else { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "A 'name' attribute is required for <%s>\n", + String8(*curTag).string()); + hasErrors = localHasErrors = true; + } + + String16 comment(block.getComment(&len) ? block.getComment(&len) : nulStr); + + if (curIsBag) { + // Figure out the parent of this bag... + String16 parentIdent; + ssize_t parentIdentIdx = block.indexOfAttribute(NULL, "parent"); + if (parentIdentIdx >= 0) { + parentIdent = String16(block.getAttributeStringValue(parentIdentIdx, &len)); + } else { + ssize_t sep = ident.findLast('.'); + if (sep >= 0) { + parentIdent.setTo(ident, sep); + } + } + + if (!localHasErrors) { + err = outTable->startBag(SourcePos(in->getPrintableSource(), block.getLineNumber()), + myPackage, curType, ident, parentIdent, &curParams); + if (err != NO_ERROR) { + hasErrors = localHasErrors = true; + } + } + + ssize_t elmIndex = 0; + char elmIndexStr[14]; + while ((code=block.next()) != ResXMLTree::END_DOCUMENT + && code != ResXMLTree::BAD_DOCUMENT) { + + if (code == ResXMLTree::START_TAG) { + if (strcmp16(block.getElementName(&len), item16.string()) != 0) { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "Tag <%s> can not appear inside <%s>, only <item>\n", + String8(block.getElementName(&len)).string(), + String8(*curTag).string()); + return UNKNOWN_ERROR; + } + + String16 itemIdent; + if (curType == array16) { + sprintf(elmIndexStr, "^index_%d", (int)elmIndex++); + itemIdent = String16(elmIndexStr); + } else if (curType == plurals16) { + ssize_t itemIdentIdx = block.indexOfAttribute(NULL, "quantity"); + if (itemIdentIdx >= 0) { + String16 quantity16(block.getAttributeStringValue(itemIdentIdx, &len)); + if (quantity16 == other16) { + itemIdent = quantityOther16; + } + else if (quantity16 == zero16) { + itemIdent = quantityZero16; + } + else if (quantity16 == one16) { + itemIdent = quantityOne16; + } + else if (quantity16 == two16) { + itemIdent = quantityTwo16; + } + else if (quantity16 == few16) { + itemIdent = quantityFew16; + } + else if (quantity16 == many16) { + itemIdent = quantityMany16; + } + else { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "Illegal 'quantity' attribute is <item> inside <plurals>\n"); + hasErrors = localHasErrors = true; + } + } else { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "A 'quantity' attribute is required for <item> inside <plurals>\n"); + hasErrors = localHasErrors = true; + } + } else { + ssize_t itemIdentIdx = block.indexOfAttribute(NULL, "name"); + if (itemIdentIdx >= 0) { + itemIdent = String16(block.getAttributeStringValue(itemIdentIdx, &len)); + } else { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "A 'name' attribute is required for <item>\n"); + hasErrors = localHasErrors = true; + } + } + + ResXMLParser::ResXMLPosition parserPosition; + block.getPosition(&parserPosition); + + err = parseAndAddBag(bundle, in, &block, curParams, myPackage, curType, + ident, parentIdent, itemIdent, curFormat, + false, overwrite, outTable); + if (err == NO_ERROR) { + if (curIsPseudolocalizable && localeIsDefined(curParams) + && bundle->getPseudolocalize()) { + // pseudolocalize here +#if 1 + block.setPosition(parserPosition); + err = parseAndAddBag(bundle, in, &block, pseudoParams, myPackage, + curType, ident, parentIdent, itemIdent, curFormat, true, + overwrite, outTable); +#endif + } + } + if (err != NO_ERROR) { + hasErrors = localHasErrors = true; + } + } else if (code == ResXMLTree::END_TAG) { + if (strcmp16(block.getElementName(&len), curTag->string()) != 0) { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "Found tag </%s> where </%s> is expected\n", + String8(block.getElementName(&len)).string(), + String8(*curTag).string()); + return UNKNOWN_ERROR; + } + break; + } + } + } else { + ResXMLParser::ResXMLPosition parserPosition; + block.getPosition(&parserPosition); + + err = parseAndAddEntry(bundle, in, &block, curParams, myPackage, curType, ident, + *curTag, curIsStyled, curFormat, false, overwrite, outTable); + + if (err < NO_ERROR) { // Why err < NO_ERROR instead of err != NO_ERROR? + hasErrors = localHasErrors = true; + } + else if (err == NO_ERROR) { + if (curIsPseudolocalizable && localeIsDefined(curParams) + && bundle->getPseudolocalize()) { + // pseudolocalize here + block.setPosition(parserPosition); + err = parseAndAddEntry(bundle, in, &block, pseudoParams, myPackage, curType, + ident, *curTag, curIsStyled, curFormat, true, false, outTable); + if (err != NO_ERROR) { + hasErrors = localHasErrors = true; + } + } + } + } + +#if 0 + if (comment.size() > 0) { + printf("Comment for @%s:%s/%s: %s\n", String8(myPackage).string(), + String8(curType).string(), String8(ident).string(), + String8(comment).string()); + } +#endif + if (!localHasErrors) { + outTable->appendComment(myPackage, curType, ident, comment, false); + } + } + else if (code == ResXMLTree::END_TAG) { + if (strcmp16(block.getElementName(&len), resources16.string()) != 0) { + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "Unexpected end tag %s\n", String8(block.getElementName(&len)).string()); + return UNKNOWN_ERROR; + } + } + else if (code == ResXMLTree::START_NAMESPACE || code == ResXMLTree::END_NAMESPACE) { + } + else if (code == ResXMLTree::TEXT) { + if (isWhitespace(block.getText(&len))) { + continue; + } + SourcePos(in->getPrintableSource(), block.getLineNumber()).error( + "Found text \"%s\" where item tag is expected\n", + String8(block.getText(&len)).string()); + return UNKNOWN_ERROR; + } + } + + return hasErrors ? UNKNOWN_ERROR : NO_ERROR; +} + +ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage) + : mAssetsPackage(assetsPackage), mNextPackageId(1), mHaveAppPackage(false), + mIsAppPackage(!bundle->getExtending()), + mNumLocal(0), + mBundle(bundle) +{ +} + +status_t ResourceTable::addIncludedResources(Bundle* bundle, const sp<AaptAssets>& assets) +{ + status_t err = assets->buildIncludedResources(bundle); + if (err != NO_ERROR) { + return err; + } + + // For future reference to included resources. + mAssets = assets; + + const ResTable& incl = assets->getIncludedResources(); + + // Retrieve all the packages. + const size_t N = incl.getBasePackageCount(); + for (size_t phase=0; phase<2; phase++) { + for (size_t i=0; i<N; i++) { + String16 name(incl.getBasePackageName(i)); + uint32_t id = incl.getBasePackageId(i); + // First time through: only add base packages (id + // is not 0); second time through add the other + // packages. + if (phase != 0) { + if (id != 0) { + // Skip base packages -- already one. + id = 0; + } else { + // Assign a dynamic id. + id = mNextPackageId; + } + } else if (id != 0) { + if (id == 127) { + if (mHaveAppPackage) { + fprintf(stderr, "Included resource have two application packages!\n"); + return UNKNOWN_ERROR; + } + mHaveAppPackage = true; + } + if (mNextPackageId > id) { + fprintf(stderr, "Included base package ID %d already in use!\n", id); + return UNKNOWN_ERROR; + } + } + if (id != 0) { + NOISY(printf("Including package %s with ID=%d\n", + String8(name).string(), id)); + sp<Package> p = new Package(name, id); + mPackages.add(name, p); + mOrderedPackages.add(p); + + if (id >= mNextPackageId) { + mNextPackageId = id+1; + } + } + } + } + + // Every resource table always has one first entry, the bag attributes. + const SourcePos unknown(String8("????"), 0); + sp<Type> attr = getType(mAssetsPackage, String16("attr"), unknown); + + return NO_ERROR; +} + +status_t ResourceTable::addPublic(const SourcePos& sourcePos, + const String16& package, + const String16& type, + const String16& name, + const uint32_t ident) +{ + uint32_t rid = mAssets->getIncludedResources() + .identifierForName(name.string(), name.size(), + type.string(), type.size(), + package.string(), package.size()); + if (rid != 0) { + sourcePos.error("Error declaring public resource %s/%s for included package %s\n", + String8(type).string(), String8(name).string(), + String8(package).string()); + return UNKNOWN_ERROR; + } + + sp<Type> t = getType(package, type, sourcePos); + if (t == NULL) { + return UNKNOWN_ERROR; + } + return t->addPublic(sourcePos, name, ident); +} + +status_t ResourceTable::addEntry(const SourcePos& sourcePos, + const String16& package, + const String16& type, + const String16& name, + const String16& value, + const Vector<StringPool::entry_style_span>* style, + const ResTable_config* params, + const bool doSetIndex, + const int32_t format, + const bool overwrite) +{ + // Check for adding entries in other packages... for now we do + // nothing. We need to do the right thing here to support skinning. + uint32_t rid = mAssets->getIncludedResources() + .identifierForName(name.string(), name.size(), + type.string(), type.size(), + package.string(), package.size()); + if (rid != 0) { + return NO_ERROR; + } + +#if 0 + if (name == String16("left")) { + printf("Adding entry left: file=%s, line=%d, type=%s, value=%s\n", + sourcePos.file.string(), sourcePos.line, String8(type).string(), + String8(value).string()); + } +#endif + + sp<Entry> e = getEntry(package, type, name, sourcePos, params, doSetIndex); + if (e == NULL) { + return UNKNOWN_ERROR; + } + status_t err = e->setItem(sourcePos, value, style, format, overwrite); + if (err == NO_ERROR) { + mNumLocal++; + } + return err; +} + +status_t ResourceTable::startBag(const SourcePos& sourcePos, + const String16& package, + const String16& type, + const String16& name, + const String16& bagParent, + const ResTable_config* params, + bool replace, bool isId) +{ + // Check for adding entries in other packages... for now we do + // nothing. We need to do the right thing here to support skinning. + uint32_t rid = mAssets->getIncludedResources() + .identifierForName(name.string(), name.size(), + type.string(), type.size(), + package.string(), package.size()); + if (rid != 0) { + return NO_ERROR; + } + +#if 0 + if (name == String16("left")) { + printf("Adding bag left: file=%s, line=%d, type=%s\n", + sourcePos.file.striing(), sourcePos.line, String8(type).string()); + } +#endif + + sp<Entry> e = getEntry(package, type, name, sourcePos, params); + if (e == NULL) { + return UNKNOWN_ERROR; + } + + // If a parent is explicitly specified, set it. + if (bagParent.size() > 0) { + String16 curPar = e->getParent(); + if (curPar.size() > 0 && curPar != bagParent) { + sourcePos.error("Conflicting parents specified, was '%s', now '%s'\n", + String8(e->getParent()).string(), + String8(bagParent).string()); + return UNKNOWN_ERROR; + } + e->setParent(bagParent); + } + + return e->makeItABag(sourcePos); +} + +status_t ResourceTable::addBag(const SourcePos& sourcePos, + const String16& package, + const String16& type, + const String16& name, + const String16& bagParent, + const String16& bagKey, + const String16& value, + const Vector<StringPool::entry_style_span>* style, + const ResTable_config* params, + bool replace, bool isId, const int32_t format) +{ + // Check for adding entries in other packages... for now we do + // nothing. We need to do the right thing here to support skinning. + uint32_t rid = mAssets->getIncludedResources() + .identifierForName(name.string(), name.size(), + type.string(), type.size(), + package.string(), package.size()); + if (rid != 0) { + return NO_ERROR; + } + +#if 0 + if (name == String16("left")) { + printf("Adding bag left: file=%s, line=%d, type=%s\n", + sourcePos.file.striing(), sourcePos.line, String8(type).string()); + } +#endif + + sp<Entry> e = getEntry(package, type, name, sourcePos, params); + if (e == NULL) { + return UNKNOWN_ERROR; + } + + // If a parent is explicitly specified, set it. + if (bagParent.size() > 0) { + String16 curPar = e->getParent(); + if (curPar.size() > 0 && curPar != bagParent) { + sourcePos.error("Conflicting parents specified, was '%s', now '%s'\n", + String8(e->getParent()).string(), + String8(bagParent).string()); + return UNKNOWN_ERROR; + } + e->setParent(bagParent); + } + + const bool first = e->getBag().indexOfKey(bagKey) < 0; + status_t err = e->addToBag(sourcePos, bagKey, value, style, replace, isId, format); + if (err == NO_ERROR && first) { + mNumLocal++; + } + return err; +} + +bool ResourceTable::hasBagOrEntry(const String16& package, + const String16& type, + const String16& name) const +{ + // First look for this in the included resources... + uint32_t rid = mAssets->getIncludedResources() + .identifierForName(name.string(), name.size(), + type.string(), type.size(), + package.string(), package.size()); + if (rid != 0) { + return true; + } + + sp<Package> p = mPackages.valueFor(package); + if (p != NULL) { + sp<Type> t = p->getTypes().valueFor(type); + if (t != NULL) { + sp<ConfigList> c = t->getConfigs().valueFor(name); + if (c != NULL) return true; + } + } + + return false; +} + +bool ResourceTable::hasBagOrEntry(const String16& ref, + const String16* defType, + const String16* defPackage) +{ + String16 package, type, name; + if (!ResTable::expandResourceRef(ref.string(), ref.size(), &package, &type, &name, + defType, defPackage ? defPackage:&mAssetsPackage, NULL)) { + return false; + } + return hasBagOrEntry(package, type, name); +} + +bool ResourceTable::appendComment(const String16& package, + const String16& type, + const String16& name, + const String16& comment, + bool onlyIfEmpty) +{ + if (comment.size() <= 0) { + return true; + } + + sp<Package> p = mPackages.valueFor(package); + if (p != NULL) { + sp<Type> t = p->getTypes().valueFor(type); + if (t != NULL) { + sp<ConfigList> c = t->getConfigs().valueFor(name); + if (c != NULL) { + c->appendComment(comment, onlyIfEmpty); + return true; + } + } + } + return false; +} + +bool ResourceTable::appendTypeComment(const String16& package, + const String16& type, + const String16& name, + const String16& comment) +{ + if (comment.size() <= 0) { + return true; + } + + sp<Package> p = mPackages.valueFor(package); + if (p != NULL) { + sp<Type> t = p->getTypes().valueFor(type); + if (t != NULL) { + sp<ConfigList> c = t->getConfigs().valueFor(name); + if (c != NULL) { + c->appendTypeComment(comment); + return true; + } + } + } + return false; +} + +size_t ResourceTable::size() const { + return mPackages.size(); +} + +size_t ResourceTable::numLocalResources() const { + return mNumLocal; +} + +bool ResourceTable::hasResources() const { + return mNumLocal > 0; +} + +sp<AaptFile> ResourceTable::flatten(Bundle* bundle) +{ + sp<AaptFile> data = new AaptFile(String8(), AaptGroupEntry(), String8()); + status_t err = flatten(bundle, data); + return err == NO_ERROR ? data : NULL; +} + +inline uint32_t ResourceTable::getResId(const sp<Package>& p, + const sp<Type>& t, + uint32_t nameId) +{ + return makeResId(p->getAssignedId(), t->getIndex(), nameId); +} + +uint32_t ResourceTable::getResId(const String16& package, + const String16& type, + const String16& name, + bool onlyPublic) const +{ + sp<Package> p = mPackages.valueFor(package); + if (p == NULL) return 0; + + // First look for this in the included resources... + uint32_t specFlags = 0; + uint32_t rid = mAssets->getIncludedResources() + .identifierForName(name.string(), name.size(), + type.string(), type.size(), + package.string(), package.size(), + &specFlags); + if (rid != 0) { + if (onlyPublic) { + if ((specFlags & ResTable_typeSpec::SPEC_PUBLIC) == 0) { + return 0; + } + } + + if (Res_INTERNALID(rid)) { + return rid; + } + return Res_MAKEID(p->getAssignedId()-1, + Res_GETTYPE(rid), + Res_GETENTRY(rid)); + } + + sp<Type> t = p->getTypes().valueFor(type); + if (t == NULL) return 0; + sp<ConfigList> c = t->getConfigs().valueFor(name); + if (c == NULL) return 0; + int32_t ei = c->getEntryIndex(); + if (ei < 0) return 0; + return getResId(p, t, ei); +} + +uint32_t ResourceTable::getResId(const String16& ref, + const String16* defType, + const String16* defPackage, + const char** outErrorMsg, + bool onlyPublic) const +{ + String16 package, type, name; + if (!ResTable::expandResourceRef( + ref.string(), ref.size(), &package, &type, &name, + defType, defPackage ? defPackage:&mAssetsPackage, + outErrorMsg)) { + NOISY(printf("Expanding resource: ref=%s\n", + String8(ref).string())); + NOISY(printf("Expanding resource: defType=%s\n", + defType ? String8(*defType).string() : "NULL")); + NOISY(printf("Expanding resource: defPackage=%s\n", + defPackage ? String8(*defPackage).string() : "NULL")); + NOISY(printf("Expanding resource: ref=%s\n", String8(ref).string())); + NOISY(printf("Expanded resource: p=%s, t=%s, n=%s, res=0\n", + String8(package).string(), String8(type).string(), + String8(name).string())); + return 0; + } + uint32_t res = getResId(package, type, name, onlyPublic); + NOISY(printf("Expanded resource: p=%s, t=%s, n=%s, res=%d\n", + String8(package).string(), String8(type).string(), + String8(name).string(), res)); + if (res == 0) { + if (outErrorMsg) + *outErrorMsg = "No resource found that matches the given name"; + } + return res; +} + +bool ResourceTable::isValidResourceName(const String16& s) +{ + const char16_t* p = s.string(); + bool first = true; + while (*p) { + if ((*p >= 'a' && *p <= 'z') + || (*p >= 'A' && *p <= 'Z') + || *p == '_' + || (!first && *p >= '0' && *p <= '9')) { + first = false; + p++; + continue; + } + return false; + } + return true; +} + +bool ResourceTable::stringToValue(Res_value* outValue, StringPool* pool, + const String16& str, + bool preserveSpaces, bool coerceType, + uint32_t attrID, + const Vector<StringPool::entry_style_span>* style, + String16* outStr, void* accessorCookie, + uint32_t attrType) +{ + String16 finalStr; + + bool res = true; + if (style == NULL || style->size() == 0) { + // Text is not styled so it can be any type... let's figure it out. + res = mAssets->getIncludedResources() + .stringToValue(outValue, &finalStr, str.string(), str.size(), preserveSpaces, + coerceType, attrID, NULL, &mAssetsPackage, this, + accessorCookie, attrType); + } else { + // Styled text can only be a string, and while collecting the style + // information we have already processed that string! + outValue->size = sizeof(Res_value); + outValue->res0 = 0; + outValue->dataType = outValue->TYPE_STRING; + outValue->data = 0; + finalStr = str; + } + + if (!res) { + return false; + } + + if (outValue->dataType == outValue->TYPE_STRING) { + // Should do better merging styles. + if (pool) { + if (style != NULL && style->size() > 0) { + outValue->data = pool->add(finalStr, *style); + } else { + outValue->data = pool->add(finalStr, true); + } + } else { + // Caller will fill this in later. + outValue->data = 0; + } + + if (outStr) { + *outStr = finalStr; + } + + } + + return true; +} + +uint32_t ResourceTable::getCustomResource( + const String16& package, const String16& type, const String16& name) const +{ + //printf("getCustomResource: %s %s %s\n", String8(package).string(), + // String8(type).string(), String8(name).string()); + sp<Package> p = mPackages.valueFor(package); + if (p == NULL) return 0; + sp<Type> t = p->getTypes().valueFor(type); + if (t == NULL) return 0; + sp<ConfigList> c = t->getConfigs().valueFor(name); + if (c == NULL) return 0; + int32_t ei = c->getEntryIndex(); + if (ei < 0) return 0; + return getResId(p, t, ei); +} + +uint32_t ResourceTable::getCustomResourceWithCreation( + const String16& package, const String16& type, const String16& name, + const bool createIfNotFound) +{ + uint32_t resId = getCustomResource(package, type, name); + if (resId != 0 || !createIfNotFound) { + return resId; + } + String16 value("false"); + + status_t status = addEntry(mCurrentXmlPos, package, type, name, value, NULL, NULL, true); + if (status == NO_ERROR) { + resId = getResId(package, type, name); + return resId; + } + return 0; +} + +uint32_t ResourceTable::getRemappedPackage(uint32_t origPackage) const +{ + return origPackage; +} + +bool ResourceTable::getAttributeType(uint32_t attrID, uint32_t* outType) +{ + //printf("getAttributeType #%08x\n", attrID); + Res_value value; + if (getItemValue(attrID, ResTable_map::ATTR_TYPE, &value)) { + //printf("getAttributeType #%08x (%s): #%08x\n", attrID, + // String8(getEntry(attrID)->getName()).string(), value.data); + *outType = value.data; + return true; + } + return false; +} + +bool ResourceTable::getAttributeMin(uint32_t attrID, uint32_t* outMin) +{ + //printf("getAttributeMin #%08x\n", attrID); + Res_value value; + if (getItemValue(attrID, ResTable_map::ATTR_MIN, &value)) { + *outMin = value.data; + return true; + } + return false; +} + +bool ResourceTable::getAttributeMax(uint32_t attrID, uint32_t* outMax) +{ + //printf("getAttributeMax #%08x\n", attrID); + Res_value value; + if (getItemValue(attrID, ResTable_map::ATTR_MAX, &value)) { + *outMax = value.data; + return true; + } + return false; +} + +uint32_t ResourceTable::getAttributeL10N(uint32_t attrID) +{ + //printf("getAttributeL10N #%08x\n", attrID); + Res_value value; + if (getItemValue(attrID, ResTable_map::ATTR_L10N, &value)) { + return value.data; + } + return ResTable_map::L10N_NOT_REQUIRED; +} + +bool ResourceTable::getLocalizationSetting() +{ + return mBundle->getRequireLocalization(); +} + +void ResourceTable::reportError(void* accessorCookie, const char* fmt, ...) +{ + if (accessorCookie != NULL && fmt != NULL) { + AccessorCookie* ac = (AccessorCookie*)accessorCookie; + int retval=0; + char buf[1024]; + va_list ap; + va_start(ap, fmt); + retval = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + ac->sourcePos.error("Error: %s (at '%s' with value '%s').\n", + buf, ac->attr.string(), ac->value.string()); + } +} + +bool ResourceTable::getAttributeKeys( + uint32_t attrID, Vector<String16>* outKeys) +{ + sp<const Entry> e = getEntry(attrID); + if (e != NULL) { + const size_t N = e->getBag().size(); + for (size_t i=0; i<N; i++) { + const String16& key = e->getBag().keyAt(i); + if (key.size() > 0 && key.string()[0] != '^') { + outKeys->add(key); + } + } + return true; + } + return false; +} + +bool ResourceTable::getAttributeEnum( + uint32_t attrID, const char16_t* name, size_t nameLen, + Res_value* outValue) +{ + //printf("getAttributeEnum #%08x %s\n", attrID, String8(name, nameLen).string()); + String16 nameStr(name, nameLen); + sp<const Entry> e = getEntry(attrID); + if (e != NULL) { + const size_t N = e->getBag().size(); + for (size_t i=0; i<N; i++) { + //printf("Comparing %s to %s\n", String8(name, nameLen).string(), + // String8(e->getBag().keyAt(i)).string()); + if (e->getBag().keyAt(i) == nameStr) { + return getItemValue(attrID, e->getBag().valueAt(i).bagKeyId, outValue); + } + } + } + return false; +} + +bool ResourceTable::getAttributeFlags( + uint32_t attrID, const char16_t* name, size_t nameLen, + Res_value* outValue) +{ + outValue->dataType = Res_value::TYPE_INT_HEX; + outValue->data = 0; + + //printf("getAttributeFlags #%08x %s\n", attrID, String8(name, nameLen).string()); + String16 nameStr(name, nameLen); + sp<const Entry> e = getEntry(attrID); + if (e != NULL) { + const size_t N = e->getBag().size(); + + const char16_t* end = name + nameLen; + const char16_t* pos = name; + bool failed = false; + while (pos < end && !failed) { + const char16_t* start = pos; + end++; + while (pos < end && *pos != '|') { + pos++; + } + + String16 nameStr(start, pos-start); + size_t i; + for (i=0; i<N; i++) { + //printf("Comparing \"%s\" to \"%s\"\n", String8(nameStr).string(), + // String8(e->getBag().keyAt(i)).string()); + if (e->getBag().keyAt(i) == nameStr) { + Res_value val; + bool got = getItemValue(attrID, e->getBag().valueAt(i).bagKeyId, &val); + if (!got) { + return false; + } + //printf("Got value: 0x%08x\n", val.data); + outValue->data |= val.data; + break; + } + } + + if (i >= N) { + // Didn't find this flag identifier. + return false; + } + if (pos < end) { + pos++; + } + } + + return true; + } + return false; +} + +status_t ResourceTable::assignResourceIds() +{ + const size_t N = mOrderedPackages.size(); + size_t pi; + status_t firstError = NO_ERROR; + + // First generate all bag attributes and assign indices. + for (pi=0; pi<N; pi++) { + sp<Package> p = mOrderedPackages.itemAt(pi); + if (p == NULL || p->getTypes().size() == 0) { + // Empty, skip! + continue; + } + + status_t err = p->applyPublicTypeOrder(); + if (err != NO_ERROR && firstError == NO_ERROR) { + firstError = err; + } + + // Generate attributes... + const size_t N = p->getOrderedTypes().size(); + size_t ti; + for (ti=0; ti<N; ti++) { + sp<Type> t = p->getOrderedTypes().itemAt(ti); + if (t == NULL) { + continue; + } + const size_t N = t->getOrderedConfigs().size(); + for (size_t ci=0; ci<N; ci++) { + sp<ConfigList> c = t->getOrderedConfigs().itemAt(ci); + if (c == NULL) { + continue; + } + const size_t N = c->getEntries().size(); + for (size_t ei=0; ei<N; ei++) { + sp<Entry> e = c->getEntries().valueAt(ei); + if (e == NULL) { + continue; + } + status_t err = e->generateAttributes(this, p->getName()); + if (err != NO_ERROR && firstError == NO_ERROR) { + firstError = err; + } + } + } + } + + const SourcePos unknown(String8("????"), 0); + sp<Type> attr = p->getType(String16("attr"), unknown); + + // Assign indices... + for (ti=0; ti<N; ti++) { + sp<Type> t = p->getOrderedTypes().itemAt(ti); + if (t == NULL) { + continue; + } + err = t->applyPublicEntryOrder(); + if (err != NO_ERROR && firstError == NO_ERROR) { + firstError = err; + } + + const size_t N = t->getOrderedConfigs().size(); + t->setIndex(ti+1); + + LOG_ALWAYS_FATAL_IF(ti == 0 && attr != t, + "First type is not attr!"); + + for (size_t ei=0; ei<N; ei++) { + sp<ConfigList> c = t->getOrderedConfigs().itemAt(ei); + if (c == NULL) { + continue; + } + c->setEntryIndex(ei); + } + } + + // Assign resource IDs to keys in bags... + for (ti=0; ti<N; ti++) { + sp<Type> t = p->getOrderedTypes().itemAt(ti); + if (t == NULL) { + continue; + } + const size_t N = t->getOrderedConfigs().size(); + for (size_t ci=0; ci<N; ci++) { + sp<ConfigList> c = t->getOrderedConfigs().itemAt(ci); + //printf("Ordered config #%d: %p\n", ci, c.get()); + const size_t N = c->getEntries().size(); + for (size_t ei=0; ei<N; ei++) { + sp<Entry> e = c->getEntries().valueAt(ei); + if (e == NULL) { + continue; + } + status_t err = e->assignResourceIds(this, p->getName()); + if (err != NO_ERROR && firstError == NO_ERROR) { + firstError = err; + } + } + } + } + } + return firstError; +} + +status_t ResourceTable::addSymbols(const sp<AaptSymbols>& outSymbols) { + const size_t N = mOrderedPackages.size(); + size_t pi; + + for (pi=0; pi<N; pi++) { + sp<Package> p = mOrderedPackages.itemAt(pi); + if (p->getTypes().size() == 0) { + // Empty, skip! + continue; + } + + const size_t N = p->getOrderedTypes().size(); + size_t ti; + + for (ti=0; ti<N; ti++) { + sp<Type> t = p->getOrderedTypes().itemAt(ti); + if (t == NULL) { + continue; + } + const size_t N = t->getOrderedConfigs().size(); + sp<AaptSymbols> typeSymbols; + typeSymbols = outSymbols->addNestedSymbol(String8(t->getName()), t->getPos()); + for (size_t ci=0; ci<N; ci++) { + sp<ConfigList> c = t->getOrderedConfigs().itemAt(ci); + if (c == NULL) { + continue; + } + uint32_t rid = getResId(p, t, ci); + if (rid == 0) { + return UNKNOWN_ERROR; + } + if (Res_GETPACKAGE(rid) == (size_t)(p->getAssignedId()-1)) { + typeSymbols->addSymbol(String8(c->getName()), rid, c->getPos()); + + String16 comment(c->getComment()); + typeSymbols->appendComment(String8(c->getName()), comment, c->getPos()); + //printf("Type symbol %s comment: %s\n", String8(e->getName()).string(), + // String8(comment).string()); + comment = c->getTypeComment(); + typeSymbols->appendTypeComment(String8(c->getName()), comment); + } else { +#if 0 + printf("**** NO MATCH: 0x%08x vs 0x%08x\n", + Res_GETPACKAGE(rid), p->getAssignedId()); +#endif + } + } + } + } + return NO_ERROR; +} + + +void +ResourceTable::addLocalization(const String16& name, const String8& locale) +{ + mLocalizations[name].insert(locale); +} + + +/*! + * Flag various sorts of localization problems. '+' indicates checks already implemented; + * '-' indicates checks that will be implemented in the future. + * + * + A localized string for which no default-locale version exists => warning + * + A string for which no version in an explicitly-requested locale exists => warning + * + A localized translation of an translateable="false" string => warning + * - A localized string not provided in every locale used by the table + */ +status_t +ResourceTable::validateLocalizations(void) +{ + status_t err = NO_ERROR; + const String8 defaultLocale; + + // For all strings... + for (map<String16, set<String8> >::iterator nameIter = mLocalizations.begin(); + nameIter != mLocalizations.end(); + nameIter++) { + const set<String8>& configSet = nameIter->second; // naming convenience + + // Look for strings with no default localization + if (configSet.count(defaultLocale) == 0) { + fprintf(stdout, "aapt: warning: string '%s' has no default translation in %s; found:", + String8(nameIter->first).string(), mBundle->getResourceSourceDirs()[0]); + for (set<String8>::iterator locales = configSet.begin(); + locales != configSet.end(); + locales++) { + fprintf(stdout, " %s", (*locales).string()); + } + fprintf(stdout, "\n"); + // !!! TODO: throw an error here in some circumstances + } + + // Check that all requested localizations are present for this string + if (mBundle->getConfigurations() != NULL && mBundle->getRequireLocalization()) { + const char* allConfigs = mBundle->getConfigurations(); + const char* start = allConfigs; + const char* comma; + + do { + String8 config; + comma = strchr(start, ','); + if (comma != NULL) { + config.setTo(start, comma - start); + start = comma + 1; + } else { + config.setTo(start); + } + + // don't bother with the pseudolocale "zz_ZZ" + if (config != "zz_ZZ") { + if (configSet.find(config) == configSet.end()) { + // okay, no specific localization found. it's possible that we are + // requiring a specific regional localization [e.g. de_DE] but there is an + // available string in the generic language localization [e.g. de]; + // consider that string to have fulfilled the localization requirement. + String8 region(config.string(), 2); + if (configSet.find(region) == configSet.end()) { + if (configSet.count(defaultLocale) == 0) { + fprintf(stdout, "aapt: error: " + "*** string '%s' has no default or required localization " + "for '%s' in %s\n", + String8(nameIter->first).string(), + config.string(), + mBundle->getResourceSourceDirs()[0]); + err = UNKNOWN_ERROR; + } + } + } + } + } while (comma != NULL); + } + } + + return err; +} + + +status_t +ResourceFilter::parse(const char* arg) +{ + if (arg == NULL) { + return 0; + } + + const char* p = arg; + const char* q; + + while (true) { + q = strchr(p, ','); + if (q == NULL) { + q = p + strlen(p); + } + + String8 part(p, q-p); + + if (part == "zz_ZZ") { + mContainsPseudo = true; + } + int axis; + uint32_t value; + if (AaptGroupEntry::parseNamePart(part, &axis, &value)) { + fprintf(stderr, "Invalid configuration: %s\n", arg); + fprintf(stderr, " "); + for (int i=0; i<p-arg; i++) { + fprintf(stderr, " "); + } + for (int i=0; i<q-p; i++) { + fprintf(stderr, "^"); + } + fprintf(stderr, "\n"); + return 1; + } + + ssize_t index = mData.indexOfKey(axis); + if (index < 0) { + mData.add(axis, SortedVector<uint32_t>()); + } + SortedVector<uint32_t>& sv = mData.editValueFor(axis); + sv.add(value); + // if it's a locale with a region, also match an unmodified locale of the + // same language + if (axis == AXIS_LANGUAGE) { + if (value & 0xffff0000) { + sv.add(value & 0x0000ffff); + } + } + p = q; + if (!*p) break; + p++; + } + + return NO_ERROR; +} + +bool +ResourceFilter::match(int axis, uint32_t value) +{ + if (value == 0) { + // they didn't specify anything so take everything + return true; + } + ssize_t index = mData.indexOfKey(axis); + if (index < 0) { + // we didn't request anything on this axis so take everything + return true; + } + const SortedVector<uint32_t>& sv = mData.valueAt(index); + return sv.indexOf(value) >= 0; +} + +bool +ResourceFilter::match(const ResTable_config& config) +{ + if (config.locale) { + uint32_t locale = (config.country[1] << 24) | (config.country[0] << 16) + | (config.language[1] << 8) | (config.language[0]); + if (!match(AXIS_LANGUAGE, locale)) { + return false; + } + } + if (!match(AXIS_ORIENTATION, config.orientation)) { + return false; + } + if (!match(AXIS_DENSITY, config.density)) { + return false; + } + if (!match(AXIS_TOUCHSCREEN, config.touchscreen)) { + return false; + } + if (!match(AXIS_KEYSHIDDEN, config.inputFlags)) { + return false; + } + if (!match(AXIS_KEYBOARD, config.keyboard)) { + return false; + } + if (!match(AXIS_NAVIGATION, config.navigation)) { + return false; + } + if (!match(AXIS_SCREENSIZE, config.screenSize)) { + return false; + } + if (!match(AXIS_VERSION, config.version)) { + return false; + } + return true; +} + +status_t ResourceTable::flatten(Bundle* bundle, const sp<AaptFile>& dest) +{ + ResourceFilter filter; + status_t err = filter.parse(bundle->getConfigurations()); + if (err != NO_ERROR) { + return err; + } + + const size_t N = mOrderedPackages.size(); + size_t pi; + + // Iterate through all data, collecting all values (strings, + // references, etc). + StringPool valueStrings; + for (pi=0; pi<N; pi++) { + sp<Package> p = mOrderedPackages.itemAt(pi); + if (p->getTypes().size() == 0) { + // Empty, skip! + continue; + } + + StringPool typeStrings; + StringPool keyStrings; + + const size_t N = p->getOrderedTypes().size(); + for (size_t ti=0; ti<N; ti++) { + sp<Type> t = p->getOrderedTypes().itemAt(ti); + if (t == NULL) { + typeStrings.add(String16("<empty>"), false); + continue; + } + typeStrings.add(t->getName(), false); + + const size_t N = t->getOrderedConfigs().size(); + for (size_t ci=0; ci<N; ci++) { + sp<ConfigList> c = t->getOrderedConfigs().itemAt(ci); + if (c == NULL) { + continue; + } + const size_t N = c->getEntries().size(); + for (size_t ei=0; ei<N; ei++) { + ConfigDescription config = c->getEntries().keyAt(ei); + if (!filter.match(config)) { + continue; + } + sp<Entry> e = c->getEntries().valueAt(ei); + if (e == NULL) { + continue; + } + e->setNameIndex(keyStrings.add(e->getName(), true)); + status_t err = e->prepareFlatten(&valueStrings, this); + if (err != NO_ERROR) { + return err; + } + } + } + } + + p->setTypeStrings(typeStrings.createStringBlock()); + p->setKeyStrings(keyStrings.createStringBlock()); + } + + ssize_t strAmt = 0; + + // Now build the array of package chunks. + Vector<sp<AaptFile> > flatPackages; + for (pi=0; pi<N; pi++) { + sp<Package> p = mOrderedPackages.itemAt(pi); + if (p->getTypes().size() == 0) { + // Empty, skip! + continue; + } + + const size_t N = p->getTypeStrings().size(); + + const size_t baseSize = sizeof(ResTable_package); + + // Start the package data. + sp<AaptFile> data = new AaptFile(String8(), AaptGroupEntry(), String8()); + ResTable_package* header = (ResTable_package*)data->editData(baseSize); + if (header == NULL) { + fprintf(stderr, "ERROR: out of memory creating ResTable_package\n"); + return NO_MEMORY; + } + memset(header, 0, sizeof(*header)); + header->header.type = htods(RES_TABLE_PACKAGE_TYPE); + header->header.headerSize = htods(sizeof(*header)); + header->id = htodl(p->getAssignedId()); + strcpy16_htod(header->name, p->getName().string()); + + // Write the string blocks. + const size_t typeStringsStart = data->getSize(); + sp<AaptFile> strFile = p->getTypeStringsData(); + ssize_t amt = data->writeData(strFile->getData(), strFile->getSize()); + #if PRINT_STRING_METRICS + fprintf(stderr, "**** type strings: %d\n", amt); + #endif + strAmt += amt; + if (amt < 0) { + return amt; + } + const size_t keyStringsStart = data->getSize(); + strFile = p->getKeyStringsData(); + amt = data->writeData(strFile->getData(), strFile->getSize()); + #if PRINT_STRING_METRICS + fprintf(stderr, "**** key strings: %d\n", amt); + #endif + strAmt += amt; + if (amt < 0) { + return amt; + } + + // Build the type chunks inside of this package. + for (size_t ti=0; ti<N; ti++) { + // Retrieve them in the same order as the type string block. + size_t len; + String16 typeName(p->getTypeStrings().stringAt(ti, &len)); + sp<Type> t = p->getTypes().valueFor(typeName); + LOG_ALWAYS_FATAL_IF(t == NULL && typeName != String16("<empty>"), + "Type name %s not found", + String8(typeName).string()); + + const size_t N = t != NULL ? t->getOrderedConfigs().size() : 0; + + // First write the typeSpec chunk, containing information about + // each resource entry in this type. + { + const size_t typeSpecSize = sizeof(ResTable_typeSpec) + sizeof(uint32_t)*N; + const size_t typeSpecStart = data->getSize(); + ResTable_typeSpec* tsHeader = (ResTable_typeSpec*) + (((uint8_t*)data->editData(typeSpecStart+typeSpecSize)) + typeSpecStart); + if (tsHeader == NULL) { + fprintf(stderr, "ERROR: out of memory creating ResTable_typeSpec\n"); + return NO_MEMORY; + } + memset(tsHeader, 0, sizeof(*tsHeader)); + tsHeader->header.type = htods(RES_TABLE_TYPE_SPEC_TYPE); + tsHeader->header.headerSize = htods(sizeof(*tsHeader)); + tsHeader->header.size = htodl(typeSpecSize); + tsHeader->id = ti+1; + tsHeader->entryCount = htodl(N); + + uint32_t* typeSpecFlags = (uint32_t*) + (((uint8_t*)data->editData()) + + typeSpecStart + sizeof(ResTable_typeSpec)); + memset(typeSpecFlags, 0, sizeof(uint32_t)*N); + + for (size_t ei=0; ei<N; ei++) { + sp<ConfigList> cl = t->getOrderedConfigs().itemAt(ei); + if (cl->getPublic()) { + typeSpecFlags[ei] |= htodl(ResTable_typeSpec::SPEC_PUBLIC); + } + const size_t CN = cl->getEntries().size(); + for (size_t ci=0; ci<CN; ci++) { + if (!filter.match(cl->getEntries().keyAt(ci))) { + continue; + } + for (size_t cj=ci+1; cj<CN; cj++) { + if (!filter.match(cl->getEntries().keyAt(cj))) { + continue; + } + typeSpecFlags[ei] |= htodl( + cl->getEntries().keyAt(ci).diff(cl->getEntries().keyAt(cj))); + } + } + } + } + + // We need to write one type chunk for each configuration for + // which we have entries in this type. + const size_t NC = t->getUniqueConfigs().size(); + + const size_t typeSize = sizeof(ResTable_type) + sizeof(uint32_t)*N; + + for (size_t ci=0; ci<NC; ci++) { + ConfigDescription config = t->getUniqueConfigs().itemAt(ci); + + NOISY(printf("Writing config %d config: imsi:%d/%d lang:%c%c cnt:%c%c " + "orien:%d touch:%d density:%d key:%d inp:%d nav:%d w:%d h:%d\n", + ti+1, + config.mcc, config.mnc, + config.language[0] ? config.language[0] : '-', + config.language[1] ? config.language[1] : '-', + config.country[0] ? config.country[0] : '-', + config.country[1] ? config.country[1] : '-', + config.orientation, + config.touchscreen, + config.density, + config.keyboard, + config.inputFlags, + config.navigation, + config.screenWidth, + config.screenHeight)); + + if (!filter.match(config)) { + continue; + } + + const size_t typeStart = data->getSize(); + + ResTable_type* tHeader = (ResTable_type*) + (((uint8_t*)data->editData(typeStart+typeSize)) + typeStart); + if (tHeader == NULL) { + fprintf(stderr, "ERROR: out of memory creating ResTable_type\n"); + return NO_MEMORY; + } + + memset(tHeader, 0, sizeof(*tHeader)); + tHeader->header.type = htods(RES_TABLE_TYPE_TYPE); + tHeader->header.headerSize = htods(sizeof(*tHeader)); + tHeader->id = ti+1; + tHeader->entryCount = htodl(N); + tHeader->entriesStart = htodl(typeSize); + tHeader->config = config; + NOISY(printf("Writing type %d config: imsi:%d/%d lang:%c%c cnt:%c%c " + "orien:%d touch:%d density:%d key:%d inp:%d nav:%d w:%d h:%d\n", + ti+1, + tHeader->config.mcc, tHeader->config.mnc, + tHeader->config.language[0] ? tHeader->config.language[0] : '-', + tHeader->config.language[1] ? tHeader->config.language[1] : '-', + tHeader->config.country[0] ? tHeader->config.country[0] : '-', + tHeader->config.country[1] ? tHeader->config.country[1] : '-', + tHeader->config.orientation, + tHeader->config.touchscreen, + tHeader->config.density, + tHeader->config.keyboard, + tHeader->config.inputFlags, + tHeader->config.navigation, + tHeader->config.screenWidth, + tHeader->config.screenHeight)); + tHeader->config.swapHtoD(); + + // Build the entries inside of this type. + for (size_t ei=0; ei<N; ei++) { + sp<ConfigList> cl = t->getOrderedConfigs().itemAt(ei); + sp<Entry> e = cl->getEntries().valueFor(config); + + // Set the offset for this entry in its type. + uint32_t* index = (uint32_t*) + (((uint8_t*)data->editData()) + + typeStart + sizeof(ResTable_type)); + if (e != NULL) { + index[ei] = htodl(data->getSize()-typeStart-typeSize); + + // Create the entry. + ssize_t amt = e->flatten(bundle, data, cl->getPublic()); + if (amt < 0) { + return amt; + } + } else { + index[ei] = htodl(ResTable_type::NO_ENTRY); + } + } + + // Fill in the rest of the type information. + tHeader = (ResTable_type*) + (((uint8_t*)data->editData()) + typeStart); + tHeader->header.size = htodl(data->getSize()-typeStart); + } + } + + // Fill in the rest of the package information. + header = (ResTable_package*)data->editData(); + header->header.size = htodl(data->getSize()); + header->typeStrings = htodl(typeStringsStart); + header->lastPublicType = htodl(p->getTypeStrings().size()); + header->keyStrings = htodl(keyStringsStart); + header->lastPublicKey = htodl(p->getKeyStrings().size()); + + flatPackages.add(data); + } + + // And now write out the final chunks. + const size_t dataStart = dest->getSize(); + + { + // blah + ResTable_header header; + memset(&header, 0, sizeof(header)); + header.header.type = htods(RES_TABLE_TYPE); + header.header.headerSize = htods(sizeof(header)); + header.packageCount = htodl(flatPackages.size()); + status_t err = dest->writeData(&header, sizeof(header)); + if (err != NO_ERROR) { + fprintf(stderr, "ERROR: out of memory creating ResTable_header\n"); + return err; + } + } + + ssize_t strStart = dest->getSize(); + err = valueStrings.writeStringBlock(dest); + if (err != NO_ERROR) { + return err; + } + + ssize_t amt = (dest->getSize()-strStart); + strAmt += amt; + #if PRINT_STRING_METRICS + fprintf(stderr, "**** value strings: %d\n", amt); + fprintf(stderr, "**** total strings: %d\n", strAmt); + #endif + + for (pi=0; pi<flatPackages.size(); pi++) { + err = dest->writeData(flatPackages[pi]->getData(), + flatPackages[pi]->getSize()); + if (err != NO_ERROR) { + fprintf(stderr, "ERROR: out of memory creating package chunk for ResTable_header\n"); + return err; + } + } + + ResTable_header* header = (ResTable_header*) + (((uint8_t*)dest->getData()) + dataStart); + header->header.size = htodl(dest->getSize() - dataStart); + + NOISY(aout << "Resource table:" + << HexDump(dest->getData(), dest->getSize()) << endl); + + #if PRINT_STRING_METRICS + fprintf(stderr, "**** total resource table size: %d / %d%% strings\n", + dest->getSize(), (strAmt*100)/dest->getSize()); + #endif + + return NO_ERROR; +} + +void ResourceTable::writePublicDefinitions(const String16& package, FILE* fp) +{ + fprintf(fp, + "<!-- This file contains <public> resource definitions for all\n" + " resources that were generated from the source data. -->\n" + "\n" + "<resources>\n"); + + writePublicDefinitions(package, fp, true); + writePublicDefinitions(package, fp, false); + + fprintf(fp, + "\n" + "</resources>\n"); +} + +void ResourceTable::writePublicDefinitions(const String16& package, FILE* fp, bool pub) +{ + bool didHeader = false; + + sp<Package> pkg = mPackages.valueFor(package); + if (pkg != NULL) { + const size_t NT = pkg->getOrderedTypes().size(); + for (size_t i=0; i<NT; i++) { + sp<Type> t = pkg->getOrderedTypes().itemAt(i); + if (t == NULL) { + continue; + } + + bool didType = false; + + const size_t NC = t->getOrderedConfigs().size(); + for (size_t j=0; j<NC; j++) { + sp<ConfigList> c = t->getOrderedConfigs().itemAt(j); + if (c == NULL) { + continue; + } + + if (c->getPublic() != pub) { + continue; + } + + if (!didType) { + fprintf(fp, "\n"); + didType = true; + } + if (!didHeader) { + if (pub) { + fprintf(fp," <!-- PUBLIC SECTION. These resources have been declared public.\n"); + fprintf(fp," Changes to these definitions will break binary compatibility. -->\n\n"); + } else { + fprintf(fp," <!-- PRIVATE SECTION. These resources have not been declared public.\n"); + fprintf(fp," You can make them public my moving these lines into a file in res/values. -->\n\n"); + } + didHeader = true; + } + if (!pub) { + const size_t NE = c->getEntries().size(); + for (size_t k=0; k<NE; k++) { + const SourcePos& pos = c->getEntries().valueAt(k)->getPos(); + if (pos.file != "") { + fprintf(fp," <!-- Declared at %s:%d -->\n", + pos.file.string(), pos.line); + } + } + } + fprintf(fp, " <public type=\"%s\" name=\"%s\" id=\"0x%08x\" />\n", + String8(t->getName()).string(), + String8(c->getName()).string(), + getResId(pkg, t, c->getEntryIndex())); + } + } + } +} + +ResourceTable::Item::Item(const SourcePos& _sourcePos, + bool _isId, + const String16& _value, + const Vector<StringPool::entry_style_span>* _style, + int32_t _format) + : sourcePos(_sourcePos) + , isId(_isId) + , value(_value) + , format(_format) + , bagKeyId(0) + , evaluating(false) +{ + if (_style) { + style = *_style; + } +} + +status_t ResourceTable::Entry::makeItABag(const SourcePos& sourcePos) +{ + if (mType == TYPE_BAG) { + return NO_ERROR; + } + if (mType == TYPE_UNKNOWN) { + mType = TYPE_BAG; + return NO_ERROR; + } + sourcePos.error("Resource entry %s is already defined as a single item.\n" + "%s:%d: Originally defined here.\n", + String8(mName).string(), + mItem.sourcePos.file.string(), mItem.sourcePos.line); + return UNKNOWN_ERROR; +} + +status_t ResourceTable::Entry::setItem(const SourcePos& sourcePos, + const String16& value, + const Vector<StringPool::entry_style_span>* style, + int32_t format, + const bool overwrite) +{ + Item item(sourcePos, false, value, style); + + if (mType == TYPE_BAG) { + const Item& item(mBag.valueAt(0)); + sourcePos.error("Resource entry %s is already defined as a bag.\n" + "%s:%d: Originally defined here.\n", + String8(mName).string(), + item.sourcePos.file.string(), item.sourcePos.line); + return UNKNOWN_ERROR; + } + if ( (mType != TYPE_UNKNOWN) && (overwrite == false) ) { + sourcePos.error("Resource entry %s is already defined.\n" + "%s:%d: Originally defined here.\n", + String8(mName).string(), + mItem.sourcePos.file.string(), mItem.sourcePos.line); + return UNKNOWN_ERROR; + } + + mType = TYPE_ITEM; + mItem = item; + mItemFormat = format; + return NO_ERROR; +} + +status_t ResourceTable::Entry::addToBag(const SourcePos& sourcePos, + const String16& key, const String16& value, + const Vector<StringPool::entry_style_span>* style, + bool replace, bool isId, int32_t format) +{ + status_t err = makeItABag(sourcePos); + if (err != NO_ERROR) { + return err; + } + + Item item(sourcePos, isId, value, style, format); + + // XXX NOTE: there is an error if you try to have a bag with two keys, + // one an attr and one an id, with the same name. Not something we + // currently ever have to worry about. + ssize_t origKey = mBag.indexOfKey(key); + if (origKey >= 0) { + if (!replace) { + const Item& item(mBag.valueAt(origKey)); + sourcePos.error("Resource entry %s already has bag item %s.\n" + "%s:%d: Originally defined here.\n", + String8(mName).string(), String8(key).string(), + item.sourcePos.file.string(), item.sourcePos.line); + return UNKNOWN_ERROR; + } + //printf("Replacing %s with %s\n", + // String8(mBag.valueFor(key).value).string(), String8(value).string()); + mBag.replaceValueFor(key, item); + } + + mBag.add(key, item); + return NO_ERROR; +} + +status_t ResourceTable::Entry::generateAttributes(ResourceTable* table, + const String16& package) +{ + const String16 attr16("attr"); + const String16 id16("id"); + const size_t N = mBag.size(); + for (size_t i=0; i<N; i++) { + const String16& key = mBag.keyAt(i); + const Item& it = mBag.valueAt(i); + if (it.isId) { + if (!table->hasBagOrEntry(key, &id16, &package)) { + String16 value("false"); + status_t err = table->addEntry(SourcePos(String8("<generated>"), 0), package, + id16, key, value); + if (err != NO_ERROR) { + return err; + } + } + } else if (!table->hasBagOrEntry(key, &attr16, &package)) { + +#if 1 +// fprintf(stderr, "ERROR: Bag attribute '%s' has not been defined.\n", +// String8(key).string()); +// const Item& item(mBag.valueAt(i)); +// fprintf(stderr, "Referenced from file %s line %d\n", +// item.sourcePos.file.string(), item.sourcePos.line); +// return UNKNOWN_ERROR; +#else + char numberStr[16]; + sprintf(numberStr, "%d", ResTable_map::TYPE_ANY); + status_t err = table->addBag(SourcePos("<generated>", 0), package, + attr16, key, String16(""), + String16("^type"), + String16(numberStr), NULL, NULL); + if (err != NO_ERROR) { + return err; + } +#endif + } + } + return NO_ERROR; +} + +status_t ResourceTable::Entry::assignResourceIds(ResourceTable* table, + const String16& package) +{ + bool hasErrors = false; + + if (mType == TYPE_BAG) { + const char* errorMsg; + const String16 style16("style"); + const String16 attr16("attr"); + const String16 id16("id"); + mParentId = 0; + if (mParent.size() > 0) { + mParentId = table->getResId(mParent, &style16, NULL, &errorMsg); + if (mParentId == 0) { + mPos.error("Error retrieving parent for item: %s '%s'.\n", + errorMsg, String8(mParent).string()); + hasErrors = true; + } + } + const size_t N = mBag.size(); + for (size_t i=0; i<N; i++) { + const String16& key = mBag.keyAt(i); + Item& it = mBag.editValueAt(i); + it.bagKeyId = table->getResId(key, + it.isId ? &id16 : &attr16, NULL, &errorMsg); + //printf("Bag key of %s: #%08x\n", String8(key).string(), it.bagKeyId); + if (it.bagKeyId == 0) { + it.sourcePos.error("Error: %s: %s '%s'.\n", errorMsg, + String8(it.isId ? id16 : attr16).string(), + String8(key).string()); + hasErrors = true; + } + } + } + return hasErrors ? UNKNOWN_ERROR : NO_ERROR; +} + +status_t ResourceTable::Entry::prepareFlatten(StringPool* strings, ResourceTable* table) +{ + if (mType == TYPE_ITEM) { + Item& it = mItem; + AccessorCookie ac(it.sourcePos, String8(mName), String8(it.value)); + if (!table->stringToValue(&it.parsedValue, strings, + it.value, false, true, 0, + &it.style, NULL, &ac, mItemFormat)) { + return UNKNOWN_ERROR; + } + } else if (mType == TYPE_BAG) { + const size_t N = mBag.size(); + for (size_t i=0; i<N; i++) { + const String16& key = mBag.keyAt(i); + Item& it = mBag.editValueAt(i); + AccessorCookie ac(it.sourcePos, String8(key), String8(it.value)); + if (!table->stringToValue(&it.parsedValue, strings, + it.value, false, true, it.bagKeyId, + &it.style, NULL, &ac, it.format)) { + return UNKNOWN_ERROR; + } + } + } else { + mPos.error("Error: entry %s is not a single item or a bag.\n", + String8(mName).string()); + return UNKNOWN_ERROR; + } + return NO_ERROR; +} + +ssize_t ResourceTable::Entry::flatten(Bundle* bundle, const sp<AaptFile>& data, bool isPublic) +{ + size_t amt = 0; + ResTable_entry header; + memset(&header, 0, sizeof(header)); + header.size = htods(sizeof(header)); + const type ty = this != NULL ? mType : TYPE_ITEM; + if (this != NULL) { + if (ty == TYPE_BAG) { + header.flags |= htods(header.FLAG_COMPLEX); + } + if (isPublic) { + header.flags |= htods(header.FLAG_PUBLIC); + } + header.key.index = htodl(mNameIndex); + } + if (ty != TYPE_BAG) { + status_t err = data->writeData(&header, sizeof(header)); + if (err != NO_ERROR) { + fprintf(stderr, "ERROR: out of memory creating ResTable_entry\n"); + return err; + } + + const Item& it = mItem; + Res_value par; + memset(&par, 0, sizeof(par)); + par.size = htods(it.parsedValue.size); + par.dataType = it.parsedValue.dataType; + par.res0 = it.parsedValue.res0; + par.data = htodl(it.parsedValue.data); + #if 0 + printf("Writing item (%s): type=%d, data=0x%x, res0=0x%x\n", + String8(mName).string(), it.parsedValue.dataType, + it.parsedValue.data, par.res0); + #endif + err = data->writeData(&par, it.parsedValue.size); + if (err != NO_ERROR) { + fprintf(stderr, "ERROR: out of memory creating Res_value\n"); + return err; + } + amt += it.parsedValue.size; + } else { + size_t N = mBag.size(); + size_t i; + // Create correct ordering of items. + KeyedVector<uint32_t, const Item*> items; + for (i=0; i<N; i++) { + const Item& it = mBag.valueAt(i); + items.add(it.bagKeyId, &it); + } + N = items.size(); + + ResTable_map_entry mapHeader; + memcpy(&mapHeader, &header, sizeof(header)); + mapHeader.size = htods(sizeof(mapHeader)); + mapHeader.parent.ident = htodl(mParentId); + mapHeader.count = htodl(N); + status_t err = data->writeData(&mapHeader, sizeof(mapHeader)); + if (err != NO_ERROR) { + fprintf(stderr, "ERROR: out of memory creating ResTable_entry\n"); + return err; + } + + for (i=0; i<N; i++) { + const Item& it = *items.valueAt(i); + ResTable_map map; + map.name.ident = htodl(it.bagKeyId); + map.value.size = htods(it.parsedValue.size); + map.value.dataType = it.parsedValue.dataType; + map.value.res0 = it.parsedValue.res0; + map.value.data = htodl(it.parsedValue.data); + err = data->writeData(&map, sizeof(map)); + if (err != NO_ERROR) { + fprintf(stderr, "ERROR: out of memory creating Res_value\n"); + return err; + } + amt += sizeof(map); + } + } + return amt; +} + +void ResourceTable::ConfigList::appendComment(const String16& comment, + bool onlyIfEmpty) +{ + if (comment.size() <= 0) { + return; + } + if (onlyIfEmpty && mComment.size() > 0) { + return; + } + if (mComment.size() > 0) { + mComment.append(String16("\n")); + } + mComment.append(comment); +} + +void ResourceTable::ConfigList::appendTypeComment(const String16& comment) +{ + if (comment.size() <= 0) { + return; + } + if (mTypeComment.size() > 0) { + mTypeComment.append(String16("\n")); + } + mTypeComment.append(comment); +} + +status_t ResourceTable::Type::addPublic(const SourcePos& sourcePos, + const String16& name, + const uint32_t ident) +{ + #if 0 + int32_t entryIdx = Res_GETENTRY(ident); + if (entryIdx < 0) { + sourcePos.error("Public resource %s/%s has an invalid 0 identifier (0x%08x).\n", + String8(mName).string(), String8(name).string(), ident); + return UNKNOWN_ERROR; + } + #endif + + int32_t typeIdx = Res_GETTYPE(ident); + if (typeIdx >= 0) { + typeIdx++; + if (mPublicIndex > 0 && mPublicIndex != typeIdx) { + sourcePos.error("Public resource %s/%s has conflicting type codes for its" + " public identifiers (0x%x vs 0x%x).\n", + String8(mName).string(), String8(name).string(), + mPublicIndex, typeIdx); + return UNKNOWN_ERROR; + } + mPublicIndex = typeIdx; + } + + if (mFirstPublicSourcePos == NULL) { + mFirstPublicSourcePos = new SourcePos(sourcePos); + } + + if (mPublic.indexOfKey(name) < 0) { + mPublic.add(name, Public(sourcePos, String16(), ident)); + } else { + Public& p = mPublic.editValueFor(name); + if (p.ident != ident) { + sourcePos.error("Public resource %s/%s has conflicting public identifiers" + " (0x%08x vs 0x%08x).\n" + "%s:%d: Originally defined here.\n", + String8(mName).string(), String8(name).string(), p.ident, ident, + p.sourcePos.file.string(), p.sourcePos.line); + return UNKNOWN_ERROR; + } + } + + return NO_ERROR; +} + +sp<ResourceTable::Entry> ResourceTable::Type::getEntry(const String16& entry, + const SourcePos& sourcePos, + const ResTable_config* config, + bool doSetIndex) +{ + int pos = -1; + sp<ConfigList> c = mConfigs.valueFor(entry); + if (c == NULL) { + c = new ConfigList(entry, sourcePos); + mConfigs.add(entry, c); + pos = (int)mOrderedConfigs.size(); + mOrderedConfigs.add(c); + if (doSetIndex) { + c->setEntryIndex(pos); + } + } + + ConfigDescription cdesc; + if (config) cdesc = *config; + + sp<Entry> e = c->getEntries().valueFor(cdesc); + if (e == NULL) { + if (config != NULL) { + NOISY(printf("New entry at %s:%d: imsi:%d/%d lang:%c%c cnt:%c%c " + "orien:%d touch:%d density:%d key:%d inp:%d nav:%d w:%d h:%d\n", + sourcePos.file.string(), sourcePos.line, + config->mcc, config->mnc, + config->language[0] ? config->language[0] : '-', + config->language[1] ? config->language[1] : '-', + config->country[0] ? config->country[0] : '-', + config->country[1] ? config->country[1] : '-', + config->orientation, + config->touchscreen, + config->density, + config->keyboard, + config->inputFlags, + config->navigation, + config->screenWidth, + config->screenHeight)); + } else { + NOISY(printf("New entry at %s:%d: NULL config\n", + sourcePos.file.string(), sourcePos.line)); + } + e = new Entry(entry, sourcePos); + c->addEntry(cdesc, e); + /* + if (doSetIndex) { + if (pos < 0) { + for (pos=0; pos<(int)mOrderedConfigs.size(); pos++) { + if (mOrderedConfigs[pos] == c) { + break; + } + } + if (pos >= (int)mOrderedConfigs.size()) { + sourcePos.error("Internal error: config not found in mOrderedConfigs when adding entry"); + return NULL; + } + } + e->setEntryIndex(pos); + } + */ + } + + mUniqueConfigs.add(cdesc); + + return e; +} + +status_t ResourceTable::Type::applyPublicEntryOrder() +{ + size_t N = mOrderedConfigs.size(); + Vector<sp<ConfigList> > origOrder(mOrderedConfigs); + bool hasError = false; + + size_t i; + for (i=0; i<N; i++) { + mOrderedConfigs.replaceAt(NULL, i); + } + + const size_t NP = mPublic.size(); + //printf("Ordering %d configs from %d public defs\n", N, NP); + size_t j; + for (j=0; j<NP; j++) { + const String16& name = mPublic.keyAt(j); + const Public& p = mPublic.valueAt(j); + int32_t idx = Res_GETENTRY(p.ident); + //printf("Looking for entry \"%s\"/\"%s\" (0x%08x) in %d...\n", + // String8(mName).string(), String8(name).string(), p.ident, N); + bool found = false; + for (i=0; i<N; i++) { + sp<ConfigList> e = origOrder.itemAt(i); + //printf("#%d: \"%s\"\n", i, String8(e->getName()).string()); + if (e->getName() == name) { + if (idx >= (int32_t)mOrderedConfigs.size()) { + p.sourcePos.error("Public entry identifier 0x%x entry index " + "is larger than available symbols (index %d, total symbols %d).\n", + p.ident, idx, mOrderedConfigs.size()); + hasError = true; + } else if (mOrderedConfigs.itemAt(idx) == NULL) { + e->setPublic(true); + e->setPublicSourcePos(p.sourcePos); + mOrderedConfigs.replaceAt(e, idx); + origOrder.removeAt(i); + N--; + found = true; + break; + } else { + sp<ConfigList> oe = mOrderedConfigs.itemAt(idx); + + p.sourcePos.error("Multiple entry names declared for public entry" + " identifier 0x%x in type %s (%s vs %s).\n" + "%s:%d: Originally defined here.", + idx+1, String8(mName).string(), + String8(oe->getName()).string(), + String8(name).string(), + oe->getPublicSourcePos().file.string(), + oe->getPublicSourcePos().line); + hasError = true; + } + } + } + + if (!found) { + p.sourcePos.error("Public symbol %s/%s declared here is not defined.", + String8(mName).string(), String8(name).string()); + hasError = true; + } + } + + //printf("Copying back in %d non-public configs, have %d\n", N, origOrder.size()); + + if (N != origOrder.size()) { + printf("Internal error: remaining private symbol count mismatch\n"); + N = origOrder.size(); + } + + j = 0; + for (i=0; i<N; i++) { + sp<ConfigList> e = origOrder.itemAt(i); + // There will always be enough room for the remaining entries. + while (mOrderedConfigs.itemAt(j) != NULL) { + j++; + } + mOrderedConfigs.replaceAt(e, j); + j++; + } + + return hasError ? UNKNOWN_ERROR : NO_ERROR; +} + +ResourceTable::Package::Package(const String16& name, ssize_t includedId) + : mName(name), mIncludedId(includedId), + mTypeStringsMapping(0xffffffff), + mKeyStringsMapping(0xffffffff) +{ +} + +sp<ResourceTable::Type> ResourceTable::Package::getType(const String16& type, + const SourcePos& sourcePos, + bool doSetIndex) +{ + sp<Type> t = mTypes.valueFor(type); + if (t == NULL) { + t = new Type(type, sourcePos); + mTypes.add(type, t); + mOrderedTypes.add(t); + if (doSetIndex) { + // For some reason the type's index is set to one plus the index + // in the mOrderedTypes list, rather than just the index. + t->setIndex(mOrderedTypes.size()); + } + } + return t; +} + +status_t ResourceTable::Package::setTypeStrings(const sp<AaptFile>& data) +{ + mTypeStringsData = data; + status_t err = setStrings(data, &mTypeStrings, &mTypeStringsMapping); + if (err != NO_ERROR) { + fprintf(stderr, "ERROR: Type string data is corrupt!\n"); + } + return err; +} + +status_t ResourceTable::Package::setKeyStrings(const sp<AaptFile>& data) +{ + mKeyStringsData = data; + status_t err = setStrings(data, &mKeyStrings, &mKeyStringsMapping); + if (err != NO_ERROR) { + fprintf(stderr, "ERROR: Key string data is corrupt!\n"); + } + return err; +} + +status_t ResourceTable::Package::setStrings(const sp<AaptFile>& data, + ResStringPool* strings, + DefaultKeyedVector<String16, uint32_t>* mappings) +{ + if (data->getData() == NULL) { + return UNKNOWN_ERROR; + } + + NOISY(aout << "Setting restable string pool: " + << HexDump(data->getData(), data->getSize()) << endl); + + status_t err = strings->setTo(data->getData(), data->getSize()); + if (err == NO_ERROR) { + const size_t N = strings->size(); + for (size_t i=0; i<N; i++) { + size_t len; + mappings->add(String16(strings->stringAt(i, &len)), i); + } + } + return err; +} + +status_t ResourceTable::Package::applyPublicTypeOrder() +{ + size_t N = mOrderedTypes.size(); + Vector<sp<Type> > origOrder(mOrderedTypes); + + size_t i; + for (i=0; i<N; i++) { + mOrderedTypes.replaceAt(NULL, i); + } + + for (i=0; i<N; i++) { + sp<Type> t = origOrder.itemAt(i); + int32_t idx = t->getPublicIndex(); + if (idx > 0) { + idx--; + while (idx >= (int32_t)mOrderedTypes.size()) { + mOrderedTypes.add(); + } + if (mOrderedTypes.itemAt(idx) != NULL) { + sp<Type> ot = mOrderedTypes.itemAt(idx); + t->getFirstPublicSourcePos().error("Multiple type names declared for public type" + " identifier 0x%x (%s vs %s).\n" + "%s:%d: Originally defined here.", + idx, String8(ot->getName()).string(), + String8(t->getName()).string(), + ot->getFirstPublicSourcePos().file.string(), + ot->getFirstPublicSourcePos().line); + return UNKNOWN_ERROR; + } + mOrderedTypes.replaceAt(t, idx); + origOrder.removeAt(i); + i--; + N--; + } + } + + size_t j=0; + for (i=0; i<N; i++) { + sp<Type> t = origOrder.itemAt(i); + // There will always be enough room for the remaining types. + while (mOrderedTypes.itemAt(j) != NULL) { + j++; + } + mOrderedTypes.replaceAt(t, j); + } + + return NO_ERROR; +} + +sp<ResourceTable::Package> ResourceTable::getPackage(const String16& package) +{ + sp<Package> p = mPackages.valueFor(package); + if (p == NULL) { + if (mIsAppPackage) { + if (mHaveAppPackage) { + fprintf(stderr, "Adding multiple application package resources; only one is allowed.\n" + "Use -x to create extended resources.\n"); + return NULL; + } + mHaveAppPackage = true; + p = new Package(package, 127); + } else { + p = new Package(package, mNextPackageId); + } + //printf("*** NEW PACKAGE: \"%s\" id=%d\n", + // String8(package).string(), p->getAssignedId()); + mPackages.add(package, p); + mOrderedPackages.add(p); + mNextPackageId++; + } + return p; +} + +sp<ResourceTable::Type> ResourceTable::getType(const String16& package, + const String16& type, + const SourcePos& sourcePos, + bool doSetIndex) +{ + sp<Package> p = getPackage(package); + if (p == NULL) { + return NULL; + } + return p->getType(type, sourcePos, doSetIndex); +} + +sp<ResourceTable::Entry> ResourceTable::getEntry(const String16& package, + const String16& type, + const String16& name, + const SourcePos& sourcePos, + const ResTable_config* config, + bool doSetIndex) +{ + sp<Type> t = getType(package, type, sourcePos, doSetIndex); + if (t == NULL) { + return NULL; + } + return t->getEntry(name, sourcePos, config, doSetIndex); +} + +sp<const ResourceTable::Entry> ResourceTable::getEntry(uint32_t resID, + const ResTable_config* config) const +{ + int pid = Res_GETPACKAGE(resID)+1; + const size_t N = mOrderedPackages.size(); + size_t i; + sp<Package> p; + for (i=0; i<N; i++) { + sp<Package> check = mOrderedPackages[i]; + if (check->getAssignedId() == pid) { + p = check; + break; + } + + } + if (p == NULL) { + fprintf(stderr, "WARNING: Package not found for resource #%08x\n", resID); + return NULL; + } + + int tid = Res_GETTYPE(resID); + if (tid < 0 || tid >= (int)p->getOrderedTypes().size()) { + fprintf(stderr, "WARNING: Type not found for resource #%08x\n", resID); + return NULL; + } + sp<Type> t = p->getOrderedTypes()[tid]; + + int eid = Res_GETENTRY(resID); + if (eid < 0 || eid >= (int)t->getOrderedConfigs().size()) { + fprintf(stderr, "WARNING: Entry not found for resource #%08x\n", resID); + return NULL; + } + + sp<ConfigList> c = t->getOrderedConfigs()[eid]; + if (c == NULL) { + fprintf(stderr, "WARNING: Entry not found for resource #%08x\n", resID); + return NULL; + } + + ConfigDescription cdesc; + if (config) cdesc = *config; + sp<Entry> e = c->getEntries().valueFor(cdesc); + if (c == NULL) { + fprintf(stderr, "WARNING: Entry configuration not found for resource #%08x\n", resID); + return NULL; + } + + return e; +} + +const ResourceTable::Item* ResourceTable::getItem(uint32_t resID, uint32_t attrID) const +{ + sp<const Entry> e = getEntry(resID); + if (e == NULL) { + return NULL; + } + + const size_t N = e->getBag().size(); + for (size_t i=0; i<N; i++) { + const Item& it = e->getBag().valueAt(i); + if (it.bagKeyId == 0) { + fprintf(stderr, "WARNING: ID not yet assigned to '%s' in bag '%s'\n", + String8(e->getName()).string(), + String8(e->getBag().keyAt(i)).string()); + } + if (it.bagKeyId == attrID) { + return ⁢ + } + } + + return NULL; +} + +bool ResourceTable::getItemValue( + uint32_t resID, uint32_t attrID, Res_value* outValue) +{ + const Item* item = getItem(resID, attrID); + + bool res = false; + if (item != NULL) { + if (item->evaluating) { + sp<const Entry> e = getEntry(resID); + const size_t N = e->getBag().size(); + size_t i; + for (i=0; i<N; i++) { + if (&e->getBag().valueAt(i) == item) { + break; + } + } + fprintf(stderr, "WARNING: Circular reference detected in key '%s' of bag '%s'\n", + String8(e->getName()).string(), + String8(e->getBag().keyAt(i)).string()); + return false; + } + item->evaluating = true; + res = stringToValue(outValue, NULL, item->value, false, false, item->bagKeyId); + NOISY( + if (res) { + printf("getItemValue of #%08x[#%08x] (%s): type=#%08x, data=#%08x\n", + resID, attrID, String8(getEntry(resID)->getName()).string(), + outValue->dataType, outValue->data); + } else { + printf("getItemValue of #%08x[#%08x]: failed\n", + resID, attrID); + } + ); + item->evaluating = false; + } + return res; +} diff --git a/tools/aapt/ResourceTable.h b/tools/aapt/ResourceTable.h new file mode 100644 index 0000000..e8fbd9b --- /dev/null +++ b/tools/aapt/ResourceTable.h @@ -0,0 +1,534 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#ifndef RESOURCE_TABLE_H +#define RESOURCE_TABLE_H + +#include "StringPool.h" +#include "SourcePos.h" + +#include <set> +#include <map> + +using namespace std; + +class ResourceTable; + +enum { + XML_COMPILE_STRIP_COMMENTS = 1<<0, + XML_COMPILE_ASSIGN_ATTRIBUTE_IDS = 1<<1, + XML_COMPILE_COMPACT_WHITESPACE = 1<<2, + XML_COMPILE_STRIP_WHITESPACE = 1<<3, + XML_COMPILE_STRIP_RAW_VALUES = 1<<4, + + XML_COMPILE_STANDARD_RESOURCE = + XML_COMPILE_STRIP_COMMENTS | XML_COMPILE_ASSIGN_ATTRIBUTE_IDS + | XML_COMPILE_STRIP_WHITESPACE | XML_COMPILE_STRIP_RAW_VALUES +}; + +status_t compileXmlFile(const sp<AaptAssets>& assets, + const sp<AaptFile>& target, + ResourceTable* table, + int options = XML_COMPILE_STANDARD_RESOURCE); + +status_t compileResourceFile(Bundle* bundle, + const sp<AaptAssets>& assets, + const sp<AaptFile>& in, + const ResTable_config& defParams, + const bool overwrite, + ResourceTable* outTable); + +struct AccessorCookie +{ + SourcePos sourcePos; + String8 attr; + String8 value; + + AccessorCookie(const SourcePos&p, const String8& a, const String8& v) + :sourcePos(p), + attr(a), + value(v) + { + } +}; + +class ResourceTable : public ResTable::Accessor +{ +public: + class Package; + class Type; + class Entry; + + ResourceTable(Bundle* bundle, const String16& assetsPackage); + + status_t addIncludedResources(Bundle* bundle, const sp<AaptAssets>& assets); + + status_t addPublic(const SourcePos& pos, + const String16& package, + const String16& type, + const String16& name, + const uint32_t ident); + + status_t addEntry(const SourcePos& pos, + const String16& package, + const String16& type, + const String16& name, + const String16& value, + const Vector<StringPool::entry_style_span>* style = NULL, + const ResTable_config* params = NULL, + const bool doSetIndex = false, + const int32_t format = ResTable_map::TYPE_ANY, + const bool overwrite = false); + + status_t startBag(const SourcePos& pos, + const String16& package, + const String16& type, + const String16& name, + const String16& bagParent, + const ResTable_config* params = NULL, + bool replace = false, + bool isId = false); + + status_t addBag(const SourcePos& pos, + const String16& package, + const String16& type, + const String16& name, + const String16& bagParent, + const String16& bagKey, + const String16& value, + const Vector<StringPool::entry_style_span>* style = NULL, + const ResTable_config* params = NULL, + bool replace = false, + bool isId = false, + const int32_t format = ResTable_map::TYPE_ANY); + + bool hasBagOrEntry(const String16& package, + const String16& type, + const String16& name) const; + + bool hasBagOrEntry(const String16& ref, + const String16* defType = NULL, + const String16* defPackage = NULL); + + bool appendComment(const String16& package, + const String16& type, + const String16& name, + const String16& comment, + bool onlyIfEmpty = false); + + bool appendTypeComment(const String16& package, + const String16& type, + const String16& name, + const String16& comment); + + size_t size() const; + size_t numLocalResources() const; + bool hasResources() const; + + sp<AaptFile> flatten(Bundle*); + + static inline uint32_t makeResId(uint32_t packageId, + uint32_t typeId, + uint32_t nameId) + { + return nameId | (typeId<<16) | (packageId<<24); + } + + static inline uint32_t getResId(const sp<Package>& p, + const sp<Type>& t, + uint32_t nameId); + + uint32_t getResId(const String16& package, + const String16& type, + const String16& name, + bool onlyPublic = false) const; + + uint32_t getResId(const String16& ref, + const String16* defType = NULL, + const String16* defPackage = NULL, + const char** outErrorMsg = NULL, + bool onlyPublic = false) const; + + static bool isValidResourceName(const String16& s); + + bool stringToValue(Res_value* outValue, StringPool* pool, + const String16& str, + bool preserveSpaces, bool coerceType, + uint32_t attrID, + const Vector<StringPool::entry_style_span>* style = NULL, + String16* outStr = NULL, void* accessorCookie = NULL, + uint32_t attrType = ResTable_map::TYPE_ANY); + + status_t assignResourceIds(); + status_t addSymbols(const sp<AaptSymbols>& outSymbols = NULL); + void addLocalization(const String16& name, const String8& locale); + status_t validateLocalizations(void); + + status_t flatten(Bundle*, const sp<AaptFile>& dest); + + void writePublicDefinitions(const String16& package, FILE* fp); + + virtual uint32_t getCustomResource(const String16& package, + const String16& type, + const String16& name) const; + virtual uint32_t getCustomResourceWithCreation(const String16& package, + const String16& type, + const String16& name, + const bool createIfNeeded); + virtual uint32_t getRemappedPackage(uint32_t origPackage) const; + virtual bool getAttributeType(uint32_t attrID, uint32_t* outType); + virtual bool getAttributeMin(uint32_t attrID, uint32_t* outMin); + virtual bool getAttributeMax(uint32_t attrID, uint32_t* outMax); + virtual bool getAttributeKeys(uint32_t attrID, Vector<String16>* outKeys); + virtual bool getAttributeEnum(uint32_t attrID, + const char16_t* name, size_t nameLen, + Res_value* outValue); + virtual bool getAttributeFlags(uint32_t attrID, + const char16_t* name, size_t nameLen, + Res_value* outValue); + virtual uint32_t getAttributeL10N(uint32_t attrID); + + virtual bool getLocalizationSetting(); + virtual void reportError(void* accessorCookie, const char* fmt, ...); + + void setCurrentXmlPos(const SourcePos& pos) { mCurrentXmlPos = pos; } + + class Item { + public: + Item() : isId(false), format(ResTable_map::TYPE_ANY), bagKeyId(0), evaluating(false) + { memset(&parsedValue, 0, sizeof(parsedValue)); } + Item(const SourcePos& pos, + bool _isId, + const String16& _value, + const Vector<StringPool::entry_style_span>* _style = NULL, + int32_t format = ResTable_map::TYPE_ANY); + Item(const Item& o) : sourcePos(o.sourcePos), + isId(o.isId), value(o.value), style(o.style), + format(o.format), bagKeyId(o.bagKeyId), evaluating(false) { + memset(&parsedValue, 0, sizeof(parsedValue)); + } + ~Item() { } + + Item& operator=(const Item& o) { + sourcePos = o.sourcePos; + isId = o.isId; + value = o.value; + style = o.style; + format = o.format; + bagKeyId = o.bagKeyId; + parsedValue = o.parsedValue; + return *this; + } + + SourcePos sourcePos; + mutable bool isId; + String16 value; + Vector<StringPool::entry_style_span> style; + int32_t format; + uint32_t bagKeyId; + mutable bool evaluating; + Res_value parsedValue; + }; + + class Entry : public RefBase { + public: + Entry(const String16& name, const SourcePos& pos) + : mName(name), mType(TYPE_UNKNOWN), + mItemFormat(ResTable_map::TYPE_ANY), mNameIndex(-1), mPos(pos) + { } + virtual ~Entry() { } + + enum type { + TYPE_UNKNOWN = 0, + TYPE_ITEM, + TYPE_BAG + }; + + String16 getName() const { return mName; } + type getType() const { return mType; } + + void setParent(const String16& parent) { mParent = parent; } + String16 getParent() const { return mParent; } + + status_t makeItABag(const SourcePos& sourcePos); + + status_t setItem(const SourcePos& pos, + const String16& value, + const Vector<StringPool::entry_style_span>* style = NULL, + int32_t format = ResTable_map::TYPE_ANY, + const bool overwrite = false); + + status_t addToBag(const SourcePos& pos, + const String16& key, const String16& value, + const Vector<StringPool::entry_style_span>* style = NULL, + bool replace=false, bool isId = false, + int32_t format = ResTable_map::TYPE_ANY); + + // Index of the entry's name string in the key pool. + int32_t getNameIndex() const { return mNameIndex; } + void setNameIndex(int32_t index) { mNameIndex = index; } + + const Item* getItem() const { return mType == TYPE_ITEM ? &mItem : NULL; } + const KeyedVector<String16, Item>& getBag() const { return mBag; } + + status_t generateAttributes(ResourceTable* table, + const String16& package); + + status_t assignResourceIds(ResourceTable* table, + const String16& package); + + status_t prepareFlatten(StringPool* strings, ResourceTable* table); + + ssize_t flatten(Bundle*, const sp<AaptFile>& data, bool isPublic); + + const SourcePos& getPos() const { return mPos; } + + private: + String16 mName; + String16 mParent; + type mType; + Item mItem; + int32_t mItemFormat; + KeyedVector<String16, Item> mBag; + int32_t mNameIndex; + uint32_t mParentId; + SourcePos mPos; + }; + + struct ConfigDescription : public ResTable_config { + ConfigDescription() { + memset(this, 0, sizeof(*this)); + size = sizeof(ResTable_config); + } + ConfigDescription(const ResTable_config&o) { + *static_cast<ResTable_config*>(this) = o; + size = sizeof(ResTable_config); + } + ConfigDescription(const ConfigDescription&o) { + *static_cast<ResTable_config*>(this) = o; + } + + ConfigDescription& operator=(const ResTable_config& o) { + *static_cast<ResTable_config*>(this) = o; + size = sizeof(ResTable_config); + return *this; + } + ConfigDescription& operator=(const ConfigDescription& o) { + *static_cast<ResTable_config*>(this) = o; + return *this; + } + + inline bool operator<(const ConfigDescription& o) const { return compare(o) < 0; } + inline bool operator<=(const ConfigDescription& o) const { return compare(o) <= 0; } + inline bool operator==(const ConfigDescription& o) const { return compare(o) == 0; } + inline bool operator!=(const ConfigDescription& o) const { return compare(o) != 0; } + inline bool operator>=(const ConfigDescription& o) const { return compare(o) >= 0; } + inline bool operator>(const ConfigDescription& o) const { return compare(o) > 0; } + }; + + class ConfigList : public RefBase { + public: + ConfigList(const String16& name, const SourcePos& pos) + : mName(name), mPos(pos), mPublic(false), mEntryIndex(-1) { } + virtual ~ConfigList() { } + + String16 getName() const { return mName; } + const SourcePos& getPos() const { return mPos; } + + void appendComment(const String16& comment, bool onlyIfEmpty = false); + const String16& getComment() const { return mComment; } + + void appendTypeComment(const String16& comment); + const String16& getTypeComment() const { return mTypeComment; } + + // Index of this entry in its Type. + int32_t getEntryIndex() const { return mEntryIndex; } + void setEntryIndex(int32_t index) { mEntryIndex = index; } + + void setPublic(bool pub) { mPublic = pub; } + bool getPublic() const { return mPublic; } + void setPublicSourcePos(const SourcePos& pos) { mPublicSourcePos = pos; } + const SourcePos& getPublicSourcePos() { return mPublicSourcePos; } + + void addEntry(const ResTable_config& config, const sp<Entry>& entry) { + mEntries.add(config, entry); + } + + const DefaultKeyedVector<ConfigDescription, sp<Entry> >& getEntries() const { return mEntries; } + private: + const String16 mName; + const SourcePos mPos; + String16 mComment; + String16 mTypeComment; + bool mPublic; + SourcePos mPublicSourcePos; + int32_t mEntryIndex; + DefaultKeyedVector<ConfigDescription, sp<Entry> > mEntries; + }; + + class Public { + public: + Public() : sourcePos(), ident(0) { } + Public(const SourcePos& pos, + const String16& _comment, + uint32_t _ident) + : sourcePos(pos), + comment(_comment), ident(_ident) { } + Public(const Public& o) : sourcePos(o.sourcePos), + comment(o.comment), ident(o.ident) { } + ~Public() { } + + Public& operator=(const Public& o) { + sourcePos = o.sourcePos; + comment = o.comment; + ident = o.ident; + return *this; + } + + SourcePos sourcePos; + String16 comment; + uint32_t ident; + }; + + class Type : public RefBase { + public: + Type(const String16& name, const SourcePos& pos) + : mName(name), mFirstPublicSourcePos(NULL), mPublicIndex(-1), mIndex(-1), mPos(pos) + { } + virtual ~Type() { delete mFirstPublicSourcePos; } + + status_t addPublic(const SourcePos& pos, + const String16& name, + const uint32_t ident); + + String16 getName() const { return mName; } + sp<Entry> getEntry(const String16& entry, + const SourcePos& pos, + const ResTable_config* config = NULL, + bool doSetIndex = false); + + const SourcePos& getFirstPublicSourcePos() const { return *mFirstPublicSourcePos; } + + int32_t getPublicIndex() const { return mPublicIndex; } + + int32_t getIndex() const { return mIndex; } + void setIndex(int32_t index) { mIndex = index; } + + status_t applyPublicEntryOrder(); + + const SortedVector<ConfigDescription>& getUniqueConfigs() const { return mUniqueConfigs; } + + const DefaultKeyedVector<String16, sp<ConfigList> >& getConfigs() const { return mConfigs; } + const Vector<sp<ConfigList> >& getOrderedConfigs() const { return mOrderedConfigs; } + + const SourcePos& getPos() const { return mPos; } + private: + String16 mName; + SourcePos* mFirstPublicSourcePos; + DefaultKeyedVector<String16, Public> mPublic; + SortedVector<ConfigDescription> mUniqueConfigs; + DefaultKeyedVector<String16, sp<ConfigList> > mConfigs; + Vector<sp<ConfigList> > mOrderedConfigs; + int32_t mPublicIndex; + int32_t mIndex; + SourcePos mPos; + }; + + class Package : public RefBase { + public: + Package(const String16& name, ssize_t includedId=-1); + virtual ~Package() { } + + String16 getName() const { return mName; } + sp<Type> getType(const String16& type, + const SourcePos& pos, + bool doSetIndex = false); + + ssize_t getAssignedId() const { return mIncludedId; } + + const ResStringPool& getTypeStrings() const { return mTypeStrings; } + uint32_t indexOfTypeString(const String16& s) const { return mTypeStringsMapping.valueFor(s); } + const sp<AaptFile> getTypeStringsData() const { return mTypeStringsData; } + status_t setTypeStrings(const sp<AaptFile>& data); + + const ResStringPool& getKeyStrings() const { return mKeyStrings; } + uint32_t indexOfKeyString(const String16& s) const { return mKeyStringsMapping.valueFor(s); } + const sp<AaptFile> getKeyStringsData() const { return mKeyStringsData; } + status_t setKeyStrings(const sp<AaptFile>& data); + + status_t applyPublicTypeOrder(); + + const DefaultKeyedVector<String16, sp<Type> >& getTypes() const { return mTypes; } + const Vector<sp<Type> >& getOrderedTypes() const { return mOrderedTypes; } + + private: + status_t setStrings(const sp<AaptFile>& data, + ResStringPool* strings, + DefaultKeyedVector<String16, uint32_t>* mappings); + + const String16 mName; + const ssize_t mIncludedId; + DefaultKeyedVector<String16, sp<Type> > mTypes; + Vector<sp<Type> > mOrderedTypes; + sp<AaptFile> mTypeStringsData; + sp<AaptFile> mKeyStringsData; + ResStringPool mTypeStrings; + ResStringPool mKeyStrings; + DefaultKeyedVector<String16, uint32_t> mTypeStringsMapping; + DefaultKeyedVector<String16, uint32_t> mKeyStringsMapping; + }; + +private: + void writePublicDefinitions(const String16& package, FILE* fp, bool pub); + sp<Package> getPackage(const String16& package); + sp<Type> getType(const String16& package, + const String16& type, + const SourcePos& pos, + bool doSetIndex = false); + sp<Entry> getEntry(const String16& package, + const String16& type, + const String16& name, + const SourcePos& pos, + const ResTable_config* config = NULL, + bool doSetIndex = false); + sp<const Entry> getEntry(uint32_t resID, + const ResTable_config* config = NULL) const; + const Item* getItem(uint32_t resID, uint32_t attrID) const; + bool getItemValue(uint32_t resID, uint32_t attrID, + Res_value* outValue); + + + String16 mAssetsPackage; + sp<AaptAssets> mAssets; + DefaultKeyedVector<String16, sp<Package> > mPackages; + Vector<sp<Package> > mOrderedPackages; + uint32_t mNextPackageId; + bool mHaveAppPackage; + bool mIsAppPackage; + size_t mNumLocal; + SourcePos mCurrentXmlPos; + Bundle* mBundle; + + // key = string resource name, value = set of locales in which that name is defined + map<String16, set<String8> > mLocalizations; +}; + +class ResourceFilter +{ +public: + ResourceFilter() : mData(), mContainsPseudo(false) {} + status_t parse(const char* arg); + bool match(int axis, uint32_t value); + bool match(const ResTable_config& config); + inline bool containsPseudo() { return mContainsPseudo; } + +private: + KeyedVector<int,SortedVector<uint32_t> > mData; + bool mContainsPseudo; +}; + + +#endif diff --git a/tools/aapt/SourcePos.cpp b/tools/aapt/SourcePos.cpp new file mode 100644 index 0000000..2761d18 --- /dev/null +++ b/tools/aapt/SourcePos.cpp @@ -0,0 +1,171 @@ +#include "SourcePos.h" + +#include <stdarg.h> +#include <vector> + +using namespace std; + + +// ErrorPos +// ============================================================================= +struct ErrorPos +{ + String8 file; + int line; + String8 error; + bool fatal; + + ErrorPos(); + ErrorPos(const ErrorPos& that); + ErrorPos(const String8& file, int line, const String8& error, bool fatal); + ~ErrorPos(); + bool operator<(const ErrorPos& rhs) const; + bool operator==(const ErrorPos& rhs) const; + ErrorPos& operator=(const ErrorPos& rhs); + + void print(FILE* to) const; +}; + +static vector<ErrorPos> g_errors; + +ErrorPos::ErrorPos() + :line(-1), fatal(false) +{ +} + +ErrorPos::ErrorPos(const ErrorPos& that) + :file(that.file), + line(that.line), + error(that.error), + fatal(that.fatal) +{ +} + +ErrorPos::ErrorPos(const String8& f, int l, const String8& e, bool fat) + :file(f), + line(l), + error(e), + fatal(fat) +{ +} + +ErrorPos::~ErrorPos() +{ +} + +bool +ErrorPos::operator<(const ErrorPos& rhs) const +{ + if (this->file < rhs.file) return true; + if (this->file == rhs.file) { + if (this->line < rhs.line) return true; + if (this->line == rhs.line) { + if (this->error < rhs.error) return true; + } + } + return false; +} + +bool +ErrorPos::operator==(const ErrorPos& rhs) const +{ + return this->file == rhs.file + && this->line == rhs.line + && this->error == rhs.error; +} + +ErrorPos& +ErrorPos::operator=(const ErrorPos& rhs) +{ + this->file = rhs.file; + this->line = rhs.line; + this->error = rhs.error; + return *this; +} + +void +ErrorPos::print(FILE* to) const +{ + const char* type = fatal ? "ERROR" : "WARNING"; + + if (this->line >= 0) { + fprintf(to, "%s:%d: %s %s\n", this->file.string(), this->line, type, this->error.string()); + } else { + fprintf(to, "%s: %s %s\n", this->file.string(), type, this->error.string()); + } +} + +// SourcePos +// ============================================================================= +SourcePos::SourcePos(const String8& f, int l) + : file(f), line(l) +{ +} + +SourcePos::SourcePos(const SourcePos& that) + : file(that.file), line(that.line) +{ +} + +SourcePos::SourcePos() + : file("???", 0), line(-1) +{ +} + +SourcePos::~SourcePos() +{ +} + +int +SourcePos::error(const char* fmt, ...) const +{ + int retval=0; + char buf[1024]; + va_list ap; + va_start(ap, fmt); + retval = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + char* p = buf + retval - 1; + while (p > buf && *p == '\n') { + *p = '\0'; + p--; + } + g_errors.push_back(ErrorPos(this->file, this->line, String8(buf), true)); + return retval; +} + +int +SourcePos::warning(const char* fmt, ...) const +{ + int retval=0; + char buf[1024]; + va_list ap; + va_start(ap, fmt); + retval = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + char* p = buf + retval - 1; + while (p > buf && *p == '\n') { + *p = '\0'; + p--; + } + ErrorPos(this->file, this->line, String8(buf), false).print(stderr); + return retval; +} + +bool +SourcePos::hasErrors() +{ + return g_errors.size() > 0; +} + +void +SourcePos::printErrors(FILE* to) +{ + vector<ErrorPos>::const_iterator it; + for (it=g_errors.begin(); it!=g_errors.end(); it++) { + it->print(to); + } +} + + + diff --git a/tools/aapt/SourcePos.h b/tools/aapt/SourcePos.h new file mode 100644 index 0000000..33f72a9 --- /dev/null +++ b/tools/aapt/SourcePos.h @@ -0,0 +1,28 @@ +#ifndef SOURCEPOS_H +#define SOURCEPOS_H + +#include <utils/String8.h> +#include <stdio.h> + +using namespace android; + +class SourcePos +{ +public: + String8 file; + int line; + + SourcePos(const String8& f, int l); + SourcePos(const SourcePos& that); + SourcePos(); + ~SourcePos(); + + int error(const char* fmt, ...) const; + int warning(const char* fmt, ...) const; + + static bool hasErrors(); + static void printErrors(FILE* to); +}; + + +#endif // SOURCEPOS_H diff --git a/tools/aapt/StringPool.cpp b/tools/aapt/StringPool.cpp new file mode 100644 index 0000000..878d3b1 --- /dev/null +++ b/tools/aapt/StringPool.cpp @@ -0,0 +1,369 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#include "StringPool.h" + +#include <utils/ByteOrder.h> + +#define NOISY(x) //x + +void strcpy16_htod(uint16_t* dst, const uint16_t* src) +{ + while (*src) { + char16_t s = htods(*src); + *dst++ = s; + src++; + } + *dst = 0; +} + +void printStringPool(const ResStringPool* pool) +{ + const size_t NS = pool->size(); + for (size_t s=0; s<NS; s++) { + size_t len; + printf("String #%d: %s\n", s, + String8(pool->stringAt(s, &len)).string()); + } +} + +StringPool::StringPool(bool sorted) + : mSorted(sorted), mValues(-1), mIdents(-1) +{ +} + +ssize_t StringPool::add(const String16& value, bool mergeDuplicates) +{ + return add(String16(), value, mergeDuplicates); +} + +ssize_t StringPool::add(const String16& value, const Vector<entry_style_span>& spans) +{ + ssize_t res = add(String16(), value, false); + if (res >= 0) { + addStyleSpans(res, spans); + } + return res; +} + +ssize_t StringPool::add(const String16& ident, const String16& value, + bool mergeDuplicates) +{ + if (ident.size() > 0) { + ssize_t idx = mIdents.valueFor(ident); + if (idx >= 0) { + fprintf(stderr, "ERROR: Duplicate string identifier %s\n", + String8(mEntries[idx].value).string()); + return UNKNOWN_ERROR; + } + } + + ssize_t vidx = mValues.indexOfKey(value); + ssize_t pos = vidx >= 0 ? mValues.valueAt(vidx) : -1; + ssize_t eidx = pos >= 0 ? mEntryArray.itemAt(pos) : -1; + if (eidx < 0) { + eidx = mEntries.add(entry(value)); + if (eidx < 0) { + fprintf(stderr, "Failure adding string %s\n", String8(value).string()); + return eidx; + } + } + + const bool first = vidx < 0; + if (first || !mergeDuplicates) { + pos = mEntryArray.add(eidx); + if (first) { + vidx = mValues.add(value, pos); + const size_t N = mEntryArrayToValues.size(); + for (size_t i=0; i<N; i++) { + size_t& e = mEntryArrayToValues.editItemAt(i); + if ((ssize_t)e >= vidx) { + e++; + } + } + } + mEntryArrayToValues.add(vidx); + if (!mSorted) { + entry& ent = mEntries.editItemAt(eidx); + ent.indices.add(pos); + } + } + + if (ident.size() > 0) { + mIdents.add(ident, vidx); + } + + NOISY(printf("Adding string %s to pool: pos=%d eidx=%d vidx=%d\n", + String8(value).string(), pos, eidx, vidx)); + + return pos; +} + +status_t StringPool::addStyleSpan(size_t idx, const String16& name, + uint32_t start, uint32_t end) +{ + entry_style_span span; + span.name = name; + span.span.firstChar = start; + span.span.lastChar = end; + return addStyleSpan(idx, span); +} + +status_t StringPool::addStyleSpans(size_t idx, const Vector<entry_style_span>& spans) +{ + const size_t N=spans.size(); + for (size_t i=0; i<N; i++) { + status_t err = addStyleSpan(idx, spans[i]); + if (err != NO_ERROR) { + return err; + } + } + return NO_ERROR; +} + +status_t StringPool::addStyleSpan(size_t idx, const entry_style_span& span) +{ + LOG_ALWAYS_FATAL_IF(mSorted, "Can't use styles with sorted string pools."); + + // Place blank entries in the span array up to this index. + while (mEntryStyleArray.size() <= idx) { + mEntryStyleArray.add(); + } + + entry_style& style = mEntryStyleArray.editItemAt(idx); + style.spans.add(span); + return NO_ERROR; +} + +size_t StringPool::size() const +{ + return mSorted ? mValues.size() : mEntryArray.size(); +} + +const StringPool::entry& StringPool::entryAt(size_t idx) const +{ + if (!mSorted) { + return mEntries[mEntryArray[idx]]; + } else { + return mEntries[mEntryArray[mValues.valueAt(idx)]]; + } +} + +size_t StringPool::countIdentifiers() const +{ + return mIdents.size(); +} + +sp<AaptFile> StringPool::createStringBlock() +{ + sp<AaptFile> pool = new AaptFile(String8(), AaptGroupEntry(), + String8()); + status_t err = writeStringBlock(pool); + return err == NO_ERROR ? pool : NULL; +} + +status_t StringPool::writeStringBlock(const sp<AaptFile>& pool) +{ + // Allow appending. Sorry this is a little wacky. + if (pool->getSize() > 0) { + sp<AaptFile> block = createStringBlock(); + if (block == NULL) { + return UNKNOWN_ERROR; + } + ssize_t res = pool->writeData(block->getData(), block->getSize()); + return (res >= 0) ? (status_t)NO_ERROR : res; + } + + // First we need to add all style span names to the string pool. + // We do this now (instead of when the span is added) so that these + // will appear at the end of the pool, not disrupting the order + // our client placed their own strings in it. + + const size_t STYLES = mEntryStyleArray.size(); + size_t i; + + for (i=0; i<STYLES; i++) { + entry_style& style = mEntryStyleArray.editItemAt(i); + const size_t N = style.spans.size(); + for (size_t i=0; i<N; i++) { + entry_style_span& span = style.spans.editItemAt(i); + ssize_t idx = add(span.name, true); + if (idx < 0) { + fprintf(stderr, "Error adding span for style tag '%s'\n", + String8(span.name).string()); + return idx; + } + span.span.name.index = (uint32_t)idx; + } + } + + const size_t ENTRIES = size(); + + // Now build the pool of unique strings. + + const size_t STRINGS = mEntries.size(); + const size_t preSize = sizeof(ResStringPool_header) + + (sizeof(uint32_t)*ENTRIES) + + (sizeof(uint32_t)*STYLES); + if (pool->editData(preSize) == NULL) { + fprintf(stderr, "ERROR: Out of memory for string pool\n"); + return NO_MEMORY; + } + + size_t strPos = 0; + for (i=0; i<STRINGS; i++) { + entry& ent = mEntries.editItemAt(i); + const size_t strSize = (ent.value.size()); + const size_t lenSize = strSize > 0x7fff ? sizeof(uint32_t) : sizeof(uint16_t); + const size_t totalSize = lenSize + ((strSize+1)*sizeof(uint16_t)); + + ent.offset = strPos; + uint16_t* dat = (uint16_t*)pool->editData(preSize + strPos + totalSize); + if (dat == NULL) { + fprintf(stderr, "ERROR: Out of memory for string pool\n"); + return NO_MEMORY; + } + dat += (preSize+strPos)/sizeof(uint16_t); + if (lenSize > sizeof(uint16_t)) { + *dat = htods(0x8000 | ((strSize>>16)&0x7ffff)); + dat++; + } + *dat++ = htods(strSize); + strcpy16_htod(dat, ent.value); + + strPos += lenSize + (strSize+1)*sizeof(uint16_t); + } + + // Pad ending string position up to a uint32_t boundary. + + if (strPos&0x3) { + size_t padPos = ((strPos+3)&~0x3); + uint8_t* dat = (uint8_t*)pool->editData(preSize + padPos); + if (dat == NULL) { + fprintf(stderr, "ERROR: Out of memory padding string pool\n"); + return NO_MEMORY; + } + memset(dat+preSize+strPos, 0, padPos-strPos); + strPos = padPos; + } + + // Build the pool of style spans. + + size_t styPos = strPos; + for (i=0; i<STYLES; i++) { + entry_style& ent = mEntryStyleArray.editItemAt(i); + const size_t N = ent.spans.size(); + const size_t totalSize = (N*sizeof(ResStringPool_span)) + + sizeof(ResStringPool_ref); + + ent.offset = styPos-strPos; + uint8_t* dat = (uint8_t*)pool->editData(preSize + styPos + totalSize); + if (dat == NULL) { + fprintf(stderr, "ERROR: Out of memory for string styles\n"); + return NO_MEMORY; + } + ResStringPool_span* span = (ResStringPool_span*)(dat+preSize+styPos); + for (size_t i=0; i<N; i++) { + span->name.index = htodl(ent.spans[i].span.name.index); + span->firstChar = htodl(ent.spans[i].span.firstChar); + span->lastChar = htodl(ent.spans[i].span.lastChar); + span++; + } + span->name.index = htodl(ResStringPool_span::END); + + styPos += totalSize; + } + + if (STYLES > 0) { + // Add full terminator at the end (when reading we validate that + // the end of the pool is fully terminated to simplify error + // checking). + size_t extra = sizeof(ResStringPool_span)-sizeof(ResStringPool_ref); + uint8_t* dat = (uint8_t*)pool->editData(preSize + styPos + extra); + if (dat == NULL) { + fprintf(stderr, "ERROR: Out of memory for string styles\n"); + return NO_MEMORY; + } + uint32_t* p = (uint32_t*)(dat+preSize+styPos); + while (extra > 0) { + *p++ = htodl(ResStringPool_span::END); + extra -= sizeof(uint32_t); + } + styPos += extra; + } + + // Write header. + + ResStringPool_header* header = + (ResStringPool_header*)pool->padData(sizeof(uint32_t)); + if (header == NULL) { + fprintf(stderr, "ERROR: Out of memory for string pool\n"); + return NO_MEMORY; + } + memset(header, 0, sizeof(*header)); + header->header.type = htods(RES_STRING_POOL_TYPE); + header->header.headerSize = htods(sizeof(*header)); + header->header.size = htodl(pool->getSize()); + header->stringCount = htodl(ENTRIES); + header->styleCount = htodl(STYLES); + if (mSorted) { + header->flags |= htodl(ResStringPool_header::SORTED_FLAG); + } + header->stringsStart = htodl(preSize); + header->stylesStart = htodl(STYLES > 0 ? (preSize+strPos) : 0); + + // Write string index array. + + uint32_t* index = (uint32_t*)(header+1); + if (mSorted) { + for (i=0; i<ENTRIES; i++) { + entry& ent = const_cast<entry&>(entryAt(i)); + ent.indices.clear(); + ent.indices.add(i); + *index++ = htodl(ent.offset); + } + } else { + for (i=0; i<ENTRIES; i++) { + entry& ent = mEntries.editItemAt(mEntryArray[i]); + *index++ = htodl(ent.offset); + NOISY(printf("Writing entry #%d: \"%s\" ent=%d off=%d\n", i, + String8(ent.value).string(), + mEntryArray[i], ent.offset)); + } + } + + // Write style index array. + + if (mSorted) { + for (i=0; i<STYLES; i++) { + LOG_ALWAYS_FATAL("Shouldn't be here!"); + } + } else { + for (i=0; i<STYLES; i++) { + *index++ = htodl(mEntryStyleArray[i].offset); + } + } + + return NO_ERROR; +} + +ssize_t StringPool::offsetForString(const String16& val) const +{ + const Vector<size_t>* indices = offsetsForString(val); + ssize_t res = indices != NULL && indices->size() > 0 ? indices->itemAt(0) : -1; + NOISY(printf("Offset for string %s: %d (%s)\n", String8(val).string(), res, + res >= 0 ? String8(mEntries[mEntryArray[res]].value).string() : String8())); + return res; +} + +const Vector<size_t>* StringPool::offsetsForString(const String16& val) const +{ + ssize_t pos = mValues.valueFor(val); + if (pos < 0) { + return NULL; + } + return &mEntries[mEntryArray[pos]].indices; +} diff --git a/tools/aapt/StringPool.h b/tools/aapt/StringPool.h new file mode 100644 index 0000000..9082b37 --- /dev/null +++ b/tools/aapt/StringPool.h @@ -0,0 +1,148 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#ifndef STRING_POOL_H +#define STRING_POOL_H + +#include "Main.h" +#include "AaptAssets.h" + +#include <utils/ResourceTypes.h> +#include <utils/String16.h> +#include <utils/TextOutput.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <ctype.h> +#include <errno.h> + +#include <expat.h> + +using namespace android; + +#define PRINT_STRING_METRICS 0 + +void strcpy16_htod(uint16_t* dst, const uint16_t* src); + +void printStringPool(const ResStringPool* pool); + +/** + * The StringPool class is used as an intermediate representation for + * generating the string pool resource data structure that can be parsed with + * ResStringPool in include/utils/ResourceTypes.h. + */ +class StringPool +{ +public: + struct entry { + entry() : offset(0) { } + entry(const String16& _value) : value(_value), offset(0) { } + entry(const entry& o) : value(o.value), offset(o.offset), indices(o.indices) { } + + String16 value; + size_t offset; + Vector<size_t> indices; + }; + + struct entry_style_span { + String16 name; + ResStringPool_span span; + }; + + struct entry_style { + entry_style() : offset(0) { } + + entry_style(const entry_style& o) : offset(o.offset), spans(o.spans) { } + + size_t offset; + Vector<entry_style_span> spans; + }; + + /** + * If 'sorted' is true, then the final strings in the resource data + * structure will be generated in sorted order. This allow for fast + * lookup with ResStringPool::indexOfString() (O(log n)), at the expense + * of support for styled string entries (which requires the same string + * be included multiple times in the pool). + */ + explicit StringPool(bool sorted = false); + + /** + * Add a new string to the pool. If mergeDuplicates is true, thenif + * the string already exists the existing entry for it will be used; + * otherwise, or if the value doesn't already exist, a new entry is + * created. + * + * Returns the index in the entry array of the new string entry. Note that + * if this string pool is sorted, the returned index will not be valid + * when the pool is finally written. + */ + ssize_t add(const String16& value, bool mergeDuplicates = false); + + ssize_t add(const String16& value, const Vector<entry_style_span>& spans); + + ssize_t add(const String16& ident, const String16& value, + bool mergeDuplicates = false); + + status_t addStyleSpan(size_t idx, const String16& name, + uint32_t start, uint32_t end); + status_t addStyleSpans(size_t idx, const Vector<entry_style_span>& spans); + status_t addStyleSpan(size_t idx, const entry_style_span& span); + + size_t size() const; + + const entry& entryAt(size_t idx) const; + + size_t countIdentifiers() const; + + sp<AaptFile> createStringBlock(); + + status_t writeStringBlock(const sp<AaptFile>& pool); + + /** + * Find out an offset in the pool for a particular string. If the string + * pool is sorted, this can not be called until after createStringBlock() + * or writeStringBlock() has been called + * (which determines the offsets). In the case of a string that appears + * multiple times in the pool, the first offset will be returned. Returns + * -1 if the string does not exist. + */ + ssize_t offsetForString(const String16& val) const; + + /** + * Find all of the offsets in the pool for a particular string. If the + * string pool is sorted, this can not be called until after + * createStringBlock() or writeStringBlock() has been called + * (which determines the offsets). Returns NULL if the string does not exist. + */ + const Vector<size_t>* offsetsForString(const String16& val) const; + +private: + const bool mSorted; + // Raw array of unique strings, in some arbitrary order. + Vector<entry> mEntries; + // Array of indices into mEntries, in the order they were + // added to the pool. This can be different than mEntries + // if the same string was added multiple times (it will appear + // once in mEntries, with multiple occurrences in this array). + Vector<size_t> mEntryArray; + // Optional style span information associated with each index of + // mEntryArray. + Vector<entry_style> mEntryStyleArray; + // Mapping from indices in mEntryArray to indices in mValues. + Vector<size_t> mEntryArrayToValues; + // Unique set of all the strings added to the pool, mapped to + // the first index of mEntryArray where the value was added. + DefaultKeyedVector<String16, ssize_t> mValues; + // Unique set of all (optional) identifiers of strings in the + // pool, mapping to indices in mEntries. + DefaultKeyedVector<String16, ssize_t> mIdents; + +}; + +#endif + diff --git a/tools/aapt/XMLNode.cpp b/tools/aapt/XMLNode.cpp new file mode 100644 index 0000000..d476567 --- /dev/null +++ b/tools/aapt/XMLNode.cpp @@ -0,0 +1,1295 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#include "XMLNode.h" +#include "ResourceTable.h" + +#include <host/pseudolocalize.h> +#include <utils/ByteOrder.h> +#include <errno.h> +#include <string.h> + +#ifndef HAVE_MS_C_RUNTIME +#define O_BINARY 0 +#endif + +#define NOISY(x) //x +#define NOISY_PARSE(x) //x + +const char* const RESOURCES_ROOT_NAMESPACE = "http://schemas.android.com/apk/res/"; +const char* const RESOURCES_ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android"; +const char* const RESOURCES_ROOT_PRV_NAMESPACE = "http://schemas.android.com/apk/prv/res/"; + +const char* const XLIFF_XMLNS = "urn:oasis:names:tc:xliff:document:1.2"; +const char* const ALLOWED_XLIFF_ELEMENTS[] = { + "bpt", + "ept", + "it", + "ph", + "g", + "bx", + "ex", + "x" + }; + +bool isWhitespace(const char16_t* str) +{ + while (*str != 0 && *str < 128 && isspace(*str)) { + str++; + } + return *str == 0; +} + +static const String16 RESOURCES_PREFIX(RESOURCES_ROOT_NAMESPACE); +static const String16 RESOURCES_PRV_PREFIX(RESOURCES_ROOT_PRV_NAMESPACE); + +String16 getNamespaceResourcePackage(String16 namespaceUri, bool* outIsPublic) +{ + //printf("%s starts with %s?\n", String8(namespaceUri).string(), + // String8(RESOURCES_PREFIX).string()); + size_t prefixSize; + bool isPublic = true; + if (namespaceUri.startsWith(RESOURCES_PREFIX)) { + prefixSize = RESOURCES_PREFIX.size(); + } else if (namespaceUri.startsWith(RESOURCES_PRV_PREFIX)) { + isPublic = false; + prefixSize = RESOURCES_PRV_PREFIX.size(); + } else { + if (outIsPublic) *outIsPublic = isPublic; // = true + return String16(); + } + + //printf("YES!\n"); + //printf("namespace: %s\n", String8(String16(namespaceUri, namespaceUri.size()-prefixSize, prefixSize)).string()); + if (outIsPublic) *outIsPublic = isPublic; + return String16(namespaceUri, namespaceUri.size()-prefixSize, prefixSize); +} + +status_t parseStyledString(Bundle* bundle, + const char* fileName, + ResXMLTree* inXml, + const String16& endTag, + String16* outString, + Vector<StringPool::entry_style_span>* outSpans, + bool pseudolocalize) +{ + Vector<StringPool::entry_style_span> spanStack; + String16 curString; + String16 rawString; + const char* errorMsg; + int xliffDepth = 0; + bool firstTime = true; + + size_t len; + ResXMLTree::event_code_t code; + while ((code=inXml->next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + + if (code == ResXMLTree::TEXT) { + String16 text(inXml->getText(&len)); + if (firstTime && text.size() > 0) { + firstTime = false; + if (text.string()[0] == '@') { + // If this is a resource reference, don't do the pseudoloc. + pseudolocalize = false; + } + } + if (xliffDepth == 0 && pseudolocalize) { + std::string orig(String8(text).string()); + std::string pseudo = pseudolocalize_string(orig); + curString.append(String16(String8(pseudo.c_str()))); + } else { + curString.append(text); + } + } else if (code == ResXMLTree::START_TAG) { + const String16 element16(inXml->getElementName(&len)); + const String8 element8(element16); + + size_t nslen; + const uint16_t* ns = inXml->getElementNamespace(&nslen); + if (ns == NULL) { + ns = (const uint16_t*)"\0\0"; + nslen = 0; + } + const String8 nspace(String16(ns, nslen)); + if (nspace == XLIFF_XMLNS) { + const int N = sizeof(ALLOWED_XLIFF_ELEMENTS)/sizeof(ALLOWED_XLIFF_ELEMENTS[0]); + for (int i=0; i<N; i++) { + if (element8 == ALLOWED_XLIFF_ELEMENTS[i]) { + xliffDepth++; + // in this case, treat it like it was just text, in other words, do nothing + // here and silently drop this element + goto moveon; + } + } + { + SourcePos(String8(fileName), inXml->getLineNumber()).error( + "Found unsupported XLIFF tag <%s>\n", + element8.string()); + return UNKNOWN_ERROR; + } +moveon: + continue; + } + + if (outSpans == NULL) { + SourcePos(String8(fileName), inXml->getLineNumber()).error( + "Found style tag <%s> where styles are not allowed\n", element8.string()); + return UNKNOWN_ERROR; + } + + if (!ResTable::collectString(outString, curString.string(), + curString.size(), false, &errorMsg, true)) { + SourcePos(String8(fileName), inXml->getLineNumber()).error("%s (in %s)\n", + errorMsg, String8(curString).string()); + return UNKNOWN_ERROR; + } + rawString.append(curString); + curString = String16(); + + StringPool::entry_style_span span; + span.name = element16; + for (size_t ai=0; ai<inXml->getAttributeCount(); ai++) { + span.name.append(String16(";")); + const char16_t* str = inXml->getAttributeName(ai, &len); + span.name.append(str, len); + span.name.append(String16("=")); + str = inXml->getAttributeStringValue(ai, &len); + span.name.append(str, len); + } + //printf("Span: %s\n", String8(span.name).string()); + span.span.firstChar = span.span.lastChar = outString->size(); + spanStack.push(span); + + } else if (code == ResXMLTree::END_TAG) { + size_t nslen; + const uint16_t* ns = inXml->getElementNamespace(&nslen); + if (ns == NULL) { + ns = (const uint16_t*)"\0\0"; + nslen = 0; + } + const String8 nspace(String16(ns, nslen)); + if (nspace == XLIFF_XMLNS) { + xliffDepth--; + continue; + } + if (!ResTable::collectString(outString, curString.string(), + curString.size(), false, &errorMsg, true)) { + SourcePos(String8(fileName), inXml->getLineNumber()).error("%s (in %s)\n", + errorMsg, String8(curString).string()); + return UNKNOWN_ERROR; + } + rawString.append(curString); + curString = String16(); + + if (spanStack.size() == 0) { + if (strcmp16(inXml->getElementName(&len), endTag.string()) != 0) { + SourcePos(String8(fileName), inXml->getLineNumber()).error( + "Found tag %s where <%s> close is expected\n", + String8(inXml->getElementName(&len)).string(), + String8(endTag).string()); + return UNKNOWN_ERROR; + } + break; + } + StringPool::entry_style_span span = spanStack.top(); + String16 spanTag; + ssize_t semi = span.name.findFirst(';'); + if (semi >= 0) { + spanTag.setTo(span.name.string(), semi); + } else { + spanTag.setTo(span.name); + } + if (strcmp16(inXml->getElementName(&len), spanTag.string()) != 0) { + SourcePos(String8(fileName), inXml->getLineNumber()).error( + "Found close tag %s where close tag %s is expected\n", + String8(inXml->getElementName(&len)).string(), + String8(spanTag).string()); + return UNKNOWN_ERROR; + } + bool empty = true; + if (outString->size() > 0) { + span.span.lastChar = outString->size()-1; + if (span.span.lastChar >= span.span.firstChar) { + empty = false; + outSpans->add(span); + } + } + spanStack.pop(); + + if (empty) { + fprintf(stderr, "%s:%d: WARNING: empty '%s' span found in text '%s'\n", + fileName, inXml->getLineNumber(), + String8(spanTag).string(), String8(*outString).string()); + + } + } else if (code == ResXMLTree::START_NAMESPACE) { + // nothing + } + } + + if (code == ResXMLTree::BAD_DOCUMENT) { + SourcePos(String8(fileName), inXml->getLineNumber()).error( + "Error parsing XML\n"); + } + + if (outSpans != NULL && outSpans->size() > 0) { + if (curString.size() > 0) { + if (!ResTable::collectString(outString, curString.string(), + curString.size(), false, &errorMsg, true)) { + SourcePos(String8(fileName), inXml->getLineNumber()).error( + "%s (in %s)\n", + errorMsg, String8(curString).string()); + return UNKNOWN_ERROR; + } + } + } else { + // There is no style information, so string processing will happen + // later as part of the overall type conversion. Return to the + // client the raw unprocessed text. + rawString.append(curString); + outString->setTo(rawString); + } + + return NO_ERROR; +} + +struct namespace_entry { + String8 prefix; + String8 uri; +}; + +static String8 make_prefix(int depth) +{ + String8 prefix; + int i; + for (i=0; i<depth; i++) { + prefix.append(" "); + } + return prefix; +} + +static String8 build_namespace(const Vector<namespace_entry>& namespaces, + const uint16_t* ns) +{ + String8 str; + if (ns != NULL) { + str = String8(ns); + const size_t N = namespaces.size(); + for (size_t i=0; i<N; i++) { + const namespace_entry& ne = namespaces.itemAt(i); + if (ne.uri == str) { + str = ne.prefix; + break; + } + } + str.append(":"); + } + return str; +} + +void printXMLBlock(ResXMLTree* block) +{ + block->restart(); + + Vector<namespace_entry> namespaces; + + ResXMLTree::event_code_t code; + int depth = 0; + while ((code=block->next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + String8 prefix = make_prefix(depth); + int i; + if (code == ResXMLTree::START_TAG) { + size_t len; + const uint16_t* ns16 = block->getElementNamespace(&len); + String8 elemNs = build_namespace(namespaces, ns16); + const uint16_t* com16 = block->getComment(&len); + if (com16) { + printf("%s <!-- %s -->\n", prefix.string(), String8(com16).string()); + } + printf("%sE: %s%s (line=%d)\n", prefix.string(), elemNs.string(), + String8(block->getElementName(&len)).string(), + block->getLineNumber()); + int N = block->getAttributeCount(); + depth++; + prefix = make_prefix(depth); + for (i=0; i<N; i++) { + uint32_t res = block->getAttributeNameResID(i); + ns16 = block->getAttributeNamespace(i, &len); + String8 ns = build_namespace(namespaces, ns16); + String8 name(block->getAttributeName(i, &len)); + printf("%sA: ", prefix.string()); + if (res) { + printf("%s%s(0x%08x)", ns.string(), name.string(), res); + } else { + printf("%s%s", ns.string(), name.string()); + } + Res_value value; + block->getAttributeValue(i, &value); + if (value.dataType == Res_value::TYPE_NULL) { + printf("=(null)"); + } else if (value.dataType == Res_value::TYPE_REFERENCE) { + printf("=@0x%x", (int)value.data); + } else if (value.dataType == Res_value::TYPE_ATTRIBUTE) { + printf("=?0x%x", (int)value.data); + } else if (value.dataType == Res_value::TYPE_STRING) { + printf("=\"%s\"", + String8(block->getAttributeStringValue(i, &len)).string()); + } else { + printf("=(type 0x%x)0x%x", (int)value.dataType, (int)value.data); + } + const char16_t* val = block->getAttributeStringValue(i, &len); + if (val != NULL) { + printf(" (Raw: \"%s\")", String8(val).string()); + } + printf("\n"); + } + } else if (code == ResXMLTree::END_TAG) { + depth--; + } else if (code == ResXMLTree::START_NAMESPACE) { + namespace_entry ns; + size_t len; + const uint16_t* prefix16 = block->getNamespacePrefix(&len); + if (prefix16) { + ns.prefix = String8(prefix16); + } else { + ns.prefix = "<DEF>"; + } + ns.uri = String8(block->getNamespaceUri(&len)); + namespaces.push(ns); + printf("%sN: %s=%s\n", prefix.string(), ns.prefix.string(), + ns.uri.string()); + depth++; + } else if (code == ResXMLTree::END_NAMESPACE) { + depth--; + const namespace_entry& ns = namespaces.top(); + size_t len; + const uint16_t* prefix16 = block->getNamespacePrefix(&len); + String8 pr; + if (prefix16) { + pr = String8(prefix16); + } else { + pr = "<DEF>"; + } + if (ns.prefix != pr) { + prefix = make_prefix(depth); + printf("%s*** BAD END NS PREFIX: found=%s, expected=%s\n", + prefix.string(), pr.string(), ns.prefix.string()); + } + String8 uri = String8(block->getNamespaceUri(&len)); + if (ns.uri != uri) { + prefix = make_prefix(depth); + printf("%s *** BAD END NS URI: found=%s, expected=%s\n", + prefix.string(), uri.string(), ns.uri.string()); + } + namespaces.pop(); + } else if (code == ResXMLTree::TEXT) { + size_t len; + printf("%sC: \"%s\"\n", prefix.string(), String8(block->getText(&len)).string()); + } + } + + block->restart(); +} + +status_t parseXMLResource(const sp<AaptFile>& file, ResXMLTree* outTree, + bool stripAll, bool keepComments, + const char** cDataTags) +{ + sp<XMLNode> root = XMLNode::parse(file); + if (root == NULL) { + return UNKNOWN_ERROR; + } + root->removeWhitespace(stripAll, cDataTags); + + NOISY(printf("Input XML from %s:\n", (const char*)file->getPrintableSource())); + NOISY(root->print()); + sp<AaptFile> rsc = new AaptFile(String8(), AaptGroupEntry(), String8()); + status_t err = root->flatten(rsc, !keepComments, false); + if (err != NO_ERROR) { + return err; + } + err = outTree->setTo(rsc->getData(), rsc->getSize(), true); + if (err != NO_ERROR) { + return err; + } + + NOISY(printf("Output XML:\n")); + NOISY(printXMLBlock(outTree)); + + return NO_ERROR; +} + +sp<XMLNode> XMLNode::parse(const sp<AaptFile>& file) +{ + char buf[16384]; + int fd = open(file->getSourceFile().string(), O_RDONLY | O_BINARY); + if (fd < 0) { + SourcePos(file->getSourceFile(), -1).error("Unable to open file for read: %s", + strerror(errno)); + return NULL; + } + + XML_Parser parser = XML_ParserCreateNS(NULL, 1); + ParseState state; + state.filename = file->getPrintableSource(); + state.parser = parser; + XML_SetUserData(parser, &state); + XML_SetElementHandler(parser, startElement, endElement); + XML_SetNamespaceDeclHandler(parser, startNamespace, endNamespace); + XML_SetCharacterDataHandler(parser, characterData); + XML_SetCommentHandler(parser, commentData); + + ssize_t len; + bool done; + do { + len = read(fd, buf, sizeof(buf)); + done = len < (ssize_t)sizeof(buf); + if (len < 0) { + SourcePos(file->getSourceFile(), -1).error("Error reading file: %s\n", strerror(errno)); + close(fd); + return NULL; + } + if (XML_Parse(parser, buf, len, done) == XML_STATUS_ERROR) { + SourcePos(file->getSourceFile(), (int)XML_GetCurrentLineNumber(parser)).error( + "Error parsing XML: %s\n", XML_ErrorString(XML_GetErrorCode(parser))); + close(fd); + return NULL; + } + } while (!done); + + XML_ParserFree(parser); + if (state.root == NULL) { + SourcePos(file->getSourceFile(), -1).error("No XML data generated when parsing"); + } + close(fd); + return state.root; +} + +XMLNode::XMLNode(const String8& filename, const String16& s1, const String16& s2, bool isNamespace) + : mNextAttributeIndex(0x80000000) + , mFilename(filename) + , mStartLineNumber(0) + , mEndLineNumber(0) +{ + if (isNamespace) { + mNamespacePrefix = s1; + mNamespaceUri = s2; + } else { + mNamespaceUri = s1; + mElementName = s2; + } +} + +XMLNode::XMLNode(const String8& filename) + : mFilename(filename) +{ +} + +XMLNode::type XMLNode::getType() const +{ + if (mElementName.size() != 0) { + return TYPE_ELEMENT; + } + if (mNamespaceUri.size() != 0) { + return TYPE_NAMESPACE; + } + return TYPE_CDATA; +} + +const String16& XMLNode::getNamespacePrefix() const +{ + return mNamespacePrefix; +} + +const String16& XMLNode::getNamespaceUri() const +{ + return mNamespaceUri; +} + +const String16& XMLNode::getElementNamespace() const +{ + return mNamespaceUri; +} + +const String16& XMLNode::getElementName() const +{ + return mElementName; +} + +const Vector<sp<XMLNode> >& XMLNode::getChildren() const +{ + return mChildren; +} + +const Vector<XMLNode::attribute_entry>& + XMLNode::getAttributes() const +{ + return mAttributes; +} + +const String16& XMLNode::getCData() const +{ + return mChars; +} + +const String16& XMLNode::getComment() const +{ + return mComment; +} + +int32_t XMLNode::getStartLineNumber() const +{ + return mStartLineNumber; +} + +int32_t XMLNode::getEndLineNumber() const +{ + return mEndLineNumber; +} + +status_t XMLNode::addChild(const sp<XMLNode>& child) +{ + if (getType() == TYPE_CDATA) { + SourcePos(mFilename, child->getStartLineNumber()).error("Child to CDATA node."); + return UNKNOWN_ERROR; + } + //printf("Adding child %p to parent %p\n", child.get(), this); + mChildren.add(child); + return NO_ERROR; +} + +status_t XMLNode::addAttribute(const String16& ns, const String16& name, + const String16& value) +{ + if (getType() == TYPE_CDATA) { + SourcePos(mFilename, getStartLineNumber()).error("Child to CDATA node."); + return UNKNOWN_ERROR; + } + attribute_entry e; + e.index = mNextAttributeIndex++; + e.ns = ns; + e.name = name; + e.string = value; + mAttributes.add(e); + mAttributeOrder.add(e.index, mAttributes.size()-1); + return NO_ERROR; +} + +void XMLNode::setAttributeResID(size_t attrIdx, uint32_t resId) +{ + attribute_entry& e = mAttributes.editItemAt(attrIdx); + if (e.nameResId) { + mAttributeOrder.removeItem(e.nameResId); + } else { + mAttributeOrder.removeItem(e.index); + } + NOISY(printf("Elem %s %s=\"%s\": set res id = 0x%08x\n", + String8(getElementName()).string(), + String8(mAttributes.itemAt(attrIdx).name).string(), + String8(mAttributes.itemAt(attrIdx).string).string(), + resId)); + mAttributes.editItemAt(attrIdx).nameResId = resId; + mAttributeOrder.add(resId, attrIdx); +} + +status_t XMLNode::appendChars(const String16& chars) +{ + if (getType() != TYPE_CDATA) { + SourcePos(mFilename, getStartLineNumber()).error("Adding characters to element node."); + return UNKNOWN_ERROR; + } + mChars.append(chars); + return NO_ERROR; +} + +status_t XMLNode::appendComment(const String16& comment) +{ + if (mComment.size() > 0) { + mComment.append(String16("\n")); + } + mComment.append(comment); + return NO_ERROR; +} + +void XMLNode::setStartLineNumber(int32_t line) +{ + mStartLineNumber = line; +} + +void XMLNode::setEndLineNumber(int32_t line) +{ + mEndLineNumber = line; +} + +void XMLNode::removeWhitespace(bool stripAll, const char** cDataTags) +{ + //printf("Removing whitespace in %s\n", String8(mElementName).string()); + size_t N = mChildren.size(); + if (cDataTags) { + String8 tag(mElementName); + const char** p = cDataTags; + while (*p) { + if (tag == *p) { + stripAll = false; + break; + } + } + } + for (size_t i=0; i<N; i++) { + sp<XMLNode> node = mChildren.itemAt(i); + if (node->getType() == TYPE_CDATA) { + // This is a CDATA node... + const char16_t* p = node->mChars.string(); + while (*p != 0 && *p < 128 && isspace(*p)) { + p++; + } + //printf("Space ends at %d in \"%s\"\n", + // (int)(p-node->mChars.string()), + // String8(node->mChars).string()); + if (*p == 0) { + if (stripAll) { + // Remove this node! + mChildren.removeAt(i); + N--; + i--; + } else { + node->mChars = String16(" "); + } + } else { + // Compact leading/trailing whitespace. + const char16_t* e = node->mChars.string()+node->mChars.size()-1; + while (e > p && *e < 128 && isspace(*e)) { + e--; + } + if (p > node->mChars.string()) { + p--; + } + if (e < (node->mChars.string()+node->mChars.size()-1)) { + e++; + } + if (p > node->mChars.string() || + e < (node->mChars.string()+node->mChars.size()-1)) { + String16 tmp(p, e-p+1); + node->mChars = tmp; + } + } + } else { + node->removeWhitespace(stripAll, cDataTags); + } + } +} + +status_t XMLNode::parseValues(const sp<AaptAssets>& assets, + ResourceTable* table) +{ + bool hasErrors = false; + + if (getType() == TYPE_ELEMENT) { + const size_t N = mAttributes.size(); + String16 defPackage(assets->getPackage()); + for (size_t i=0; i<N; i++) { + attribute_entry& e = mAttributes.editItemAt(i); + AccessorCookie ac(SourcePos(mFilename, getStartLineNumber()), String8(e.name), + String8(e.string)); + table->setCurrentXmlPos(SourcePos(mFilename, getStartLineNumber())); + if (!assets->getIncludedResources() + .stringToValue(&e.value, &e.string, + e.string.string(), e.string.size(), true, true, + e.nameResId, NULL, &defPackage, table, &ac)) { + hasErrors = true; + } + NOISY(printf("Attr %s: type=0x%x, str=%s\n", + String8(e.name).string(), e.value.dataType, + String8(e.string).string())); + } + } + const size_t N = mChildren.size(); + for (size_t i=0; i<N; i++) { + status_t err = mChildren.itemAt(i)->parseValues(assets, table); + if (err != NO_ERROR) { + hasErrors = true; + } + } + return hasErrors ? UNKNOWN_ERROR : NO_ERROR; +} + +status_t XMLNode::assignResourceIds(const sp<AaptAssets>& assets, + const ResourceTable* table) +{ + bool hasErrors = false; + + if (getType() == TYPE_ELEMENT) { + String16 attr("attr"); + const char* errorMsg; + const size_t N = mAttributes.size(); + for (size_t i=0; i<N; i++) { + const attribute_entry& e = mAttributes.itemAt(i); + if (e.ns.size() <= 0) continue; + bool nsIsPublic; + String16 pkg(getNamespaceResourcePackage(e.ns, &nsIsPublic)); + NOISY(printf("Elem %s %s=\"%s\": namespace(%s) %s ===> %s\n", + String8(getElementName()).string(), + String8(e.name).string(), + String8(e.string).string(), + String8(e.ns).string(), + (nsIsPublic) ? "public" : "private", + String8(pkg).string())); + if (pkg.size() <= 0) continue; + uint32_t res = table != NULL + ? table->getResId(e.name, &attr, &pkg, &errorMsg, nsIsPublic) + : assets->getIncludedResources(). + identifierForName(e.name.string(), e.name.size(), + attr.string(), attr.size(), + pkg.string(), pkg.size()); + if (res != 0) { + NOISY(printf("XML attribute name %s: resid=0x%08x\n", + String8(e.name).string(), res)); + setAttributeResID(i, res); + } else { + SourcePos(mFilename, getStartLineNumber()).error( + "No resource identifier found for attribute '%s' in package '%s'\n", + String8(e.name).string(), String8(pkg).string()); + hasErrors = true; + } + } + } + const size_t N = mChildren.size(); + for (size_t i=0; i<N; i++) { + status_t err = mChildren.itemAt(i)->assignResourceIds(assets, table); + if (err < NO_ERROR) { + hasErrors = true; + } + } + + return hasErrors ? UNKNOWN_ERROR : NO_ERROR; +} + +status_t XMLNode::flatten(const sp<AaptFile>& dest, + bool stripComments, bool stripRawValues) const +{ + StringPool strings; + Vector<uint32_t> resids; + + // First collect just the strings for attribute names that have a + // resource ID assigned to them. This ensures that the resource ID + // array is compact, and makes it easier to deal with attribute names + // in different namespaces (and thus with different resource IDs). + collect_resid_strings(&strings, &resids); + + // Next collect all remainibng strings. + collect_strings(&strings, &resids, stripComments, stripRawValues); + +#if 0 // No longer compiles + NOISY(printf("Found strings:\n"); + const size_t N = strings.size(); + for (size_t i=0; i<N; i++) { + printf("%s\n", String8(strings.entryAt(i).string).string()); + } + ); +#endif + + sp<AaptFile> stringPool = strings.createStringBlock(); + NOISY(aout << "String pool:" + << HexDump(stringPool->getData(), stringPool->getSize()) << endl); + + ResXMLTree_header header; + memset(&header, 0, sizeof(header)); + header.header.type = htods(RES_XML_TYPE); + header.header.headerSize = htods(sizeof(header)); + + const size_t basePos = dest->getSize(); + dest->writeData(&header, sizeof(header)); + dest->writeData(stringPool->getData(), stringPool->getSize()); + + // If we have resource IDs, write them. + if (resids.size() > 0) { + const size_t resIdsPos = dest->getSize(); + const size_t resIdsSize = + sizeof(ResChunk_header)+(sizeof(uint32_t)*resids.size()); + ResChunk_header* idsHeader = (ResChunk_header*) + (((const uint8_t*)dest->editData(resIdsPos+resIdsSize))+resIdsPos); + idsHeader->type = htods(RES_XML_RESOURCE_MAP_TYPE); + idsHeader->headerSize = htods(sizeof(*idsHeader)); + idsHeader->size = htodl(resIdsSize); + uint32_t* ids = (uint32_t*)(idsHeader+1); + for (size_t i=0; i<resids.size(); i++) { + *ids++ = htodl(resids[i]); + } + } + + flatten_node(strings, dest, stripComments, stripRawValues); + + void* data = dest->editData(); + ResXMLTree_header* hd = (ResXMLTree_header*)(((uint8_t*)data)+basePos); + size_t size = dest->getSize()-basePos; + hd->header.size = htodl(dest->getSize()-basePos); + + NOISY(aout << "XML resource:" + << HexDump(dest->getData(), dest->getSize()) << endl); + + #if PRINT_STRING_METRICS + fprintf(stderr, "**** total xml size: %d / %d%% strings (in %s)\n", + dest->getSize(), (stringPool->getSize()*100)/dest->getSize(), + dest->getPath().string()); + #endif + + return NO_ERROR; +} + +void XMLNode::print(int indent) +{ + String8 prefix; + int i; + for (i=0; i<indent; i++) { + prefix.append(" "); + } + if (getType() == TYPE_ELEMENT) { + String8 elemNs(getNamespaceUri()); + if (elemNs.size() > 0) { + elemNs.append(":"); + } + printf("%s E: %s%s", prefix.string(), + elemNs.string(), String8(getElementName()).string()); + int N = mAttributes.size(); + for (i=0; i<N; i++) { + ssize_t idx = mAttributeOrder.valueAt(i); + if (i == 0) { + printf(" / "); + } else { + printf(", "); + } + const attribute_entry& attr = mAttributes.itemAt(idx); + String8 attrNs(attr.ns); + if (attrNs.size() > 0) { + attrNs.append(":"); + } + if (attr.nameResId) { + printf("%s%s(0x%08x)", attrNs.string(), + String8(attr.name).string(), attr.nameResId); + } else { + printf("%s%s", attrNs.string(), String8(attr.name).string()); + } + printf("=%s", String8(attr.string).string()); + } + printf("\n"); + } else if (getType() == TYPE_NAMESPACE) { + printf("%s N: %s=%s\n", prefix.string(), + getNamespacePrefix().size() > 0 + ? String8(getNamespacePrefix()).string() : "<DEF>", + String8(getNamespaceUri()).string()); + } else { + printf("%s C: \"%s\"\n", prefix.string(), String8(getCData()).string()); + } + int N = mChildren.size(); + for (i=0; i<N; i++) { + mChildren.itemAt(i)->print(indent+1); + } +} + +static void splitName(const char* name, String16* outNs, String16* outName) +{ + const char* p = name; + while (*p != 0 && *p != 1) { + p++; + } + if (*p == 0) { + *outNs = String16(); + *outName = String16(name); + } else { + *outNs = String16(name, (p-name)); + *outName = String16(p+1); + } +} + +void XMLCALL +XMLNode::startNamespace(void *userData, const char *prefix, const char *uri) +{ + NOISY_PARSE(printf("Start Namespace: %s %s\n", prefix, uri)); + ParseState* st = (ParseState*)userData; + sp<XMLNode> node = XMLNode::newNamespace(st->filename, + String16(prefix != NULL ? prefix : ""), String16(uri)); + node->setStartLineNumber(XML_GetCurrentLineNumber(st->parser)); + if (st->stack.size() > 0) { + st->stack.itemAt(st->stack.size()-1)->addChild(node); + } else { + st->root = node; + } + st->stack.push(node); +} + +void XMLCALL +XMLNode::startElement(void *userData, const char *name, const char **atts) +{ + NOISY_PARSE(printf("Start Element: %s\n", name)); + ParseState* st = (ParseState*)userData; + String16 ns16, name16; + splitName(name, &ns16, &name16); + sp<XMLNode> node = XMLNode::newElement(st->filename, ns16, name16); + node->setStartLineNumber(XML_GetCurrentLineNumber(st->parser)); + if (st->pendingComment.size() > 0) { + node->appendComment(st->pendingComment); + st->pendingComment = String16(); + } + if (st->stack.size() > 0) { + st->stack.itemAt(st->stack.size()-1)->addChild(node); + } else { + st->root = node; + } + st->stack.push(node); + + for (int i = 0; atts[i]; i += 2) { + splitName(atts[i], &ns16, &name16); + node->addAttribute(ns16, name16, String16(atts[i+1])); + } +} + +void XMLCALL +XMLNode::characterData(void *userData, const XML_Char *s, int len) +{ + NOISY_PARSE(printf("CDATA: \"%s\"\n", String8(s, len).string())); + ParseState* st = (ParseState*)userData; + sp<XMLNode> node = NULL; + if (st->stack.size() == 0) { + return; + } + sp<XMLNode> parent = st->stack.itemAt(st->stack.size()-1); + if (parent != NULL && parent->getChildren().size() > 0) { + node = parent->getChildren()[parent->getChildren().size()-1]; + if (node->getType() != TYPE_CDATA) { + // Last node is not CDATA, need to make a new node. + node = NULL; + } + } + + if (node == NULL) { + node = XMLNode::newCData(st->filename); + node->setStartLineNumber(XML_GetCurrentLineNumber(st->parser)); + parent->addChild(node); + } + + node->appendChars(String16(s, len)); +} + +void XMLCALL +XMLNode::endElement(void *userData, const char *name) +{ + NOISY_PARSE(printf("End Element: %s\n", name)); + ParseState* st = (ParseState*)userData; + sp<XMLNode> node = st->stack.itemAt(st->stack.size()-1); + node->setEndLineNumber(XML_GetCurrentLineNumber(st->parser)); + if (st->pendingComment.size() > 0) { + node->appendComment(st->pendingComment); + st->pendingComment = String16(); + } + String16 ns16, name16; + splitName(name, &ns16, &name16); + LOG_ALWAYS_FATAL_IF(node->getElementNamespace() != ns16 + || node->getElementName() != name16, + "Bad end element %s", name); + st->stack.pop(); +} + +void XMLCALL +XMLNode::endNamespace(void *userData, const char *prefix) +{ + const char* nonNullPrefix = prefix != NULL ? prefix : ""; + NOISY_PARSE(printf("End Namespace: %s\n", prefix)); + ParseState* st = (ParseState*)userData; + sp<XMLNode> node = st->stack.itemAt(st->stack.size()-1); + node->setEndLineNumber(XML_GetCurrentLineNumber(st->parser)); + LOG_ALWAYS_FATAL_IF(node->getNamespacePrefix() != String16(nonNullPrefix), + "Bad end namespace %s", prefix); + st->stack.pop(); +} + +void XMLCALL +XMLNode::commentData(void *userData, const char *comment) +{ + NOISY_PARSE(printf("Comment: %s\n", comment)); + ParseState* st = (ParseState*)userData; + if (st->pendingComment.size() > 0) { + st->pendingComment.append(String16("\n")); + } + st->pendingComment.append(String16(comment)); +} + +status_t XMLNode::collect_strings(StringPool* dest, Vector<uint32_t>* outResIds, + bool stripComments, bool stripRawValues) const +{ + collect_attr_strings(dest, outResIds, true); + + int i; + if (mNamespacePrefix.size() > 0) { + dest->add(mNamespacePrefix, true); + } + if (mNamespaceUri.size() > 0) { + dest->add(mNamespaceUri, true); + } + if (mElementName.size() > 0) { + dest->add(mElementName, true); + } + + if (!stripComments && mComment.size() > 0) { + dest->add(mComment, true); + } + + const int NA = mAttributes.size(); + + for (i=0; i<NA; i++) { + const attribute_entry& ae = mAttributes.itemAt(i); + if (ae.ns.size() > 0) { + dest->add(ae.ns, true); + } + if (!stripRawValues || ae.needStringValue()) { + dest->add(ae.string, true); + } + /* + if (ae.value.dataType == Res_value::TYPE_NULL + || ae.value.dataType == Res_value::TYPE_STRING) { + dest->add(ae.string, true); + } + */ + } + + if (mElementName.size() == 0) { + // If not an element, include the CDATA, even if it is empty. + dest->add(mChars, true); + } + + const int NC = mChildren.size(); + + for (i=0; i<NC; i++) { + mChildren.itemAt(i)->collect_strings(dest, outResIds, + stripComments, stripRawValues); + } + + return NO_ERROR; +} + +status_t XMLNode::collect_attr_strings(StringPool* outPool, + Vector<uint32_t>* outResIds, bool allAttrs) const { + const int NA = mAttributes.size(); + + for (int i=0; i<NA; i++) { + const attribute_entry& attr = mAttributes.itemAt(i); + uint32_t id = attr.nameResId; + if (id || allAttrs) { + // See if we have already assigned this resource ID to a pooled + // string... + const Vector<size_t>* indices = outPool->offsetsForString(attr.name); + ssize_t idx = -1; + if (indices != NULL) { + const int NJ = indices->size(); + const size_t NR = outResIds->size(); + for (int j=0; j<NJ; j++) { + size_t strIdx = indices->itemAt(j); + if (strIdx >= NR) { + if (id == 0) { + // We don't need to assign a resource ID for this one. + idx = strIdx; + break; + } + // Just ignore strings that are out of range of + // the currently assigned resource IDs... we add + // strings as we assign the first ID. + } else if (outResIds->itemAt(strIdx) == id) { + idx = strIdx; + break; + } + } + } + if (idx < 0) { + idx = outPool->add(attr.name); + NOISY(printf("Adding attr %s (resid 0x%08x) to pool: idx=%d\n", + String8(attr.name).string(), id, idx)); + if (id != 0) { + while ((ssize_t)outResIds->size() <= idx) { + outResIds->add(0); + } + outResIds->replaceAt(id, idx); + } + } + attr.namePoolIdx = idx; + NOISY(printf("String %s offset=0x%08x\n", + String8(attr.name).string(), idx)); + } + } + + return NO_ERROR; +} + +status_t XMLNode::collect_resid_strings(StringPool* outPool, + Vector<uint32_t>* outResIds) const +{ + collect_attr_strings(outPool, outResIds, false); + + const int NC = mChildren.size(); + + for (int i=0; i<NC; i++) { + mChildren.itemAt(i)->collect_resid_strings(outPool, outResIds); + } + + return NO_ERROR; +} + +status_t XMLNode::flatten_node(const StringPool& strings, const sp<AaptFile>& dest, + bool stripComments, bool stripRawValues) const +{ + ResXMLTree_node node; + ResXMLTree_cdataExt cdataExt; + ResXMLTree_namespaceExt namespaceExt; + ResXMLTree_attrExt attrExt; + const void* extData = NULL; + size_t extSize = 0; + ResXMLTree_attribute attr; + + const size_t NA = mAttributes.size(); + const size_t NC = mChildren.size(); + size_t i; + + LOG_ALWAYS_FATAL_IF(NA != mAttributeOrder.size(), "Attributes messed up!"); + + const String16 id16("id"); + const String16 class16("class"); + const String16 style16("style"); + + const type type = getType(); + + memset(&node, 0, sizeof(node)); + memset(&attr, 0, sizeof(attr)); + node.header.headerSize = htods(sizeof(node)); + node.lineNumber = htodl(getStartLineNumber()); + if (!stripComments) { + node.comment.index = htodl( + mComment.size() > 0 ? strings.offsetForString(mComment) : -1); + //if (mComment.size() > 0) { + // printf("Flattening comment: %s\n", String8(mComment).string()); + //} + } else { + node.comment.index = htodl((uint32_t)-1); + } + if (type == TYPE_ELEMENT) { + node.header.type = htods(RES_XML_START_ELEMENT_TYPE); + extData = &attrExt; + extSize = sizeof(attrExt); + memset(&attrExt, 0, sizeof(attrExt)); + if (mNamespaceUri.size() > 0) { + attrExt.ns.index = htodl(strings.offsetForString(mNamespaceUri)); + } else { + attrExt.ns.index = htodl((uint32_t)-1); + } + attrExt.name.index = htodl(strings.offsetForString(mElementName)); + attrExt.attributeStart = htods(sizeof(attrExt)); + attrExt.attributeSize = htods(sizeof(attr)); + attrExt.attributeCount = htods(NA); + attrExt.idIndex = htods(0); + attrExt.classIndex = htods(0); + attrExt.styleIndex = htods(0); + for (i=0; i<NA; i++) { + ssize_t idx = mAttributeOrder.valueAt(i); + const attribute_entry& ae = mAttributes.itemAt(idx); + if (ae.ns.size() == 0) { + if (ae.name == id16) { + attrExt.idIndex = htods(i+1); + } else if (ae.name == class16) { + attrExt.classIndex = htods(i+1); + } else if (ae.name == style16) { + attrExt.styleIndex = htods(i+1); + } + } + } + } else if (type == TYPE_NAMESPACE) { + node.header.type = htods(RES_XML_START_NAMESPACE_TYPE); + extData = &namespaceExt; + extSize = sizeof(namespaceExt); + memset(&namespaceExt, 0, sizeof(namespaceExt)); + if (mNamespacePrefix.size() > 0) { + namespaceExt.prefix.index = htodl(strings.offsetForString(mNamespacePrefix)); + } else { + namespaceExt.prefix.index = htodl((uint32_t)-1); + } + namespaceExt.prefix.index = htodl(strings.offsetForString(mNamespacePrefix)); + namespaceExt.uri.index = htodl(strings.offsetForString(mNamespaceUri)); + LOG_ALWAYS_FATAL_IF(NA != 0, "Namespace nodes can't have attributes!"); + } else if (type == TYPE_CDATA) { + node.header.type = htods(RES_XML_CDATA_TYPE); + extData = &cdataExt; + extSize = sizeof(cdataExt); + memset(&cdataExt, 0, sizeof(cdataExt)); + cdataExt.data.index = htodl(strings.offsetForString(mChars)); + cdataExt.typedData.size = htods(sizeof(cdataExt.typedData)); + cdataExt.typedData.res0 = 0; + cdataExt.typedData.dataType = mCharsValue.dataType; + cdataExt.typedData.data = htodl(mCharsValue.data); + LOG_ALWAYS_FATAL_IF(NA != 0, "CDATA nodes can't have attributes!"); + } + + node.header.size = htodl(sizeof(node) + extSize + (sizeof(attr)*NA)); + + dest->writeData(&node, sizeof(node)); + if (extSize > 0) { + dest->writeData(extData, extSize); + } + + for (i=0; i<NA; i++) { + ssize_t idx = mAttributeOrder.valueAt(i); + const attribute_entry& ae = mAttributes.itemAt(idx); + if (ae.ns.size() > 0) { + attr.ns.index = htodl(strings.offsetForString(ae.ns)); + } else { + attr.ns.index = htodl((uint32_t)-1); + } + attr.name.index = htodl(ae.namePoolIdx); + + if (!stripRawValues || ae.needStringValue()) { + attr.rawValue.index = htodl(strings.offsetForString(ae.string)); + } else { + attr.rawValue.index = htodl((uint32_t)-1); + } + attr.typedValue.size = htods(sizeof(attr.typedValue)); + if (ae.value.dataType == Res_value::TYPE_NULL + || ae.value.dataType == Res_value::TYPE_STRING) { + attr.typedValue.res0 = 0; + attr.typedValue.dataType = Res_value::TYPE_STRING; + attr.typedValue.data = htodl(strings.offsetForString(ae.string)); + } else { + attr.typedValue.res0 = 0; + attr.typedValue.dataType = ae.value.dataType; + attr.typedValue.data = htodl(ae.value.data); + } + dest->writeData(&attr, sizeof(attr)); + } + + for (i=0; i<NC; i++) { + status_t err = mChildren.itemAt(i)->flatten_node(strings, dest, + stripComments, stripRawValues); + if (err != NO_ERROR) { + return err; + } + } + + if (type == TYPE_ELEMENT) { + ResXMLTree_endElementExt endElementExt; + memset(&endElementExt, 0, sizeof(endElementExt)); + node.header.type = htods(RES_XML_END_ELEMENT_TYPE); + node.header.size = htodl(sizeof(node)+sizeof(endElementExt)); + node.lineNumber = htodl(getEndLineNumber()); + node.comment.index = htodl((uint32_t)-1); + endElementExt.ns.index = attrExt.ns.index; + endElementExt.name.index = attrExt.name.index; + dest->writeData(&node, sizeof(node)); + dest->writeData(&endElementExt, sizeof(endElementExt)); + } else if (type == TYPE_NAMESPACE) { + node.header.type = htods(RES_XML_END_NAMESPACE_TYPE); + node.lineNumber = htodl(getEndLineNumber()); + node.comment.index = htodl((uint32_t)-1); + node.header.size = htodl(sizeof(node)+extSize); + dest->writeData(&node, sizeof(node)); + dest->writeData(extData, extSize); + } + + return NO_ERROR; +} diff --git a/tools/aapt/XMLNode.h b/tools/aapt/XMLNode.h new file mode 100644 index 0000000..86548a2 --- /dev/null +++ b/tools/aapt/XMLNode.h @@ -0,0 +1,184 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#ifndef XML_NODE_H +#define XML_NODE_H + +#include "StringPool.h" +#include "ResourceTable.h" + +class XMLNode; + +extern const char* const RESOURCES_ROOT_NAMESPACE; +extern const char* const RESOURCES_ANDROID_NAMESPACE; + +bool isWhitespace(const char16_t* str); + +String16 getNamespaceResourcePackage(String16 namespaceUri, bool* outIsPublic = NULL); + +status_t parseStyledString(Bundle* bundle, + const char* fileName, + ResXMLTree* inXml, + const String16& endTag, + String16* outString, + Vector<StringPool::entry_style_span>* outSpans, + bool isPseudolocalizable); + +void printXMLBlock(ResXMLTree* block); + +status_t parseXMLResource(const sp<AaptFile>& file, ResXMLTree* outTree, + bool stripAll=true, bool keepComments=false, + const char** cDataTags=NULL); + +class XMLNode : public RefBase +{ +public: + static sp<XMLNode> parse(const sp<AaptFile>& file); + + static inline + sp<XMLNode> newNamespace(const String8& filename, const String16& prefix, const String16& uri) { + return new XMLNode(filename, prefix, uri, true); + } + + static inline + sp<XMLNode> newElement(const String8& filename, const String16& ns, const String16& name) { + return new XMLNode(filename, ns, name, false); + } + + static inline + sp<XMLNode> newCData(const String8& filename) { + return new XMLNode(filename); + } + + enum type { + TYPE_NAMESPACE, + TYPE_ELEMENT, + TYPE_CDATA + }; + + type getType() const; + + const String16& getNamespacePrefix() const; + const String16& getNamespaceUri() const; + + const String16& getElementNamespace() const; + const String16& getElementName() const; + const Vector<sp<XMLNode> >& getChildren() const; + + struct attribute_entry { + attribute_entry() : index(~(uint32_t)0), nameResId(0) + { + value.dataType = Res_value::TYPE_NULL; + } + + bool needStringValue() const { + return nameResId == 0 + || value.dataType == Res_value::TYPE_NULL + || value.dataType == Res_value::TYPE_STRING; + } + + String16 ns; + String16 name; + String16 string; + Res_value value; + uint32_t index; + uint32_t nameResId; + mutable uint32_t namePoolIdx; + }; + + const Vector<attribute_entry>& getAttributes() const; + + const String16& getCData() const; + + const String16& getComment() const; + + int32_t getStartLineNumber() const; + int32_t getEndLineNumber() const; + + status_t addChild(const sp<XMLNode>& child); + + status_t addAttribute(const String16& ns, const String16& name, + const String16& value); + + void setAttributeResID(size_t attrIdx, uint32_t resId); + + status_t appendChars(const String16& chars); + + status_t appendComment(const String16& comment); + + void setStartLineNumber(int32_t line); + void setEndLineNumber(int32_t line); + + void removeWhitespace(bool stripAll=true, const char** cDataTags=NULL); + + status_t parseValues(const sp<AaptAssets>& assets, ResourceTable* table); + + status_t assignResourceIds(const sp<AaptAssets>& assets, + const ResourceTable* table = NULL); + + status_t flatten(const sp<AaptFile>& dest, bool stripComments, + bool stripRawValues) const; + + void print(int indent=0); + +private: + struct ParseState + { + String8 filename; + XML_Parser parser; + sp<XMLNode> root; + Vector<sp<XMLNode> > stack; + String16 pendingComment; + }; + + static void XMLCALL + startNamespace(void *userData, const char *prefix, const char *uri); + static void XMLCALL + startElement(void *userData, const char *name, const char **atts); + static void XMLCALL + characterData(void *userData, const XML_Char *s, int len); + static void XMLCALL + endElement(void *userData, const char *name); + static void XMLCALL + endNamespace(void *userData, const char *prefix); + + static void XMLCALL + commentData(void *userData, const char *comment); + + // Creating an element node. + XMLNode(const String8& filename, const String16& s1, const String16& s2, bool isNamespace); + + // Creating a CDATA node. + XMLNode(const String8& filename); + + status_t collect_strings(StringPool* dest, Vector<uint32_t>* outResIds, + bool stripComments, bool stripRawValues) const; + + status_t collect_attr_strings(StringPool* outPool, + Vector<uint32_t>* outResIds, bool allAttrs) const; + + status_t collect_resid_strings(StringPool* outPool, + Vector<uint32_t>* outResIds) const; + + status_t flatten_node(const StringPool& strings, const sp<AaptFile>& dest, + bool stripComments, bool stripRawValues) const; + + String16 mNamespacePrefix; + String16 mNamespaceUri; + String16 mElementName; + Vector<sp<XMLNode> > mChildren; + Vector<attribute_entry> mAttributes; + KeyedVector<uint32_t, uint32_t> mAttributeOrder; + uint32_t mNextAttributeIndex; + String16 mChars; + Res_value mCharsValue; + String16 mComment; + String8 mFilename; + int32_t mStartLineNumber; + int32_t mEndLineNumber; +}; + +#endif diff --git a/tools/aapt/printapk.cpp b/tools/aapt/printapk.cpp new file mode 100644 index 0000000..4cf73d8 --- /dev/null +++ b/tools/aapt/printapk.cpp @@ -0,0 +1,127 @@ +#include <utils/ResourceTypes.h> +#include <utils/String8.h> +#include <utils/String16.h> +#include <zipfile/zipfile.h> +#include <stdio.h> +#include <fcntl.h> +#include <unistd.h> +#include <stdlib.h> + +using namespace android; + +static int +usage() +{ + fprintf(stderr, + "usage: apk APKFILE\n" + "\n" + "APKFILE an android packge file produced by aapt.\n" + ); + return 1; +} + + +int +main(int argc, char** argv) +{ + const char* filename; + int fd; + ssize_t amt; + off_t size; + void* buf; + zipfile_t zip; + zipentry_t entry; + void* cookie; + void* resfile; + int bufsize; + int err; + + if (argc != 2) { + return usage(); + } + + filename = argv[1]; + fd = open(filename, O_RDONLY); + if (fd == -1) { + fprintf(stderr, "apk: couldn't open file for read: %s\n", filename); + return 1; + } + + size = lseek(fd, 0, SEEK_END); + amt = lseek(fd, 0, SEEK_SET); + + if (size < 0 || amt < 0) { + fprintf(stderr, "apk: error determining file size: %s\n", filename); + return 1; + } + + buf = malloc(size); + if (buf == NULL) { + fprintf(stderr, "apk: file too big: %s\n", filename); + return 1; + } + + amt = read(fd, buf, size); + if (amt != size) { + fprintf(stderr, "apk: error reading file: %s\n", filename); + return 1; + } + + close(fd); + + zip = init_zipfile(buf, size); + if (zip == NULL) { + fprintf(stderr, "apk: file doesn't seem to be a zip file: %s\n", + filename); + return 1; + } + + printf("files:\n"); + cookie = NULL; + while ((entry = iterate_zipfile(zip, &cookie))) { + char* name = get_zipentry_name(entry); + printf(" %s\n", name); + free(name); + } + + entry = lookup_zipentry(zip, "resources.arsc"); + if (entry != NULL) { + size = get_zipentry_size(entry); + bufsize = size + (size / 1000) + 1; + resfile = malloc(bufsize); + + err = decompress_zipentry(entry, resfile, bufsize); + if (err != 0) { + fprintf(stderr, "apk: error decompressing resources.arsc"); + return 1; + } + + ResTable res(resfile, size, resfile); + res.print(); +#if 0 + size_t tableCount = res.getTableCount(); + printf("Tables: %d\n", (int)tableCount); + for (size_t tableIndex=0; tableIndex<tableCount; tableIndex++) { + const ResStringPool* strings = res.getTableStringBlock(tableIndex); + size_t stringCount = strings->size(); + for (size_t stringIndex=0; stringIndex<stringCount; stringIndex++) { + size_t len; + const char16_t* ch = strings->stringAt(stringIndex, &len); + String8 s(String16(ch, len)); + printf(" [%3d] %s\n", (int)stringIndex, s.string()); + } + } + + size_t basePackageCount = res.getBasePackageCount(); + printf("Base Packages: %d\n", (int)basePackageCount); + for (size_t bpIndex=0; bpIndex<basePackageCount; bpIndex++) { + const char16_t* ch = res.getBasePackageName(bpIndex); + String8 s = String8(String16(ch)); + printf(" [%3d] %s\n", (int)bpIndex, s.string()); + } +#endif + } + + + return 0; +} diff --git a/tools/aapt/tests/plurals/AndroidManifest.xml b/tools/aapt/tests/plurals/AndroidManifest.xml new file mode 100644 index 0000000..c721dee --- /dev/null +++ b/tools/aapt/tests/plurals/AndroidManifest.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.aapt.test.plurals"> + +</manifest> diff --git a/tools/aapt/tests/plurals/res/values/strings.xml b/tools/aapt/tests/plurals/res/values/strings.xml new file mode 100644 index 0000000..1c1fc19 --- /dev/null +++ b/tools/aapt/tests/plurals/res/values/strings.xml @@ -0,0 +1,7 @@ +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="ok">OK</string> + <plurals name="a_plural"> + <item quantity="one">A dog</item> + <item quantity="other">Some dogs</item> + </plurals> +</resources> diff --git a/tools/aapt/tests/plurals/run.sh b/tools/aapt/tests/plurals/run.sh new file mode 100755 index 0000000..4d39e10 --- /dev/null +++ b/tools/aapt/tests/plurals/run.sh @@ -0,0 +1,16 @@ +TEST_DIR=tools/aapt/tests/plurals +TEST_OUT_DIR=out/plurals_test + +rm -rf $TEST_OUT_DIR +mkdir -p $TEST_OUT_DIR +mkdir -p $TEST_OUT_DIR/java + +#gdb --args \ +aapt package -v -x -m -z -J $TEST_OUT_DIR/java -M $TEST_DIR/AndroidManifest.xml \ + -I out/target/common/obj/APPS/framework-res_intermediates/package-export.apk \ + -P $TEST_OUT_DIR/public_resources.xml \ + -S $TEST_DIR/res + +echo +echo "==================== FILES CREATED ==================== " +find $TEST_OUT_DIR -type f diff --git a/tools/aidl/AST.cpp b/tools/aidl/AST.cpp new file mode 100755 index 0000000..91802a9 --- /dev/null +++ b/tools/aidl/AST.cpp @@ -0,0 +1,867 @@ +#include "AST.h" +#include "Type.h" + +void +WriteModifiers(FILE* to, int mod, int mask) +{ + int m = mod & mask; + + if ((m & SCOPE_MASK) == PUBLIC) { + fprintf(to, "public "); + } + else if ((m & SCOPE_MASK) == PRIVATE) { + fprintf(to, "private "); + } + else if ((m & SCOPE_MASK) == PROTECTED) { + fprintf(to, "protected "); + } + + if (m & STATIC) { + fprintf(to, "static "); + } + + if (m & FINAL) { + fprintf(to, "final "); + } + + if (m & ABSTRACT) { + fprintf(to, "abstract "); + } +} + +void +WriteArgumentList(FILE* to, const vector<Expression*>& arguments) +{ + size_t N = arguments.size(); + for (size_t i=0; i<N; i++) { + arguments[i]->Write(to); + if (i != N-1) { + fprintf(to, ", "); + } + } +} + +ClassElement::ClassElement() +{ +} + +ClassElement::~ClassElement() +{ +} + +Field::Field() + :ClassElement(), + modifiers(0), + variable(NULL) +{ +} + +Field::Field(int m, Variable* v) + :ClassElement(), + modifiers(m), + variable(v) +{ +} + +Field::~Field() +{ +} + +void +Field::GatherTypes(set<Type*>* types) const +{ + types->insert(this->variable->type); +} + +void +Field::Write(FILE* to) +{ + if (this->comment.length() != 0) { + fprintf(to, "%s\n", this->comment.c_str()); + } + WriteModifiers(to, this->modifiers, SCOPE_MASK | STATIC | FINAL); + fprintf(to, "%s %s", this->variable->type->QualifiedName().c_str(), + this->variable->name.c_str()); + if (this->value.length() != 0) { + fprintf(to, " = %s", this->value.c_str()); + } + fprintf(to, ";\n"); +} + +Expression::~Expression() +{ +} + +LiteralExpression::LiteralExpression(const string& v) + :value(v) +{ +} + +LiteralExpression::~LiteralExpression() +{ +} + +void +LiteralExpression::Write(FILE* to) +{ + fprintf(to, "%s", this->value.c_str()); +} + +Variable::Variable() + :type(NULL), + name(), + dimension(0) +{ +} + +Variable::Variable(Type* t, const string& n) + :type(t), + name(n), + dimension(0) +{ +} + +Variable::Variable(Type* t, const string& n, int d) + :type(t), + name(n), + dimension(d) +{ +} + +Variable::~Variable() +{ +} + +void +Variable::GatherTypes(set<Type*>* types) const +{ + types->insert(this->type); +} + +void +Variable::WriteDeclaration(FILE* to) +{ + string dim; + for (int i=0; i<this->dimension; i++) { + dim += "[]"; + } + fprintf(to, "%s%s %s", this->type->QualifiedName().c_str(), dim.c_str(), + this->name.c_str()); +} + +void +Variable::Write(FILE* to) +{ + fprintf(to, "%s", name.c_str()); +} + +FieldVariable::FieldVariable(Expression* o, const string& n) + :object(o), + clazz(NULL), + name(n) +{ +} + +FieldVariable::FieldVariable(Type* c, const string& n) + :object(NULL), + clazz(c), + name(n) +{ +} + +FieldVariable::~FieldVariable() +{ +} + +void +FieldVariable::Write(FILE* to) +{ + if (this->object != NULL) { + this->object->Write(to); + } + else if (this->clazz != NULL) { + fprintf(to, "%s", this->clazz->QualifiedName().c_str()); + } + fprintf(to, ".%s", name.c_str()); +} + + +Statement::~Statement() +{ +} + +StatementBlock::StatementBlock() +{ +} + +StatementBlock::~StatementBlock() +{ +} + +void +StatementBlock::Write(FILE* to) +{ + fprintf(to, "{\n"); + int N = this->statements.size(); + for (int i=0; i<N; i++) { + this->statements[i]->Write(to); + } + fprintf(to, "}\n"); +} + +void +StatementBlock::Add(Statement* statement) +{ + this->statements.push_back(statement); +} + +void +StatementBlock::Add(Expression* expression) +{ + this->statements.push_back(new ExpressionStatement(expression)); +} + +ExpressionStatement::ExpressionStatement(Expression* e) + :expression(e) +{ +} + +ExpressionStatement::~ExpressionStatement() +{ +} + +void +ExpressionStatement::Write(FILE* to) +{ + this->expression->Write(to); + fprintf(to, ";\n"); +} + +Assignment::Assignment(Variable* l, Expression* r) + :lvalue(l), + rvalue(r), + cast(NULL) +{ +} + +Assignment::Assignment(Variable* l, Expression* r, Type* c) + :lvalue(l), + rvalue(r), + cast(c) +{ +} + +Assignment::~Assignment() +{ +} + +void +Assignment::Write(FILE* to) +{ + this->lvalue->Write(to); + fprintf(to, " = "); + if (this->cast != NULL) { + fprintf(to, "(%s)", this->cast->QualifiedName().c_str()); + } + this->rvalue->Write(to); +} + +MethodCall::MethodCall(const string& n) + :obj(NULL), + clazz(NULL), + name(n) +{ +} + +MethodCall::MethodCall(Expression* o, const string& n) + :obj(o), + clazz(NULL), + name(n) +{ +} + +MethodCall::MethodCall(Type* t, const string& n) + :obj(NULL), + clazz(t), + name(n) +{ +} + +MethodCall::MethodCall(Expression* o, const string& n, int argc = 0, ...) + :obj(o), + clazz(NULL), + name(n) +{ + va_list args; + va_start(args, argc); + init(argc, args); + va_end(args); +} + +MethodCall::MethodCall(Type* t, const string& n, int argc = 0, ...) + :obj(NULL), + clazz(t), + name(n) +{ + va_list args; + va_start(args, argc); + init(argc, args); + va_end(args); +} + +MethodCall::~MethodCall() +{ +} + +void +MethodCall::init(int n, va_list args) +{ + for (int i=0; i<n; i++) { + Expression* expression = (Expression*)va_arg(args, void*); + this->arguments.push_back(expression); + } +} + +void +MethodCall::Write(FILE* to) +{ + if (this->obj != NULL) { + this->obj->Write(to); + fprintf(to, "."); + } + else if (this->clazz != NULL) { + fprintf(to, "%s.", this->clazz->QualifiedName().c_str()); + } + fprintf(to, "%s(", this->name.c_str()); + WriteArgumentList(to, this->arguments); + fprintf(to, ")"); +} + +Comparison::Comparison(Expression* l, const string& o, Expression* r) + :lvalue(l), + op(o), + rvalue(r) +{ +} + +Comparison::~Comparison() +{ +} + +void +Comparison::Write(FILE* to) +{ + fprintf(to, "("); + this->lvalue->Write(to); + fprintf(to, "%s", this->op.c_str()); + this->rvalue->Write(to); + fprintf(to, ")"); +} + +NewExpression::NewExpression(Type* t) + :type(t) +{ +} + +NewExpression::~NewExpression() +{ +} + +void +NewExpression::Write(FILE* to) +{ + fprintf(to, "new %s(", this->type->InstantiableName().c_str()); + WriteArgumentList(to, this->arguments); + fprintf(to, ")"); +} + +NewArrayExpression::NewArrayExpression(Type* t, Expression* s) + :type(t), + size(s) +{ +} + +NewArrayExpression::~NewArrayExpression() +{ +} + +void +NewArrayExpression::Write(FILE* to) +{ + fprintf(to, "new %s[", this->type->QualifiedName().c_str()); + size->Write(to); + fprintf(to, "]"); +} + +Ternary::Ternary() + :condition(NULL), + ifpart(NULL), + elsepart(NULL) +{ +} + +Ternary::Ternary(Expression* a, Expression* b, Expression* c) + :condition(a), + ifpart(b), + elsepart(c) +{ +} + +Ternary::~Ternary() +{ +} + +void +Ternary::Write(FILE* to) +{ + fprintf(to, "(("); + this->condition->Write(to); + fprintf(to, ")?("); + this->ifpart->Write(to); + fprintf(to, "):("); + this->elsepart->Write(to); + fprintf(to, "))"); +} + +Cast::Cast() + :type(NULL), + expression(NULL) +{ +} + +Cast::Cast(Type* t, Expression* e) + :type(t), + expression(e) +{ +} + +Cast::~Cast() +{ +} + +void +Cast::Write(FILE* to) +{ + fprintf(to, "((%s)", this->type->QualifiedName().c_str()); + expression->Write(to); + fprintf(to, ")"); +} + +VariableDeclaration::VariableDeclaration(Variable* l, Expression* r, Type* c) + :lvalue(l), + cast(c), + rvalue(r) +{ +} + +VariableDeclaration::VariableDeclaration(Variable* l) + :lvalue(l), + cast(NULL), + rvalue(NULL) +{ +} + +VariableDeclaration::~VariableDeclaration() +{ +} + +void +VariableDeclaration::Write(FILE* to) +{ + this->lvalue->WriteDeclaration(to); + if (this->rvalue != NULL) { + fprintf(to, " = "); + if (this->cast != NULL) { + fprintf(to, "(%s)", this->cast->QualifiedName().c_str()); + } + this->rvalue->Write(to); + } + fprintf(to, ";\n"); +} + +IfStatement::IfStatement() + :expression(NULL), + statements(new StatementBlock), + elseif(NULL) +{ +} + +IfStatement::~IfStatement() +{ +} + +void +IfStatement::Write(FILE* to) +{ + if (this->expression != NULL) { + fprintf(to, "if ("); + this->expression->Write(to); + fprintf(to, ") "); + } + this->statements->Write(to); + if (this->elseif != NULL) { + fprintf(to, "else "); + this->elseif->Write(to); + } +} + +ReturnStatement::ReturnStatement(Expression* e) + :expression(e) +{ +} + +ReturnStatement::~ReturnStatement() +{ +} + +void +ReturnStatement::Write(FILE* to) +{ + fprintf(to, "return "); + this->expression->Write(to); + fprintf(to, ";\n"); +} + +TryStatement::TryStatement() + :statements(new StatementBlock) +{ +} + +TryStatement::~TryStatement() +{ +} + +void +TryStatement::Write(FILE* to) +{ + fprintf(to, "try "); + this->statements->Write(to); +} + +CatchStatement::CatchStatement(Variable* e) + :statements(new StatementBlock), + exception(e) +{ +} + +CatchStatement::~CatchStatement() +{ +} + +void +CatchStatement::Write(FILE* to) +{ + fprintf(to, "catch "); + if (this->exception != NULL) { + fprintf(to, "("); + this->exception->WriteDeclaration(to); + fprintf(to, ") "); + } + this->statements->Write(to); +} + +FinallyStatement::FinallyStatement() + :statements(new StatementBlock) +{ +} + +FinallyStatement::~FinallyStatement() +{ +} + +void +FinallyStatement::Write(FILE* to) +{ + fprintf(to, "finally "); + this->statements->Write(to); +} + +Case::Case() + :statements(new StatementBlock) +{ +} + +Case::Case(const string& c) + :statements(new StatementBlock) +{ + cases.push_back(c); +} + +Case::~Case() +{ +} + +void +Case::Write(FILE* to) +{ + int N = this->cases.size(); + if (N > 0) { + for (int i=0; i<N; i++) { + string s = this->cases[i]; + if (s.length() != 0) { + fprintf(to, "case %s:\n", s.c_str()); + } else { + fprintf(to, "default:\n"); + } + } + } else { + fprintf(to, "default:\n"); + } + statements->Write(to); +} + +SwitchStatement::SwitchStatement(Expression* e) + :expression(e) +{ +} + +SwitchStatement::~SwitchStatement() +{ +} + +void +SwitchStatement::Write(FILE* to) +{ + fprintf(to, "switch ("); + this->expression->Write(to); + fprintf(to, ")\n{\n"); + int N = this->cases.size(); + for (int i=0; i<N; i++) { + this->cases[i]->Write(to); + } + fprintf(to, "}\n"); +} + +Method::Method() + :ClassElement(), + modifiers(0), + returnType(NULL), // (NULL means constructor) + returnTypeDimension(0), + statements(NULL) +{ +} + +Method::~Method() +{ +} + +void +Method::GatherTypes(set<Type*>* types) const +{ + size_t N, i; + + if (this->returnType) { + types->insert(this->returnType); + } + + N = this->parameters.size(); + for (i=0; i<N; i++) { + this->parameters[i]->GatherTypes(types); + } + + N = this->exceptions.size(); + for (i=0; i<N; i++) { + types->insert(this->exceptions[i]); + } +} + +void +Method::Write(FILE* to) +{ + size_t N, i; + + if (this->comment.length() != 0) { + fprintf(to, "%s\n", this->comment.c_str()); + } + + WriteModifiers(to, this->modifiers, SCOPE_MASK | STATIC | FINAL); + + if (this->returnType != NULL) { + string dim; + for (i=0; i<this->returnTypeDimension; i++) { + dim += "[]"; + } + fprintf(to, "%s%s ", this->returnType->QualifiedName().c_str(), + dim.c_str()); + } + + fprintf(to, "%s(", this->name.c_str()); + + N = this->parameters.size(); + for (i=0; i<N; i++) { + this->parameters[i]->WriteDeclaration(to); + if (i != N-1) { + fprintf(to, ", "); + } + } + + fprintf(to, ")"); + + N = this->exceptions.size(); + for (i=0; i<N; i++) { + if (i == 0) { + fprintf(to, " throws "); + } else { + fprintf(to, ", "); + } + fprintf(to, "%s", this->exceptions[i]->QualifiedName().c_str()); + } + + if (this->statements == NULL) { + fprintf(to, ";\n"); + } else { + fprintf(to, "\n"); + this->statements->Write(to); + } +} + +Class::Class() + :modifiers(0), + what(CLASS), + type(NULL), + extends(NULL) +{ +} + +Class::~Class() +{ +} + +void +Class::GatherTypes(set<Type*>* types) const +{ + int N, i; + + types->insert(this->type); + if (this->extends != NULL) { + types->insert(this->extends); + } + + N = this->interfaces.size(); + for (i=0; i<N; i++) { + types->insert(this->interfaces[i]); + } + + N = this->elements.size(); + for (i=0; i<N; i++) { + this->elements[i]->GatherTypes(types); + } +} + +void +Class::Write(FILE* to) +{ + size_t N, i; + + if (this->comment.length() != 0) { + fprintf(to, "%s\n", this->comment.c_str()); + } + + WriteModifiers(to, this->modifiers, ALL_MODIFIERS); + + if (this->what == Class::CLASS) { + fprintf(to, "class "); + } else { + fprintf(to, "interface "); + } + + string name = this->type->Name(); + size_t pos = name.rfind('.'); + if (pos != string::npos) { + name = name.c_str() + pos + 1; + } + + fprintf(to, "%s", name.c_str()); + + if (this->extends != NULL) { + fprintf(to, " extends %s", this->extends->QualifiedName().c_str()); + } + + N = this->interfaces.size(); + if (N != 0) { + if (this->what == Class::CLASS) { + fprintf(to, " implements"); + } else { + fprintf(to, " extends"); + } + for (i=0; i<N; i++) { + fprintf(to, " %s", this->interfaces[i]->QualifiedName().c_str()); + } + } + + fprintf(to, "\n"); + fprintf(to, "{\n"); + + N = this->elements.size(); + for (i=0; i<N; i++) { + this->elements[i]->Write(to); + } + + fprintf(to, "}\n"); + +} + +Document::Document() +{ +} + +Document::~Document() +{ +} + +static string +escape_backslashes(const string& str) +{ + string result; + const size_t I=str.length(); + for (size_t i=0; i<I; i++) { + char c = str[i]; + if (c == '\\') { + result += "\\\\"; + } else { + result += c; + } + } + return result; +} + +void +Document::Write(FILE* to) +{ + size_t N, i; + + if (this->comment.length() != 0) { + fprintf(to, "%s\n", this->comment.c_str()); + } + fprintf(to, "/*\n" + " * This file is auto-generated. DO NOT MODIFY.\n" + " * Original file: %s\n" + " */\n", escape_backslashes(this->originalSrc).c_str()); + if (this->package.length() != 0) { + fprintf(to, "package %s;\n", this->package.c_str()); + } + + // gather the types for the import statements + set<Type*> types; + N = this->classes.size(); + for (i=0; i<N; i++) { + Class* c = this->classes[i]; + c->GatherTypes(&types); + } + + set<Type*>::iterator it; + for (it=types.begin(); it!=types.end(); it++) { + Type* t = *it; + string pkg = t->Package(); + if (pkg.length() != 0 && pkg != this->package) { + fprintf(to, "import %s;\n", t->ImportType().c_str()); + } + } + + N = this->classes.size(); + for (i=0; i<N; i++) { + Class* c = this->classes[i]; + c->Write(to); + } +} + diff --git a/tools/aidl/AST.h b/tools/aidl/AST.h new file mode 100755 index 0000000..1dedd04 --- /dev/null +++ b/tools/aidl/AST.h @@ -0,0 +1,346 @@ +#ifndef AIDL_AST_H +#define AIDL_AST_H + +#include <string> +#include <vector> +#include <set> +#include <stdarg.h> + +using namespace std; + +class Type; + +enum { + PACKAGE_PRIVATE = 0x00000000, + PUBLIC = 0x00000001, + PRIVATE = 0x00000002, + PROTECTED = 0x00000003, + SCOPE_MASK = 0x00000003, + + STATIC = 0x00000010, + FINAL = 0x00000020, + ABSTRACT = 0x00000040, + + ALL_MODIFIERS = 0xffffffff +}; + +// Write the modifiers that are set in both mod and mask +void WriteModifiers(FILE* to, int mod, int mask); + +struct ClassElement +{ + ClassElement(); + virtual ~ClassElement(); + + virtual void GatherTypes(set<Type*>* types) const = 0; + virtual void Write(FILE* to) = 0; +}; + +struct Expression +{ + virtual ~Expression(); + virtual void Write(FILE* to) = 0; +}; + +struct LiteralExpression : public Expression +{ + string value; + + LiteralExpression(const string& value); + virtual ~LiteralExpression(); + virtual void Write(FILE* to); +}; + +struct Variable : public Expression +{ + Type* type; + string name; + int dimension; + + Variable(); + Variable(Type* type, const string& name); + Variable(Type* type, const string& name, int dimension); + virtual ~Variable(); + + virtual void GatherTypes(set<Type*>* types) const; + void WriteDeclaration(FILE* to); + void Write(FILE* to); +}; + +struct FieldVariable : public Expression +{ + Expression* object; + Type* clazz; + string name; + + FieldVariable(Expression* object, const string& name); + FieldVariable(Type* clazz, const string& name); + virtual ~FieldVariable(); + + void Write(FILE* to); +}; + +struct Field : public ClassElement +{ + string comment; + int modifiers; + Variable *variable; + string value; + + Field(); + Field(int modifiers, Variable* variable); + virtual ~Field(); + + virtual void GatherTypes(set<Type*>* types) const; + virtual void Write(FILE* to); +}; + +struct Statement +{ + virtual ~Statement(); + virtual void Write(FILE* to) = 0; +}; + +struct StatementBlock +{ + vector<Statement*> statements; + + StatementBlock(); + virtual ~StatementBlock(); + virtual void Write(FILE* to); + + void Add(Statement* statement); + void Add(Expression* expression); +}; + +struct ExpressionStatement : public Statement +{ + Expression* expression; + + ExpressionStatement(Expression* expression); + virtual ~ExpressionStatement(); + virtual void Write(FILE* to); +}; + +struct Assignment : public Expression +{ + Variable* lvalue; + Expression* rvalue; + Type* cast; + + Assignment(Variable* lvalue, Expression* rvalue); + Assignment(Variable* lvalue, Expression* rvalue, Type* cast); + virtual ~Assignment(); + virtual void Write(FILE* to); +}; + +struct MethodCall : public Expression +{ + Expression* obj; + Type* clazz; + string name; + vector<Expression*> arguments; + vector<string> exceptions; + + MethodCall(const string& name); + MethodCall(Expression* obj, const string& name); + MethodCall(Type* clazz, const string& name); + MethodCall(Expression* obj, const string& name, int argc, ...); + MethodCall(Type* clazz, const string& name, int argc, ...); + virtual ~MethodCall(); + virtual void Write(FILE* to); + +private: + void init(int n, va_list args); +}; + +struct Comparison : public Expression +{ + Expression* lvalue; + string op; + Expression* rvalue; + + Comparison(Expression* lvalue, const string& op, Expression* rvalue); + virtual ~Comparison(); + virtual void Write(FILE* to); +}; + +struct NewExpression : public Expression +{ + Type* type; + vector<Expression*> arguments; + + NewExpression(Type* type); + virtual ~NewExpression(); + virtual void Write(FILE* to); +}; + +struct NewArrayExpression : public Expression +{ + Type* type; + Expression* size; + + NewArrayExpression(Type* type, Expression* size); + virtual ~NewArrayExpression(); + virtual void Write(FILE* to); +}; + +struct Ternary : public Expression +{ + Expression* condition; + Expression* ifpart; + Expression* elsepart; + + Ternary(); + Ternary(Expression* condition, Expression* ifpart, Expression* elsepart); + virtual ~Ternary(); + virtual void Write(FILE* to); +}; + +struct Cast : public Expression +{ + Type* type; + Expression* expression; + + Cast(); + Cast(Type* type, Expression* expression); + virtual ~Cast(); + virtual void Write(FILE* to); +}; + +struct VariableDeclaration : public Statement +{ + Variable* lvalue; + Type* cast; + Expression* rvalue; + + VariableDeclaration(Variable* lvalue); + VariableDeclaration(Variable* lvalue, Expression* rvalue, Type* cast = NULL); + virtual ~VariableDeclaration(); + virtual void Write(FILE* to); +}; + +struct IfStatement : public Statement +{ + Expression* expression; + StatementBlock* statements; + IfStatement* elseif; + + IfStatement(); + virtual ~IfStatement(); + virtual void Write(FILE* to); +}; + +struct ReturnStatement : public Statement +{ + Expression* expression; + + ReturnStatement(Expression* expression); + virtual ~ReturnStatement(); + virtual void Write(FILE* to); +}; + +struct TryStatement : public Statement +{ + StatementBlock* statements; + + TryStatement(); + virtual ~TryStatement(); + virtual void Write(FILE* to); +}; + +struct CatchStatement : public Statement +{ + StatementBlock* statements; + Variable* exception; + + CatchStatement(Variable* exception); + virtual ~CatchStatement(); + virtual void Write(FILE* to); +}; + +struct FinallyStatement : public Statement +{ + StatementBlock* statements; + + FinallyStatement(); + virtual ~FinallyStatement(); + virtual void Write(FILE* to); +}; + +struct Case +{ + vector<string> cases; + StatementBlock* statements; + + Case(); + Case(const string& c); + virtual ~Case(); + virtual void Write(FILE* to); +}; + +struct SwitchStatement : public Statement +{ + Expression* expression; + vector<Case*> cases; + + SwitchStatement(Expression* expression); + virtual ~SwitchStatement(); + virtual void Write(FILE* to); +}; + +struct Method : public ClassElement +{ + string comment; + int modifiers; + Type* returnType; + size_t returnTypeDimension; + string name; + vector<Variable*> parameters; + vector<Type*> exceptions; + StatementBlock* statements; + + Method(); + virtual ~Method(); + + virtual void GatherTypes(set<Type*>* types) const; + virtual void Write(FILE* to); +}; + +struct Class : public ClassElement +{ + enum { + CLASS, + INTERFACE + }; + + string comment; + int modifiers; + int what; // CLASS or INTERFACE + Type* type; + Type* extends; + vector<Type*> interfaces; + vector<ClassElement*> elements; + + Class(); + virtual ~Class(); + + virtual void GatherTypes(set<Type*>* types) const; + virtual void Write(FILE* to); +}; + +struct Document +{ + string comment; + string package; + string originalSrc; + set<Type*> imports; + vector<Class*> classes; + + Document(); + virtual ~Document(); + + virtual void Write(FILE* to); +}; + +#endif // AIDL_AST_H diff --git a/tools/aidl/Android.mk b/tools/aidl/Android.mk new file mode 100644 index 0000000..944aeb6 --- /dev/null +++ b/tools/aidl/Android.mk @@ -0,0 +1,24 @@ +# Copyright 2007 The Android Open Source Project +# +# Copies files into the directory structure described by a manifest + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + aidl_language_l.l \ + aidl_language_y.y \ + aidl.cpp \ + aidl_language.cpp \ + options.cpp \ + search_path.cpp \ + AST.cpp \ + Type.cpp \ + generate_java.cpp + +LOCAL_CFLAGS := -g +LOCAL_MODULE := aidl + +include $(BUILD_HOST_EXECUTABLE) + + diff --git a/tools/aidl/Type.cpp b/tools/aidl/Type.cpp new file mode 100755 index 0000000..a44072d --- /dev/null +++ b/tools/aidl/Type.cpp @@ -0,0 +1,1228 @@ +#include "Type.h" + +Namespace NAMES; + +Type* VOID_TYPE; +Type* BOOLEAN_TYPE; +Type* BYTE_TYPE; +Type* CHAR_TYPE; +Type* INT_TYPE; +Type* LONG_TYPE; +Type* FLOAT_TYPE; +Type* DOUBLE_TYPE; +Type* STRING_TYPE; +Type* CHAR_SEQUENCE_TYPE; +Type* TEXT_UTILS_TYPE; +Type* REMOTE_EXCEPTION_TYPE; +Type* RUNTIME_EXCEPTION_TYPE; +Type* IBINDER_TYPE; +Type* IINTERFACE_TYPE; +Type* BINDER_NATIVE_TYPE; +Type* BINDER_PROXY_TYPE; +Type* PARCEL_TYPE; +Type* PARCELABLE_INTERFACE_TYPE; +Type* MAP_TYPE; +Type* LIST_TYPE; +Type* CLASSLOADER_TYPE; + +Expression* NULL_VALUE; +Expression* THIS_VALUE; +Expression* SUPER_VALUE; +Expression* TRUE_VALUE; +Expression* FALSE_VALUE; + +void +register_base_types() +{ + VOID_TYPE = new BasicType("void", "XXX", "XXX", "XXX", "XXX", "XXX"); + NAMES.Add(VOID_TYPE); + + BOOLEAN_TYPE = new BooleanType(); + NAMES.Add(BOOLEAN_TYPE); + + BYTE_TYPE = new BasicType("byte", "writeByte", "readByte", + "writeByteArray", "createByteArray", "readByteArray"); + NAMES.Add(BYTE_TYPE); + + CHAR_TYPE = new CharType(); + NAMES.Add(CHAR_TYPE); + + INT_TYPE = new BasicType("int", "writeInt", "readInt", + "writeIntArray", "createIntArray", "readIntArray"); + NAMES.Add(INT_TYPE); + + LONG_TYPE = new BasicType("long", "writeLong", "readLong", + "writeLongArray", "createLongArray", "readLongArray"); + NAMES.Add(LONG_TYPE); + + FLOAT_TYPE = new BasicType("float", "writeFloat", "readFloat", + "writeFloatArray", "createFloatArray", "readFloatArray"); + NAMES.Add(FLOAT_TYPE); + + DOUBLE_TYPE = new BasicType("double", "writeDouble", "readDouble", + "writeDoubleArray", "createDoubleArray", "readDoubleArray"); + NAMES.Add(DOUBLE_TYPE); + + STRING_TYPE = new StringType(); + NAMES.Add(STRING_TYPE); + + CHAR_SEQUENCE_TYPE = new CharSequenceType(); + NAMES.Add(CHAR_SEQUENCE_TYPE); + + MAP_TYPE = new MapType(); + NAMES.Add(MAP_TYPE); + + LIST_TYPE = new ListType(); + NAMES.Add(LIST_TYPE); + + TEXT_UTILS_TYPE = new Type("android.text", "TextUtils", + Type::BUILT_IN, false, false); + NAMES.Add(TEXT_UTILS_TYPE); + + REMOTE_EXCEPTION_TYPE = new RemoteExceptionType(); + NAMES.Add(REMOTE_EXCEPTION_TYPE); + + RUNTIME_EXCEPTION_TYPE = new RuntimeExceptionType(); + NAMES.Add(RUNTIME_EXCEPTION_TYPE); + + IBINDER_TYPE = new IBinderType(); + NAMES.Add(IBINDER_TYPE); + + IINTERFACE_TYPE = new IInterfaceType(); + NAMES.Add(IINTERFACE_TYPE); + + BINDER_NATIVE_TYPE = new BinderType(); + NAMES.Add(BINDER_NATIVE_TYPE); + + BINDER_PROXY_TYPE = new BinderProxyType(); + NAMES.Add(BINDER_PROXY_TYPE); + + PARCEL_TYPE = new ParcelType(); + NAMES.Add(PARCEL_TYPE); + + PARCELABLE_INTERFACE_TYPE = new ParcelableInterfaceType(); + NAMES.Add(PARCELABLE_INTERFACE_TYPE); + + CLASSLOADER_TYPE = new ClassLoaderType(); + NAMES.Add(CLASSLOADER_TYPE); + + NULL_VALUE = new LiteralExpression("null"); + THIS_VALUE = new LiteralExpression("this"); + SUPER_VALUE = new LiteralExpression("super"); + TRUE_VALUE = new LiteralExpression("true"); + FALSE_VALUE = new LiteralExpression("false"); + + NAMES.AddGenericType("java.util", "List", 1); + NAMES.AddGenericType("java.util", "Map", 2); +} + +static Type* +make_generic_type(const string& package, const string& name, + const vector<Type*>& args) +{ + if (package == "java.util" && name == "List") { + return new GenericListType("java.util", "List", args); + } + return NULL; + //return new GenericType(package, name, args); +} + +// ================================================================ + +Type::Type(const string& name, int kind, bool canWriteToParcel, bool canBeOut) + :m_package(), + m_name(name), + m_declFile(""), + m_declLine(-1), + m_kind(kind), + m_canWriteToParcel(canWriteToParcel), + m_canBeOut(canBeOut) +{ + m_qualifiedName = name; +} + +Type::Type(const string& package, const string& name, + int kind, bool canWriteToParcel, bool canBeOut, + const string& declFile, int declLine) + :m_package(package), + m_name(name), + m_declFile(declFile), + m_declLine(declLine), + m_kind(kind), + m_canWriteToParcel(canWriteToParcel), + m_canBeOut(canBeOut) +{ + if (package.length() > 0) { + m_qualifiedName = package; + m_qualifiedName += '.'; + } + m_qualifiedName += name; +} + +Type::~Type() +{ +} + +bool +Type::CanBeArray() const +{ + return false; +} + +string +Type::ImportType() const +{ + return m_qualifiedName; +} + +string +Type::CreatorName() const +{ + return ""; +} + +string +Type::InstantiableName() const +{ + return QualifiedName(); +} + + +void +Type::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + fprintf(stderr, "aidl:internal error %s:%d qualifiedName=%sn", + __FILE__, __LINE__, m_qualifiedName.c_str()); + addTo->Add(new LiteralExpression("/* WriteToParcel error " + + m_qualifiedName + " */")); +} + +void +Type::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + fprintf(stderr, "aidl:internal error %s:%d qualifiedName=%s\n", + __FILE__, __LINE__, m_qualifiedName.c_str()); + addTo->Add(new LiteralExpression("/* CreateFromParcel error " + + m_qualifiedName + " */")); +} + +void +Type::ReadFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + fprintf(stderr, "aidl:internal error %s:%d qualifiedName=%s\n", + __FILE__, __LINE__, m_qualifiedName.c_str()); + addTo->Add(new LiteralExpression("/* ReadFromParcel error " + + m_qualifiedName + " */")); +} + +void +Type::WriteArrayToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + fprintf(stderr, "aidl:internal error %s:%d qualifiedName=%s\n", + __FILE__, __LINE__, m_qualifiedName.c_str()); + addTo->Add(new LiteralExpression("/* WriteArrayToParcel error " + + m_qualifiedName + " */")); +} + +void +Type::CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + fprintf(stderr, "aidl:internal error %s:%d qualifiedName=%s\n", + __FILE__, __LINE__, m_qualifiedName.c_str()); + addTo->Add(new LiteralExpression("/* CreateArrayFromParcel error " + + m_qualifiedName + " */")); +} + +void +Type::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + fprintf(stderr, "aidl:internal error %s:%d qualifiedName=%s\n", + __FILE__, __LINE__, m_qualifiedName.c_str()); + addTo->Add(new LiteralExpression("/* ReadArrayFromParcel error " + + m_qualifiedName + " */")); +} + +void +Type::SetQualifiedName(const string& qualified) +{ + m_qualifiedName = qualified; +} + +Expression* +Type::BuildWriteToParcelFlags(int flags) +{ + if (flags == 0) { + return new LiteralExpression("0"); + } + if ((flags&PARCELABLE_WRITE_RETURN_VALUE) != 0) { + return new FieldVariable(PARCELABLE_INTERFACE_TYPE, + "PARCELABLE_WRITE_RETURN_VALUE"); + } + return new LiteralExpression("0"); +} + +// ================================================================ + +BasicType::BasicType(const string& name, const string& marshallMethod, + const string& unmarshallMethod, + const string& writeArray, const string& createArray, + const string& readArray) + :Type(name, BUILT_IN, true, false), + m_marshallMethod(marshallMethod), + m_unmarshallMethod(unmarshallMethod), + m_writeArrayMethod(writeArray), + m_createArrayMethod(createArray), + m_readArrayMethod(readArray) +{ +} + +void +BasicType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, m_marshallMethod, 1, v)); +} + +void +BasicType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, m_unmarshallMethod))); +} + +bool +BasicType::CanBeArray() const +{ + return true; +} + +void +BasicType::WriteArrayToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, m_writeArrayMethod, 1, v)); +} + +void +BasicType::CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, m_createArrayMethod))); +} + +void +BasicType::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + addTo->Add(new MethodCall(parcel, m_readArrayMethod, 1, v)); +} + + +// ================================================================ + +BooleanType::BooleanType() + :Type("boolean", BUILT_IN, true, false) +{ +} + +void +BooleanType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeInt", 1, + new Ternary(v, new LiteralExpression("1"), + new LiteralExpression("0")))); +} + +void +BooleanType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + addTo->Add(new Assignment(v, new Comparison(new LiteralExpression("0"), + "!=", new MethodCall(parcel, "readInt")))); +} + +bool +BooleanType::CanBeArray() const +{ + return true; +} + +void +BooleanType::WriteArrayToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeBooleanArray", 1, v)); +} + +void +BooleanType::CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, "createBooleanArray"))); +} + +void +BooleanType::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + addTo->Add(new MethodCall(parcel, "readBooleanArray", 1, v)); +} + + +// ================================================================ + +CharType::CharType() + :Type("char", BUILT_IN, true, false) +{ +} + +void +CharType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeInt", 1, + new Cast(INT_TYPE, v))); +} + +void +CharType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, "readInt"), this)); +} + +bool +CharType::CanBeArray() const +{ + return true; +} + +void +CharType::WriteArrayToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeCharArray", 1, v)); +} + +void +CharType::CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, "createCharArray"))); +} + +void +CharType::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + addTo->Add(new MethodCall(parcel, "readCharArray", 1, v)); +} + +// ================================================================ + +StringType::StringType() + :Type("java.lang", "String", BUILT_IN, true, false) +{ +} + +string +StringType::CreatorName() const +{ + return "android.os.Parcel.STRING_CREATOR"; +} + +void +StringType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeString", 1, v)); +} + +void +StringType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, "readString"))); +} + +bool +StringType::CanBeArray() const +{ + return true; +} + +void +StringType::WriteArrayToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeStringArray", 1, v)); +} + +void +StringType::CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, "createStringArray"))); +} + +void +StringType::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + addTo->Add(new MethodCall(parcel, "readStringArray", 1, v)); +} + +// ================================================================ + +CharSequenceType::CharSequenceType() + :Type("java.lang", "CharSequence", BUILT_IN, true, false) +{ +} + +string +CharSequenceType::CreatorName() const +{ + return "android.os.Parcel.STRING_CREATOR"; +} + +void +CharSequenceType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + // if (v != null) { + // parcel.writeInt(1); + // v.writeToParcel(parcel); + // } else { + // parcel.writeInt(0); + // } + IfStatement* elsepart = new IfStatement(); + elsepart->statements->Add(new MethodCall(parcel, "writeInt", 1, + new LiteralExpression("0"))); + IfStatement* ifpart = new IfStatement; + ifpart->expression = new Comparison(v, "!=", NULL_VALUE); + ifpart->elseif = elsepart; + ifpart->statements->Add(new MethodCall(parcel, "writeInt", 1, + new LiteralExpression("1"))); + ifpart->statements->Add(new MethodCall(TEXT_UTILS_TYPE, "writeToParcel", + 3, v, parcel, BuildWriteToParcelFlags(flags))); + + addTo->Add(ifpart); +} + +void +CharSequenceType::CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + // if (0 != parcel.readInt()) { + // v = TextUtils.createFromParcel(parcel) + // } else { + // v = null; + // } + IfStatement* elsepart = new IfStatement(); + elsepart->statements->Add(new Assignment(v, NULL_VALUE)); + + IfStatement* ifpart = new IfStatement(); + ifpart->expression = new Comparison(new LiteralExpression("0"), "!=", + new MethodCall(parcel, "readInt")); + ifpart->elseif = elsepart; + ifpart->statements->Add(new Assignment(v, + new MethodCall(TEXT_UTILS_TYPE, + "CHAR_SEQUENCE_CREATOR.createFromParcel", 1, parcel))); + + addTo->Add(ifpart); +} + + +// ================================================================ + +RemoteExceptionType::RemoteExceptionType() + :Type("android.os", "RemoteException", BUILT_IN, false, false) +{ +} + +void +RemoteExceptionType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +void +RemoteExceptionType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +// ================================================================ + +RuntimeExceptionType::RuntimeExceptionType() + :Type("java.lang", "RuntimeException", BUILT_IN, false, false) +{ +} + +void +RuntimeExceptionType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +void +RuntimeExceptionType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + + +// ================================================================ + +IBinderType::IBinderType() + :Type("android.os", "IBinder", BUILT_IN, true, false) +{ +} + +void +IBinderType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeStrongBinder", 1, v)); +} + +void +IBinderType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, "readStrongBinder"))); +} + +void +IBinderType::WriteArrayToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeBinderArray", 1, v)); +} + +void +IBinderType::CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, "createBinderArray"))); +} + +void +IBinderType::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + addTo->Add(new MethodCall(parcel, "readBinderArray", 1, v)); +} + + +// ================================================================ + +IInterfaceType::IInterfaceType() + :Type("android.os", "IInterface", BUILT_IN, false, false) +{ +} + +void +IInterfaceType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +void +IInterfaceType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + + +// ================================================================ + +BinderType::BinderType() + :Type("android.os", "Binder", BUILT_IN, false, false) +{ +} + +void +BinderType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +void +BinderType::CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + + +// ================================================================ + +BinderProxyType::BinderProxyType() + :Type("android.os", "BinderProxy", BUILT_IN, false, false) +{ +} + +void +BinderProxyType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +void +BinderProxyType::CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + + +// ================================================================ + +ParcelType::ParcelType() + :Type("android.os", "Parcel", BUILT_IN, false, false) +{ +} + +void +ParcelType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +void +ParcelType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +// ================================================================ + +ParcelableInterfaceType::ParcelableInterfaceType() + :Type("android.os", "Parcelable", BUILT_IN, false, false) +{ +} + +void +ParcelableInterfaceType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +void +ParcelableInterfaceType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +// ================================================================ + +MapType::MapType() + :Type("java.util", "Map", BUILT_IN, true, true) +{ +} + +void +MapType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeMap", 1, v)); +} + +void +MapType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + Variable *cl = new Variable(CLASSLOADER_TYPE, "cl"); + addTo->Add(new VariableDeclaration(cl, + new LiteralExpression("this.getClass().getClassLoader()"), + CLASSLOADER_TYPE)); + addTo->Add(new Assignment(v, new MethodCall(parcel, "readHashMap", 1, cl))); +} + +void +MapType::ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + Variable *cl = new Variable(CLASSLOADER_TYPE, "cl"); + addTo->Add(new VariableDeclaration(cl, + new LiteralExpression("this.getClass().getClassLoader()"), + CLASSLOADER_TYPE)); + addTo->Add(new MethodCall(parcel, "readMap", 2, v, cl)); +} + + +// ================================================================ + +ListType::ListType() + :Type("java.util", "List", BUILT_IN, true, true) +{ +} + +string +ListType::InstantiableName() const +{ + return "java.util.ArrayList"; +} + +void +ListType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeList", 1, v)); +} + +void +ListType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + Variable *cl = new Variable(CLASSLOADER_TYPE, "cl"); + addTo->Add(new VariableDeclaration(cl, + new LiteralExpression("this.getClass().getClassLoader()"), + CLASSLOADER_TYPE)); + addTo->Add(new Assignment(v, new MethodCall(parcel, "readArrayList", 1, cl))); +} + +void +ListType::ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + Variable *cl = new Variable(CLASSLOADER_TYPE, "cl"); + addTo->Add(new VariableDeclaration(cl, + new LiteralExpression("this.getClass().getClassLoader()"), + CLASSLOADER_TYPE)); + addTo->Add(new MethodCall(parcel, "readList", 2, v, cl)); +} + + +// ================================================================ + +ParcelableType::ParcelableType(const string& package, const string& name, + bool builtIn, const string& declFile, int declLine) + :Type(package, name, builtIn ? BUILT_IN : PARCELABLE, true, true, + declFile, declLine) +{ +} + +string +ParcelableType::CreatorName() const +{ + return QualifiedName() + ".CREATOR"; +} + +void +ParcelableType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + // if (v != null) { + // parcel.writeInt(1); + // v.writeToParcel(parcel); + // } else { + // parcel.writeInt(0); + // } + IfStatement* elsepart = new IfStatement(); + elsepart->statements->Add(new MethodCall(parcel, "writeInt", 1, + new LiteralExpression("0"))); + IfStatement* ifpart = new IfStatement; + ifpart->expression = new Comparison(v, "!=", NULL_VALUE); + ifpart->elseif = elsepart; + ifpart->statements->Add(new MethodCall(parcel, "writeInt", 1, + new LiteralExpression("1"))); + ifpart->statements->Add(new MethodCall(v, "writeToParcel", 2, + parcel, BuildWriteToParcelFlags(flags))); + + addTo->Add(ifpart); +} + +void +ParcelableType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + // if (0 != parcel.readInt()) { + // v = CLASS.CREATOR.createFromParcel(parcel) + // } else { + // v = null; + // } + IfStatement* elsepart = new IfStatement(); + elsepart->statements->Add(new Assignment(v, NULL_VALUE)); + + IfStatement* ifpart = new IfStatement(); + ifpart->expression = new Comparison(new LiteralExpression("0"), "!=", + new MethodCall(parcel, "readInt")); + ifpart->elseif = elsepart; + ifpart->statements->Add(new Assignment(v, + new MethodCall(v->type, "CREATOR.createFromParcel", 1, parcel))); + + addTo->Add(ifpart); +} + +void +ParcelableType::ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + // TODO: really, we don't need to have this extra check, but we + // don't have two separate marshalling code paths + // if (0 != parcel.readInt()) { + // v.readFromParcel(parcel) + // } + IfStatement* ifpart = new IfStatement(); + ifpart->expression = new Comparison(new LiteralExpression("0"), "!=", + new MethodCall(parcel, "readInt")); + ifpart->statements->Add(new MethodCall(v, "readFromParcel", 1, parcel)); + addTo->Add(ifpart); +} + +bool +ParcelableType::CanBeArray() const +{ + return true; +} + +void +ParcelableType::WriteArrayToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeTypedArray", 2, v, + BuildWriteToParcelFlags(flags))); +} + +void +ParcelableType::CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + string creator = v->type->QualifiedName() + ".CREATOR"; + addTo->Add(new Assignment(v, new MethodCall(parcel, + "createTypedArray", 1, new LiteralExpression(creator)))); +} + +void +ParcelableType::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + string creator = v->type->QualifiedName() + ".CREATOR"; + addTo->Add(new MethodCall(parcel, "readTypedArray", 2, + v, new LiteralExpression(creator))); +} + + +// ================================================================ + +InterfaceType::InterfaceType(const string& package, const string& name, + bool builtIn, bool oneway, + const string& declFile, int declLine) + :Type(package, name, builtIn ? BUILT_IN : INTERFACE, true, false, + declFile, declLine) + ,m_oneway(oneway) +{ +} + +bool +InterfaceType::OneWay() const +{ + return m_oneway; +} + +void +InterfaceType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + // parcel.writeStrongBinder(v != null ? v.asBinder() : null); + addTo->Add(new MethodCall(parcel, "writeStrongBinder", 1, + new Ternary( + new Comparison(v, "!=", NULL_VALUE), + new MethodCall(v, "asBinder"), + NULL_VALUE))); +} + +void +InterfaceType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + // v = Interface.asInterface(parcel.readStrongBinder()); + string type = v->type->QualifiedName(); + type += ".Stub"; + addTo->Add(new Assignment(v, + new MethodCall( NAMES.Find(type), "asInterface", 1, + new MethodCall(parcel, "readStrongBinder")))); +} + + +// ================================================================ + +GenericType::GenericType(const string& package, const string& name, + const vector<Type*>& args) + :Type(package, name, BUILT_IN, true, true) +{ + m_args = args; + + m_importName = package + '.' + name; + + string gen = "<"; + int N = args.size(); + for (int i=0; i<N; i++) { + Type* t = args[i]; + gen += t->QualifiedName(); + if (i != N-1) { + gen += ','; + } + } + gen += '>'; + m_genericArguments = gen; + SetQualifiedName(m_importName + gen); +} + +string +GenericType::GenericArguments() const +{ + return m_genericArguments; +} + +string +GenericType::ImportType() const +{ + return m_importName; +} + +void +GenericType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + fprintf(stderr, "implement GenericType::WriteToParcel\n"); +} + +void +GenericType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + fprintf(stderr, "implement GenericType::CreateFromParcel\n"); +} + +void +GenericType::ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + fprintf(stderr, "implement GenericType::ReadFromParcel\n"); +} + + +// ================================================================ + +GenericListType::GenericListType(const string& package, const string& name, + const vector<Type*>& args) + :GenericType(package, name, args), + m_creator(args[0]->CreatorName()) +{ +} + +string +GenericListType::CreatorName() const +{ + return "android.os.Parcel.arrayListCreator"; +} + +string +GenericListType::InstantiableName() const +{ + return "java.util.ArrayList" + GenericArguments(); +} + +void +GenericListType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + if (m_creator == STRING_TYPE->CreatorName()) { + addTo->Add(new MethodCall(parcel, "writeStringList", 1, v)); + } else if (m_creator == IBINDER_TYPE->CreatorName()) { + addTo->Add(new MethodCall(parcel, "writeBinderList", 1, v)); + } else { + // parcel.writeTypedListXX(arg); + addTo->Add(new MethodCall(parcel, "writeTypedList", 1, v)); + } +} + +void +GenericListType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel) +{ + if (m_creator == STRING_TYPE->CreatorName()) { + addTo->Add(new Assignment(v, + new MethodCall(parcel, "createStringArrayList", 0))); + } else if (m_creator == IBINDER_TYPE->CreatorName()) { + addTo->Add(new Assignment(v, + new MethodCall(parcel, "createBinderArrayList", 0))); + } else { + // v = _data.readTypedArrayList(XXX.creator); + addTo->Add(new Assignment(v, + new MethodCall(parcel, "createTypedArrayList", 1, + new LiteralExpression(m_creator)))); + } +} + +void +GenericListType::ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + if (m_creator == STRING_TYPE->CreatorName()) { + addTo->Add(new MethodCall(parcel, "readStringList", 1, v)); + } else if (m_creator == IBINDER_TYPE->CreatorName()) { + addTo->Add(new MethodCall(parcel, "readBinderList", 1, v)); + } else { + // v = _data.readTypedList(v, XXX.creator); + addTo->Add(new MethodCall(parcel, "readTypedList", 2, + v, + new LiteralExpression(m_creator))); + } +} + +// ================================================================ + +ClassLoaderType::ClassLoaderType() + :Type("java.lang", "ClassLoader", BUILT_IN, false, false) +{ +} + + +// ================================================================ + +Namespace::Namespace() +{ +} + +Namespace::~Namespace() +{ + int N = m_types.size(); + for (int i=0; i<N; i++) { + delete m_types[i]; + } +} + +void +Namespace::Add(Type* type) +{ + Type* t = Find(type->QualifiedName()); + if (t == NULL) { + m_types.push_back(type); + } +} + +void +Namespace::AddGenericType(const string& package, const string& name, int args) +{ + Generic g; + g.package = package; + g.name = name; + g.qualified = package + '.' + name; + g.args = args; + m_generics.push_back(g); +} + +Type* +Namespace::Find(const string& name) const +{ + int N = m_types.size(); + for (int i=0; i<N; i++) { + if (m_types[i]->QualifiedName() == name) { + return m_types[i]; + } + } + return NULL; +} + +Type* +Namespace::Find(const char* package, const char* name) const +{ + string s; + if (package != NULL) { + s += package; + s += '.'; + } + s += name; + return Find(s); +} + +static string +normalize_generic(const string& s) +{ + string r; + int N = s.size(); + for (int i=0; i<N; i++) { + char c = s[i]; + if (!isspace(c)) { + r += c; + } + } + return r; +} + +Type* +Namespace::Search(const string& name) +{ + // an exact match wins + Type* result = Find(name); + if (result != NULL) { + return result; + } + + // try the class names + // our language doesn't allow you to not specify outer classes + // when referencing an inner class. that could be changed, and this + // would be the place to do it, but I don't think the complexity in + // scoping rules is worth it. + int N = m_types.size(); + for (int i=0; i<N; i++) { + if (m_types[i]->Name() == name) { + return m_types[i]; + } + } + + // we got to here and it's not a generic, give up + if (name.find('<') == name.npos) { + return NULL; + } + + // remove any whitespace + string normalized = normalize_generic(name); + + // find the part before the '<', find a generic for it + ssize_t baseIndex = normalized.find('<'); + string base(normalized.c_str(), baseIndex); + const Generic* g = search_generic(base); + if (g == NULL) { + return NULL; + } + + // For each of the args, do a recursive search on it. We don't allow + // generics within generics like Java does, because we're really limiting + // them to just built-in container classes, at least for now. Our syntax + // ensures this right now as well. + vector<Type*> args; + size_t start = baseIndex + 1; + size_t end = start; + while (normalized[start] != '\0') { + end = normalized.find(',', start); + if (end == normalized.npos) { + end = normalized.find('>', start); + } + string s(normalized.c_str()+start, end-start); + Type* t = this->Search(s); + if (t == NULL) { + // maybe we should print a warning here? + return NULL; + } + args.push_back(t); + start = end+1; + } + + // construct a GenericType, add it to our name set so they always get + // the same object, and return it. + result = make_generic_type(g->package, g->name, args); + if (result == NULL) { + return NULL; + } + + this->Add(result); + return this->Find(result->QualifiedName()); +} + +const Namespace::Generic* +Namespace::search_generic(const string& name) const +{ + int N = m_generics.size(); + + // first exact match + for (int i=0; i<N; i++) { + const Generic& g = m_generics[i]; + if (g.qualified == name) { + return &g; + } + } + + // then name match + for (int i=0; i<N; i++) { + const Generic& g = m_generics[i]; + if (g.name == name) { + return &g; + } + } + + return NULL; +} + +void +Namespace::Dump() const +{ + int n = m_types.size(); + for (int i=0; i<n; i++) { + Type* t = m_types[i]; + printf("type: package=%s name=%s qualifiedName=%s\n", + t->Package().c_str(), t->Name().c_str(), + t->QualifiedName().c_str()); + } +} diff --git a/tools/aidl/Type.h b/tools/aidl/Type.h new file mode 100755 index 0000000..2ea3ac9 --- /dev/null +++ b/tools/aidl/Type.h @@ -0,0 +1,466 @@ +#ifndef AIDL_TYPE_H +#define AIDL_TYPE_H + +#include "AST.h" +#include <string> +#include <vector> + +using namespace std; + +class Type +{ +public: + // kinds + enum { + BUILT_IN, + PARCELABLE, + INTERFACE, + GENERATED + }; + + // WriteToParcel flags + enum { + PARCELABLE_WRITE_RETURN_VALUE = 0x0001 + }; + + Type(const string& name, int kind, bool canWriteToParcel, + bool canBeOut); + Type(const string& package, const string& name, + int kind, bool canWriteToParcel, bool canBeOut, + const string& declFile = "", int declLine = -1); + virtual ~Type(); + + inline string Package() const { return m_package; } + inline string Name() const { return m_name; } + inline string QualifiedName() const { return m_qualifiedName; } + inline int Kind() const { return m_kind; } + inline string DeclFile() const { return m_declFile; } + inline int DeclLine() const { return m_declLine; } + inline bool CanBeMarshalled() const { return m_canWriteToParcel; } + inline bool CanBeOutParameter() const { return m_canBeOut; } + + virtual string ImportType() const; + virtual string CreatorName() const; + virtual string InstantiableName() const; + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + + virtual bool CanBeArray() const; + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + +protected: + void SetQualifiedName(const string& qualified); + Expression* BuildWriteToParcelFlags(int flags); + +private: + Type(); + Type(const Type&); + + string m_package; + string m_name; + string m_qualifiedName; + string m_declFile; + int m_declLine; + int m_kind; + bool m_canWriteToParcel; + bool m_canBeOut; +}; + +class BasicType : public Type +{ +public: + BasicType(const string& name, const string& marshallMethod, + const string& unmarshallMethod, + const string& writeArray, + const string& createArray, + const string& readArray); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + + virtual bool CanBeArray() const; + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + +private: + string m_marshallMethod; + string m_unmarshallMethod; + string m_writeArrayMethod; + string m_createArrayMethod; + string m_readArrayMethod; +}; + +class BooleanType : public Type +{ +public: + BooleanType(); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + + virtual bool CanBeArray() const; + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class CharType : public Type +{ +public: + CharType(); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + + virtual bool CanBeArray() const; + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + + +class StringType : public Type +{ +public: + StringType(); + + virtual string CreatorName() const; + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + + virtual bool CanBeArray() const; + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class CharSequenceType : public Type +{ +public: + CharSequenceType(); + + virtual string CreatorName() const; + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class RemoteExceptionType : public Type +{ +public: + RemoteExceptionType(); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class RuntimeExceptionType : public Type +{ +public: + RuntimeExceptionType(); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class IBinderType : public Type +{ +public: + IBinderType(); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class IInterfaceType : public Type +{ +public: + IInterfaceType(); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class BinderType : public Type +{ +public: + BinderType(); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class BinderProxyType : public Type +{ +public: + BinderProxyType(); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class ParcelType : public Type +{ +public: + ParcelType(); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class ParcelableInterfaceType : public Type +{ +public: + ParcelableInterfaceType(); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class MapType : public Type +{ +public: + MapType(); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class ListType : public Type +{ +public: + ListType(); + + virtual string InstantiableName() const; + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class ParcelableType : public Type +{ +public: + ParcelableType(const string& package, const string& name, + bool builtIn, const string& declFile, int declLine); + + virtual string CreatorName() const; + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + + virtual bool CanBeArray() const; + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); +}; + +class InterfaceType : public Type +{ +public: + InterfaceType(const string& package, const string& name, + bool builtIn, bool oneway, + const string& declFile, int declLine); + + bool OneWay() const; + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + +private: + bool m_oneway; +}; + + +class GenericType : public Type +{ +public: + GenericType(const string& package, const string& name, + const vector<Type*>& args); + + string GenericArguments() const; + + virtual string ImportType() const; + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + +private: + string m_genericArguments; + string m_importName; + vector<Type*> m_args; +}; + + +class GenericListType : public GenericType +{ +public: + GenericListType(const string& package, const string& name, + const vector<Type*>& args); + + virtual string CreatorName() const; + virtual string InstantiableName() const; + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + virtual void ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel); + +private: + string m_creator; +}; + +class ClassLoaderType : public Type +{ +public: + ClassLoaderType(); +}; + +class Namespace +{ +public: + Namespace(); + ~Namespace(); + void Add(Type* type); + + // args is the number of template types (what is this called?) + void AddGenericType(const string& package, const string& name, int args); + + // lookup a specific class name + Type* Find(const string& name) const; + Type* Find(const char* package, const char* name) const; + + // try to search by either a full name or a partial name + Type* Search(const string& name); + + void Dump() const; + +private: + struct Generic { + string package; + string name; + string qualified; + int args; + }; + + const Generic* search_generic(const string& name) const; + + vector<Type*> m_types; + vector<Generic> m_generics; +}; + +extern Namespace NAMES; + +extern Type* VOID_TYPE; +extern Type* BOOLEAN_TYPE; +extern Type* CHAR_TYPE; +extern Type* INT_TYPE; +extern Type* LONG_TYPE; +extern Type* FLOAT_TYPE; +extern Type* DOUBLE_TYPE; +extern Type* STRING_TYPE; +extern Type* CHAR_SEQUENCE_TYPE; +extern Type* TEXT_UTILS_TYPE; +extern Type* REMOTE_EXCEPTION_TYPE; +extern Type* RUNTIME_EXCEPTION_TYPE; +extern Type* IBINDER_TYPE; +extern Type* IINTERFACE_TYPE; +extern Type* BINDER_NATIVE_TYPE; +extern Type* BINDER_PROXY_TYPE; +extern Type* PARCEL_TYPE; +extern Type* PARCELABLE_INTERFACE_TYPE; + +extern Expression* NULL_VALUE; +extern Expression* THIS_VALUE; +extern Expression* SUPER_VALUE; +extern Expression* TRUE_VALUE; +extern Expression* FALSE_VALUE; + +void register_base_types(); + +#endif // AIDL_TYPE_H diff --git a/tools/aidl/aidl.cpp b/tools/aidl/aidl.cpp new file mode 100644 index 0000000..dc61567 --- /dev/null +++ b/tools/aidl/aidl.cpp @@ -0,0 +1,904 @@ + +#include "aidl_language.h" +#include "options.h" +#include "search_path.h" +#include "Type.h" +#include "generate_java.h" +#include <unistd.h> +#include <fcntl.h> +#include <sys/param.h> +#include <sys/stat.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <map> + +#ifdef HAVE_MS_C_RUNTIME +#include <io.h> +#include <sys/stat.h> +#endif + +#ifndef O_BINARY +# define O_BINARY 0 +#endif + +using namespace std; + +static void +test_document(document_item_type* d) +{ + while (d) { + if (d->item_type == INTERFACE_TYPE) { + interface_type* c = (interface_type*)d; + printf("interface %s %s {\n", c->package, c->name.data); + interface_item_type *q = (interface_item_type*)c->interface_items; + while (q) { + if (q->item_type == METHOD_TYPE) { + method_type *m = (method_type*)q; + printf(" %s %s(", m->type.type.data, m->name.data); + arg_type *p = m->args; + while (p) { + printf("%s %s",p->type.type.data,p->name.data); + if (p->next) printf(", "); + p=p->next; + } + printf(")"); + printf(";\n"); + } + q=q->next; + } + printf("}\n"); + } + else if (d->item_type == PARCELABLE_TYPE) { + parcelable_type* b = (parcelable_type*)d; + printf("parcelable %s %s;\n", b->package, b->name.data); + } + else { + printf("UNKNOWN d=0x%08x d->item_type=%ld\n", (long)d, d->item_type); + } + d = d->next; + } +} + +// ========================================================== +int +convert_direction(const char* direction) +{ + if (direction == NULL) { + return IN_PARAMETER; + } + if (0 == strcmp(direction, "in")) { + return IN_PARAMETER; + } + if (0 == strcmp(direction, "out")) { + return OUT_PARAMETER; + } + return INOUT_PARAMETER; +} + +// ========================================================== +struct import_info { + const char* from; + const char* filename; + buffer_type statement; + const char* neededClass; + document_item_type* doc; + struct import_info* next; +}; + +document_item_type* g_document = NULL; +import_info* g_imports = NULL; + +static void +main_document_parsed(document_item_type* d) +{ + g_document = d; +} + +static void +main_import_parsed(buffer_type* statement) +{ + import_info* import = (import_info*)malloc(sizeof(import_info)); + memset(import, 0, sizeof(import_info)); + import->from = strdup(g_currentFilename); + import->statement.lineno = statement->lineno; + import->statement.data = strdup(statement->data); + import->statement.extra = NULL; + import->next = g_imports; + import->neededClass = parse_import_statement(statement->data); + g_imports = import; +} + +static ParserCallbacks g_mainCallbacks = { + &main_document_parsed, + &main_import_parsed +}; + +char* +parse_import_statement(const char* text) +{ + const char* end; + int len; + + while (isspace(*text)) { + text++; + } + while (!isspace(*text)) { + text++; + } + while (isspace(*text)) { + text++; + } + end = text; + while (!isspace(*end) && *end != ';') { + end++; + } + len = end-text; + + char* rv = (char*)malloc(len+1); + memcpy(rv, text, len); + rv[len] = '\0'; + + return rv; +} + +// ========================================================== +static void +import_import_parsed(buffer_type* statement) +{ +} + +static ParserCallbacks g_importCallbacks = { + &main_document_parsed, + &import_import_parsed +}; + +// ========================================================== +static int +check_filename(const char* filename, const char* package, buffer_type* name) +{ + const char* p; + string expected; + string fn; + size_t len; + char cwd[MAXPATHLEN]; + bool valid = false; + +#ifdef HAVE_WINDOWS_PATHS + if (isalpha(filename[0]) && filename[1] == ':' + && filename[2] == OS_PATH_SEPARATOR) { +#else + if (filename[0] == OS_PATH_SEPARATOR) { +#endif + fn = filename; + } else { + fn = getcwd(cwd, sizeof(cwd)); + len = fn.length(); + if (fn[len-1] != OS_PATH_SEPARATOR) { + fn += OS_PATH_SEPARATOR; + } + fn += filename; + } + + if (package) { + expected = package; + expected += '.'; + } + + len = expected.length(); + for (size_t i=0; i<len; i++) { + if (expected[i] == '.') { + expected[i] = OS_PATH_SEPARATOR; + } + } + + p = strchr(name->data, '.'); + len = p ? p-name->data : strlen(name->data); + expected.append(name->data, len); + + expected += ".aidl"; + + len = fn.length(); + valid = (len >= expected.length()); + + if (valid) { + p = fn.c_str() + (len - expected.length()); + +#ifdef HAVE_WINDOWS_PATHS + if (OS_PATH_SEPARATOR != '/') { + // Input filename under cygwin most likely has / separators + // whereas the expected string uses \\ separators. Adjust + // them accordingly. + for (char *c = const_cast<char *>(p); *c; ++c) { + if (*c == '/') *c = OS_PATH_SEPARATOR; + } + } +#endif + +#ifdef OS_CASE_SENSITIVE + valid = (expected == p); +#else + valid = !strcasecmp(expected.c_str(), p); +#endif + } + + if (!valid) { + fprintf(stderr, "%s:%d interface %s should be declared in a file" + " called %s.\n", + filename, name->lineno, name->data, expected.c_str()); + return 1; + } + + return 0; +} + +static int +check_filenames(const char* filename, document_item_type* items) +{ + int err = 0; + while (items) { + if (items->item_type == PARCELABLE_TYPE) { + parcelable_type* p = (parcelable_type*)items; + err |= check_filename(filename, p->package, &p->name); + } + else if (items->item_type == INTERFACE_TYPE) { + interface_type* c = (interface_type*)items; + err |= check_filename(filename, c->package, &c->name); + } + else { + fprintf(stderr, "aidl: internal error unkown document type %d.\n", + items->item_type); + return 1; + } + items = items->next; + } + return err; +} + +// ========================================================== +static const char* +kind_to_string(int kind) +{ + switch (kind) + { + case Type::INTERFACE: + return "an interface"; + case Type::PARCELABLE: + return "a parcelable"; + default: + return "ERROR"; + } +} + +static char* +rfind(char* str, char c) +{ + char* p = str + strlen(str) - 1; + while (p >= str) { + if (*p == c) { + return p; + } + p--; + } + return NULL; +} + +static int +gather_types(const char* filename, document_item_type* items) +{ + int err = 0; + while (items) { + Type* type; + if (items->item_type == PARCELABLE_TYPE) { + parcelable_type* p = (parcelable_type*)items; + type = new ParcelableType(p->package ? p->package : "", + p->name.data, false, filename, p->name.lineno); + } + else if (items->item_type == INTERFACE_TYPE) { + interface_type* c = (interface_type*)items; + type = new InterfaceType(c->package ? c->package : "", + c->name.data, false, c->oneway, + filename, c->name.lineno); + } + else { + fprintf(stderr, "aidl: internal error %s:%d\n", __FILE__, __LINE__); + return 1; + } + + Type* old = NAMES.Find(type->QualifiedName()); + if (old == NULL) { + NAMES.Add(type); + + if (items->item_type == INTERFACE_TYPE) { + // for interfaces, also add the stub and proxy types, we don't + // bother checking these for duplicates, because the parser + // won't let us do it. + interface_type* c = (interface_type*)items; + + string name = c->name.data; + name += ".Stub"; + Type* stub = new Type(c->package ? c->package : "", + name, Type::GENERATED, false, false, + filename, c->name.lineno); + NAMES.Add(stub); + + name = c->name.data; + name += ".Stub.Proxy"; + Type* proxy = new Type(c->package ? c->package : "", + name, Type::GENERATED, false, false, + filename, c->name.lineno); + NAMES.Add(proxy); + } + } else { + if (old->Kind() == Type::BUILT_IN) { + fprintf(stderr, "%s:%d attempt to redefine built in class %s\n", + filename, type->DeclLine(), + type->QualifiedName().c_str()); + err = 1; + } + else if (type->Kind() != old->Kind()) { + const char* oldKind = kind_to_string(old->Kind()); + const char* newKind = kind_to_string(type->Kind()); + + fprintf(stderr, "%s:%d attempt to redefine %s as %s,\n", + filename, type->DeclLine(), + type->QualifiedName().c_str(), newKind); + fprintf(stderr, "%s:%d previously defined here as %s.\n", + old->DeclFile().c_str(), old->DeclLine(), oldKind); + err = 1; + } + } + + items = items->next; + } + return err; +} + +// ========================================================== +static bool +matches_keyword(const char* str) +{ + static const char* KEYWORDS[] = { "abstract", "assert", "boolean", "break", + "byte", "case", "catch", "char", "class", "const", "continue", + "default", "do", "double", "else", "enum", "extends", "final", + "finally", "float", "for", "goto", "if", "implements", "import", + "instanceof", "int", "interface", "long", "native", "new", "package", + "private", "protected", "public", "return", "short", "static", + "strictfp", "super", "switch", "synchronized", "this", "throw", + "throws", "transient", "try", "void", "volatile", "while", + "true", "false", "null", + NULL + }; + const char** k = KEYWORDS; + while (*k) { + if (0 == strcmp(str, *k)) { + return true; + } + k++; + } + return false; +} + +static int +check_method(const char* filename, method_type* m) +{ + int err = 0; + + // return type + Type* returnType = NAMES.Search(m->type.type.data); + if (returnType == NULL) { + fprintf(stderr, "%s:%d unknown return type %s\n", filename, + m->type.type.lineno, m->type.type.data); + err = 1; + return err; + } + + if (!returnType->CanBeMarshalled()) { + fprintf(stderr, "%s:%d return type %s can't be marshalled.\n", filename, + m->type.type.lineno, m->type.type.data); + err = 1; + } + + if (m->type.dimension > 0 && !returnType->CanBeArray()) { + fprintf(stderr, "%s:%d return type %s%s can't be an array.\n", filename, + m->type.array_token.lineno, m->type.type.data, + m->type.array_token.data); + err = 1; + } + + if (m->type.dimension > 1) { + fprintf(stderr, "%s:%d return type %s%s only one" + " dimensional arrays are supported\n", filename, + m->type.array_token.lineno, m->type.type.data, + m->type.array_token.data); + err = 1; + } + + int index = 1; + + arg_type* arg = m->args; + while (arg) { + Type* t = NAMES.Search(arg->type.type.data); + + // check the arg type + if (t == NULL) { + fprintf(stderr, "%s:%d parameter %s (%d) unknown type %s\n", + filename, m->type.type.lineno, arg->name.data, index, + arg->type.type.data); + err = 1; + goto next; + } + + if (!t->CanBeMarshalled()) { + fprintf(stderr, "%s:%d parameter %d: '%s %s' can't be marshalled.\n", + filename, m->type.type.lineno, index, + arg->type.type.data, arg->name.data); + err = 1; + } + + if (arg->direction.data == NULL + && (arg->type.dimension != 0 || t->CanBeOutParameter())) { + fprintf(stderr, "%s:%d parameter %d: '%s %s' can be an out" + " parameter, so you must declare it as in," + " out or inout.\n", + filename, m->type.type.lineno, index, + arg->type.type.data, arg->name.data); + err = 1; + } + + if (convert_direction(arg->direction.data) != IN_PARAMETER + && !t->CanBeOutParameter() + && arg->type.dimension == 0) { + fprintf(stderr, "%s:%d parameter %d: '%s %s %s' can only be an in" + " parameter.\n", + filename, m->type.type.lineno, index, + arg->direction.data, arg->type.type.data, + arg->name.data); + err = 1; + } + + if (arg->type.dimension > 0 && !t->CanBeArray()) { + fprintf(stderr, "%s:%d parameter %d: '%s %s%s %s' can't be an" + " array.\n", filename, + m->type.array_token.lineno, index, arg->direction.data, + arg->type.type.data, arg->type.array_token.data, + arg->name.data); + err = 1; + } + + if (arg->type.dimension > 1) { + fprintf(stderr, "%s:%d parameter %d: '%s %s%s %s' only one" + " dimensional arrays are supported\n", filename, + m->type.array_token.lineno, index, arg->direction.data, + arg->type.type.data, arg->type.array_token.data, + arg->name.data); + err = 1; + } + + // check that the name doesn't match a keyword + if (matches_keyword(arg->name.data)) { + fprintf(stderr, "%s:%d parameter %d %s is named the same as a" + " Java keyword\n", + filename, m->name.lineno, index, arg->name.data); + err = 1; + } + +next: + index++; + arg = arg->next; + } + + return err; +} + +static int +check_types(const char* filename, document_item_type* items) +{ + int err = 0; + while (items) { + // (nothing to check for PARCELABLE_TYPE) + if (items->item_type == INTERFACE_TYPE) { + map<string,method_type*> methodNames; + interface_type* c = (interface_type*)items; + + interface_item_type* member = c->interface_items; + while (member) { + if (member->item_type == METHOD_TYPE) { + method_type* m = (method_type*)member; + + err |= check_method(filename, m); + + // prevent duplicate methods + if (methodNames.find(m->name.data) == methodNames.end()) { + methodNames[m->name.data] = m; + } else { + fprintf(stderr,"%s:%d attempt to redefine method %s,\n", + filename, m->name.lineno, m->name.data); + method_type* old = methodNames[m->name.data]; + fprintf(stderr, "%s:%d previously defined here.\n", + filename, old->name.lineno); + err = 1; + } + } + member = member->next; + } + } + + items = items->next; + } + return err; +} + +// ========================================================== +static int +exactly_one_interface(const char* filename, const document_item_type* items, const Options& options, + bool* onlyParcelable) +{ + if (items == NULL) { + fprintf(stderr, "%s: file does not contain any interfaces\n", + filename); + return 1; + } + + const document_item_type* next = items->next; + if (items->next != NULL) { + int lineno = -1; + if (next->item_type == INTERFACE_TYPE) { + lineno = ((interface_type*)next)->interface_token.lineno; + } + else if (next->item_type == PARCELABLE_TYPE) { + lineno = ((parcelable_type*)next)->parcelable_token.lineno; + } + fprintf(stderr, "%s:%d aidl can only handle one interface per file\n", + filename, lineno); + return 1; + } + + if (items->item_type == PARCELABLE_TYPE) { + *onlyParcelable = true; + if (options.failOnParcelable) { + fprintf(stderr, "%s:%d aidl can only generate code for interfaces, not" + " parcelables,\n", filename, + ((parcelable_type*)items)->parcelable_token.lineno); + fprintf(stderr, "%s:%d .aidl files that only declare parcelables " + "don't need to go in the Makefile.\n", filename, + ((parcelable_type*)items)->parcelable_token.lineno); + return 1; + } + } else { + *onlyParcelable = false; + } + + return 0; +} + +// ========================================================== +void +generate_dep_file(const Options& options) +{ + /* we open the file in binary mode to ensure that the same output is + * generated on all platforms !! + */ + FILE* to = fopen(options.depFileName.c_str(), "wb"); + if (to == NULL) { + return; + } + + const char* slash = "\\"; + import_info* import = g_imports; + if (import == NULL) { + slash = ""; + } + + fprintf(to, "%s: \\\n", options.outputFileName.c_str()); + fprintf(to, " %s %s\n", options.inputFileName.c_str(), slash); + + while (import) { + if (import->next == NULL) { + slash = ""; + } + if (import->filename) { + fprintf(to, " %s %s\n", import->filename, slash); + } + import = import->next; + } + + fprintf(to, "\n"); + + fclose(to); +} + +// ========================================================== +static int +parse_preprocessed_file(const string& filename) +{ + int err; + + FILE* f = fopen(filename.c_str(), "rb"); + if (f == NULL) { + fprintf(stderr, "aidl: can't open preprocessd file: %s\n", + filename.c_str()); + return 1; + } + + int lineno = 1; + char line[1024]; + char type[1024]; + char fullname[1024]; + while (fgets(line, sizeof(line), f)) { + // skip comments and empty lines + if (!line[0] || strncmp(line, "//", 2) == 0) { + continue; + } + + sscanf(line, "%s %[^; \r\n\t];", type, fullname); + + char* packagename; + char* classname = rfind(fullname, '.'); + if (classname != NULL) { + *classname = '\0'; + classname++; + packagename = fullname; + } else { + classname = fullname; + packagename = NULL; + } + + //printf("%s:%d:...%s...%s...%s...\n", filename.c_str(), lineno, + // type, packagename, classname); + document_item_type* doc; + + if (0 == strcmp("parcelable", type)) { + parcelable_type* parcl = (parcelable_type*)malloc( + sizeof(parcelable_type)); + memset(parcl, 0, sizeof(parcelable_type)); + parcl->document_item.item_type = PARCELABLE_TYPE; + parcl->parcelable_token.lineno = lineno; + parcl->parcelable_token.data = strdup(type); + parcl->package = packagename ? strdup(packagename) : NULL; + parcl->name.lineno = lineno; + parcl->name.data = strdup(classname); + parcl->semicolon_token.lineno = lineno; + parcl->semicolon_token.data = strdup(";"); + doc = (document_item_type*)parcl; + } + else if (0 == strcmp("interface", type)) { + interface_type* iface = (interface_type*)malloc( + sizeof(interface_type)); + memset(iface, 0, sizeof(interface_type)); + iface->document_item.item_type = INTERFACE_TYPE; + iface->interface_token.lineno = lineno; + iface->interface_token.data = strdup(type); + iface->package = packagename ? strdup(packagename) : NULL; + iface->name.lineno = lineno; + iface->name.data = strdup(classname); + iface->open_brace_token.lineno = lineno; + iface->open_brace_token.data = strdup("{"); + iface->close_brace_token.lineno = lineno; + iface->close_brace_token.data = strdup("}"); + doc = (document_item_type*)iface; + } + else { + fprintf(stderr, "%s:%d: bad type in line: %s\n", + filename.c_str(), lineno, line); + return 1; + } + err = gather_types(filename.c_str(), doc); + lineno++; + } + + if (!feof(f)) { + fprintf(stderr, "%s:%d: error reading file, line to long.\n", + filename.c_str(), lineno); + return 1; + } + + fclose(f); + return 0; +} + +// ========================================================== +static int +compile_aidl(const Options& options) +{ + int err = 0, N; + + set_import_paths(options.importPaths); + + register_base_types(); + + // import the preprocessed file + N = options.preprocessedFiles.size(); + for (int i=0; i<N; i++) { + const string& s = options.preprocessedFiles[i]; + err |= parse_preprocessed_file(s); + } + if (err != 0) { + return err; + } + + // parse the main file + g_callbacks = &g_mainCallbacks; + err = parse_aidl(options.inputFileName.c_str()); + document_item_type* mainDoc = g_document; + g_document = NULL; + + // parse the imports + g_callbacks = &g_mainCallbacks; + import_info* import = g_imports; + while (import) { + if (NAMES.Find(import->neededClass) == NULL) { + import->filename = find_import_file(import->neededClass); + if (!import->filename) { + fprintf(stderr, "%s:%d: couldn't find import for class %s\n", + import->from, import->statement.lineno, + import->neededClass); + err |= 1; + } else { + err |= parse_aidl(import->filename); + import->doc = g_document; + if (import->doc == NULL) { + err |= 1; + } + } + } + import = import->next; + } + // bail out now if parsing wasn't successful + if (err != 0 || mainDoc == NULL) { + //fprintf(stderr, "aidl: parsing failed, stopping.\n"); + return 1; + } + + // complain about ones that aren't in the right files + err |= check_filenames(options.inputFileName.c_str(), mainDoc); + import = g_imports; + while (import) { + err |= check_filenames(import->filename, import->doc); + import = import->next; + } + + // gather the types that have been declared + err |= gather_types(options.inputFileName.c_str(), mainDoc); + import = g_imports; + while (import) { + err |= gather_types(import->filename, import->doc); + import = import->next; + } + +#if 0 + printf("---- main doc ----\n"); + test_document(mainDoc); + + import = g_imports; + while (import) { + printf("---- import doc ----\n"); + test_document(import->doc); + import = import->next; + } + NAMES.Dump(); +#endif + + // check the referenced types in mainDoc to make sure we've imported them + err |= check_types(options.inputFileName.c_str(), mainDoc); + + // finally, there really only needs to be one thing in mainDoc, and it + // needs to be an interface. + bool onlyParcelable = false; + err |= exactly_one_interface(options.inputFileName.c_str(), mainDoc, options, &onlyParcelable); + + // after this, there shouldn't be any more errors because of the + // input. + if (err != 0 || mainDoc == NULL) { + return 1; + } + + // they didn't ask to fail on parcelables, so just exit quietly. + if (onlyParcelable && !options.failOnParcelable) { + return 0; + } + + // if we were asked to, generate a make dependency file + if (options.depFileName != "") { + generate_dep_file(options); + } + + err = generate_java(options.outputFileName, options.inputFileName.c_str(), + (interface_type*)mainDoc); + + return err; +} + +static int +preprocess_aidl(const Options& options) +{ + vector<string> lines; + int err; + + // read files + int N = options.filesToPreprocess.size(); + for (int i=0; i<N; i++) { + g_callbacks = &g_mainCallbacks; + err = parse_aidl(options.filesToPreprocess[i].c_str()); + if (err != 0) { + return err; + } + document_item_type* doc = g_document; + string line; + if (doc->item_type == PARCELABLE_TYPE) { + line = "parcelable "; + parcelable_type* parcelable = (parcelable_type*)doc; + if (parcelable->package) { + line += parcelable->package; + line += '.'; + } + line += parcelable->name.data; + } else { + line = "interface "; + interface_type* iface = (interface_type*)doc; + if (iface->package) { + line += iface->package; + line += '.'; + } + line += iface->name.data; + } + line += ";\n"; + lines.push_back(line); + } + + // write preprocessed file + int fd = open( options.outputFileName.c_str(), + O_RDWR|O_CREAT|O_TRUNC|O_BINARY, +#ifdef HAVE_MS_C_RUNTIME + _S_IREAD|_S_IWRITE); +#else + S_IRUSR|S_IWUSR|S_IRGRP); +#endif + if (fd == -1) { + fprintf(stderr, "aidl: could not open file for write: %s\n", + options.outputFileName.c_str()); + return 1; + } + + N = lines.size(); + for (int i=0; i<N; i++) { + const string& s = lines[i]; + int len = s.length(); + if (len != write(fd, s.c_str(), len)) { + fprintf(stderr, "aidl: error writing to file %s\n", + options.outputFileName.c_str()); + close(fd); + unlink(options.outputFileName.c_str()); + return 1; + } + } + + close(fd); + return 0; +} + +// ========================================================== +int +main(int argc, const char **argv) +{ + int err = 0; + + Options options; + int result = parse_options(argc, argv, &options); + if (result) { + return result; + } + + switch (options.task) + { + case COMPILE_AIDL: + return compile_aidl(options); + case PREPROCESS_AIDL: + return preprocess_aidl(options); + } + fprintf(stderr, "aidl: internal error\n"); + return 1; +} + + diff --git a/tools/aidl/aidl_language.cpp b/tools/aidl/aidl_language.cpp new file mode 100644 index 0000000..cd6a3bd --- /dev/null +++ b/tools/aidl/aidl_language.cpp @@ -0,0 +1,20 @@ +#include "aidl_language.h" +#include <stdio.h> +#include <string.h> +#include <stdlib.h> + +#ifdef HAVE_MS_C_RUNTIME +int isatty(int fd) +{ + return (fd == 0); +} +#endif + +#if 0 +ParserCallbacks k_parserCallbacks = { + NULL +}; +#endif + +ParserCallbacks* g_callbacks = NULL; // &k_parserCallbacks; + diff --git a/tools/aidl/aidl_language.h b/tools/aidl/aidl_language.h new file mode 100644 index 0000000..9ca5deb --- /dev/null +++ b/tools/aidl/aidl_language.h @@ -0,0 +1,159 @@ +#ifndef DEVICE_TOOLS_AIDL_AIDL_LANGUAGE_H +#define DEVICE_TOOLS_AIDL_AIDL_LANGUAGE_H + + +typedef enum { + NO_EXTRA_TEXT = 0, + SHORT_COMMENT, + LONG_COMMENT, + COPY_TEXT, + WHITESPACE +} which_extra_text; + +typedef struct extra_text_type { + unsigned lineno; + which_extra_text which; + char* data; + unsigned len; + struct extra_text_type* next; +} extra_text_type; + +typedef struct buffer_type { + unsigned lineno; + unsigned token; + char *data; + extra_text_type* extra; +} buffer_type; + +typedef struct type_type { + buffer_type type; + buffer_type array_token; + int dimension; +} type_type; + +typedef struct arg_type { + buffer_type comma_token; // empty in the first one in the list + buffer_type direction; + type_type type; + buffer_type name; + struct arg_type *next; +} arg_type; + +enum { + METHOD_TYPE +}; + +typedef struct interface_item_type { + unsigned item_type; + struct interface_item_type* next; +} interface_item_type; + +typedef struct method_type { + interface_item_type interface_item; + type_type type; + bool oneway; + buffer_type oneway_token; + buffer_type name; + buffer_type open_paren_token; + arg_type* args; + buffer_type close_paren_token; + // XXX missing comments/copy text here + buffer_type semicolon_token; + buffer_type* comments_token; // points into this structure, DO NOT DELETE +} method_type; + +enum { + PARCELABLE_TYPE = 12, + INTERFACE_TYPE +}; + +typedef struct document_item_type { + unsigned item_type; + struct document_item_type* next; +} document_item_type; + +typedef struct parcelable_type { + document_item_type document_item; + buffer_type parcelable_token; + char* package; + buffer_type name; + buffer_type semicolon_token; +} parcelable_type; + +typedef struct interface_type { + document_item_type document_item; + buffer_type interface_token; + bool oneway; + buffer_type oneway_token; + char* package; + buffer_type name; + buffer_type open_brace_token; + interface_item_type* interface_items; + buffer_type close_brace_token; + buffer_type* comments_token; // points into this structure, DO NOT DELETE +} interface_type; + +typedef union lexer_type { + buffer_type buffer; + type_type type; + arg_type *arg; + method_type* method; + interface_item_type* interface_item; + interface_type* interface_obj; + parcelable_type* parcelable; + document_item_type* document_item; +} lexer_type; + + +#define YYSTYPE lexer_type + +#if __cplusplus +extern "C" { +#endif + +int parse_aidl(char const *); + +// strips off the leading whitespace, the "import" text +// also returns whether it's a local or system import +// we rely on the input matching the import regex from below +char* parse_import_statement(const char* text); + +// in, out or inout +enum { + IN_PARAMETER = 1, + OUT_PARAMETER = 2, + INOUT_PARAMETER = 3 +}; +int convert_direction(const char* direction); + +// callbacks from within the parser +// these functions all take ownership of the strings +typedef struct ParserCallbacks { + void (*document)(document_item_type* items); + void (*import)(buffer_type* statement); +} ParserCallbacks; + +extern ParserCallbacks* g_callbacks; + +// true if there was an error parsing, false otherwise +extern int g_error; + +// the name of the file we're currently parsing +extern char const* g_currentFilename; + +// the package name for our current file +extern char const* g_currentPackage; + +typedef enum { + STATEMENT_INSIDE_INTERFACE +} error_type; + +void init_buffer_type(buffer_type* buf, int lineno); + + +#if __cplusplus +} +#endif + + +#endif // DEVICE_TOOLS_AIDL_AIDL_LANGUAGE_H diff --git a/tools/aidl/aidl_language_l.l b/tools/aidl/aidl_language_l.l new file mode 100644 index 0000000..567b1cf --- /dev/null +++ b/tools/aidl/aidl_language_l.l @@ -0,0 +1,210 @@ +%{ +#include "aidl_language.h" +#include "aidl_language_y.h" +#include "search_path.h" +#include <string.h> +#include <stdlib.h> + +extern YYSTYPE yylval; + +// comment and whitespace handling +// these functions save a copy of the buffer +static void begin_extra_text(unsigned lineno, which_extra_text which); +static void append_extra_text(char* text); +static extra_text_type* get_extra_text(void); // you now own the object + // this returns +static void drop_extra_text(void); + +// package handling +static void do_package_statement(const char* importText); + +#define SET_BUFFER(t) \ + do { \ + yylval.buffer.lineno = yylineno; \ + yylval.buffer.token = (t); \ + yylval.buffer.data = strdup(yytext); \ + yylval.buffer.extra = get_extra_text(); \ + } while(0) + +%} + +%option yylineno +%option noyywrap + +%x COPYING LONG_COMMENT + +identifier [_a-zA-Z][_a-zA-Z0-9\.]* +whitespace ([ \t\n\r]+) +brackets \[{whitespace}?\] + +%% + + +\%\%\{ { begin_extra_text(yylineno, COPY_TEXT); BEGIN(COPYING); } +<COPYING>\}\%\% { BEGIN(INITIAL); } +<COPYING>.*\n { append_extra_text(yytext); } +<COPYING>.* { append_extra_text(yytext); } +<COPYING>\n+ { append_extra_text(yytext); } + + +\/\* { begin_extra_text(yylineno, (which_extra_text)LONG_COMMENT); + BEGIN(LONG_COMMENT); } +<LONG_COMMENT>[^*]* { append_extra_text(yytext); } +<LONG_COMMENT>\*+[^/] { append_extra_text(yytext); } +<LONG_COMMENT>\n { append_extra_text(yytext); } +<LONG_COMMENT>\**\/ { BEGIN(INITIAL); } + +^{whitespace}?import{whitespace}[^ \t\r\n]+{whitespace}?; { + SET_BUFFER(IMPORT); + return IMPORT; + } +^{whitespace}?package{whitespace}[^ \t\r\n]+{whitespace}?; { + do_package_statement(yytext); + SET_BUFFER(PACKAGE); + return PACKAGE; + } +<<EOF>> { yyterminate(); } + +\/\/.*\n { begin_extra_text(yylineno, SHORT_COMMENT); + append_extra_text(yytext); } + +{whitespace} { /* begin_extra_text(yylineno, WHITESPACE); + append_extra_text(yytext); */ } + +; { SET_BUFFER(';'); return ';'; } +\{ { SET_BUFFER('{'); return '{'; } +\} { SET_BUFFER('}'); return '}'; } +\( { SET_BUFFER('('); return '('; } +\) { SET_BUFFER(')'); return ')'; } +, { SET_BUFFER(','); return ','; } + + /* keywords */ +parcelable { SET_BUFFER(PARCELABLE); return PARCELABLE; } +interface { SET_BUFFER(INTERFACE); return INTERFACE; } +in { SET_BUFFER(IN); return IN; } +out { SET_BUFFER(OUT); return OUT; } +inout { SET_BUFFER(INOUT); return INOUT; } +oneway { SET_BUFFER(ONEWAY); return ONEWAY; } + +{brackets}+ { SET_BUFFER(ARRAY); return ARRAY; } + +{identifier} { SET_BUFFER(IDENTIFIER); return IDENTIFIER; } +{identifier}\<{whitespace}*{identifier}({whitespace}*,{whitespace}*{identifier})*{whitespace}*\> { + SET_BUFFER(GENERIC); return GENERIC; } + + /* syntax error! */ +. { printf("UNKNOWN(%s)", yytext); + yylval.buffer.lineno = yylineno; + yylval.buffer.token = IDENTIFIER; + yylval.buffer.data = strdup(yytext); + return IDENTIFIER; + } + +%% + +// comment and whitespace handling +// ================================================ +extra_text_type* g_extraText = NULL; +extra_text_type* g_nextExtraText = NULL; + +void begin_extra_text(unsigned lineno, which_extra_text which) +{ + extra_text_type* text = (extra_text_type*)malloc(sizeof(extra_text_type)); + text->lineno = lineno; + text->which = which; + text->data = NULL; + text->len = 0; + text->next = NULL; + if (g_nextExtraText == NULL) { + g_extraText = text; + } else { + g_nextExtraText->next = text; + } + g_nextExtraText = text; +} + +void append_extra_text(char* text) +{ + if (g_nextExtraText->data == NULL) { + g_nextExtraText->data = strdup(text); + g_nextExtraText->len = strlen(text); + } else { + char* orig = g_nextExtraText->data; + unsigned oldLen = g_nextExtraText->len; + unsigned len = strlen(text); + g_nextExtraText->len += len; + g_nextExtraText->data = (char*)malloc(g_nextExtraText->len+1); + memcpy(g_nextExtraText->data, orig, oldLen); + memcpy(g_nextExtraText->data+oldLen, text, len); + g_nextExtraText->data[g_nextExtraText->len] = '\0'; + free(orig); + } +} + +extra_text_type* +get_extra_text(void) +{ + extra_text_type* result = g_extraText; + g_extraText = NULL; + g_nextExtraText = NULL; + return result; +} + +void drop_extra_text(void) +{ + extra_text_type* p = g_extraText; + while (p) { + extra_text_type* next = p->next; + free(p->data); + free(p); + free(next); + } + g_extraText = NULL; + g_nextExtraText = NULL; +} + + +// package handling +// ================================================ +void do_package_statement(const char* importText) +{ + if (g_currentPackage) free((void*)g_currentPackage); + g_currentPackage = parse_import_statement(importText); +} + + +// main parse function +// ================================================ +char const* g_currentFilename = NULL; +char const* g_currentPackage = NULL; + +int yyparse(void); + +int parse_aidl(char const *filename) +{ + yyin = fopen(filename, "r"); + if (yyin) { + char const* oldFilename = g_currentFilename; + char const* oldPackage = g_currentPackage; + g_currentFilename = strdup(filename); + + g_error = 0; + yylineno = 1; + int rv = yyparse(); + if (g_error != 0) { + rv = g_error; + } + + free((void*)g_currentFilename); + g_currentFilename = oldFilename; + + if (g_currentPackage) free((void*)g_currentPackage); + g_currentPackage = oldPackage; + + return rv; + } else { + fprintf(stderr, "aidl: unable to open file for read: %s\n", filename); + return 1; + } +} + diff --git a/tools/aidl/aidl_language_y.y b/tools/aidl/aidl_language_y.y new file mode 100644 index 0000000..3d65f17 --- /dev/null +++ b/tools/aidl/aidl_language_y.y @@ -0,0 +1,288 @@ +%{ +#include "aidl_language.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +int yyerror(char* errstr); +int yylex(void); +extern int yylineno; + +static int count_brackets(const char*); + +%} + +%token IMPORT +%token PACKAGE +%token IDENTIFIER +%token GENERIC +%token ARRAY +%token PARCELABLE +%token INTERFACE +%token IN +%token OUT +%token INOUT +%token ONEWAY + +%% +document: + document_items { g_callbacks->document($1.document_item); } + | headers document_items { g_callbacks->document($2.document_item); } + ; + +headers: + package { } + | imports { } + | package imports { } + ; + +package: + PACKAGE { } + ; + +imports: + IMPORT { g_callbacks->import(&($1.buffer)); } + | IMPORT imports { g_callbacks->import(&($1.buffer)); } + ; + +document_items: + { $$.document_item = NULL; } + | document_items declaration { + if ($2.document_item == NULL) { + // error cases only + $$ = $1; + } else { + document_item_type* p = $1.document_item; + while (p && p->next) { + p=p->next; + } + if (p) { + p->next = (document_item_type*)$2.document_item; + $$ = $1; + } else { + $$.document_item = (document_item_type*)$2.document_item; + } + } + } + | document_items error { + fprintf(stderr, "%s:%d: syntax error don't know what to do with \"%s\"\n", g_currentFilename, + $2.buffer.lineno, $2.buffer.data); + $$ = $1; + } + ; + +declaration: + parcelable_decl { $$.document_item = (document_item_type*)$1.parcelable; } + | interface_decl { $$.document_item = (document_item_type*)$1.interface_item; } + ; + +parcelable_decl: + PARCELABLE IDENTIFIER ';' { + parcelable_type* b = (parcelable_type*)malloc(sizeof(parcelable_type)); + b->document_item.item_type = PARCELABLE_TYPE; + b->document_item.next = NULL; + b->parcelable_token = $1.buffer; + b->name = $2.buffer; + b->package = g_currentPackage ? strdup(g_currentPackage) : NULL; + b->semicolon_token = $3.buffer; + $$.parcelable = b; + } + | PARCELABLE ';' { + fprintf(stderr, "%s:%d syntax error in parcelable declaration. Expected type name.\n", + g_currentFilename, $1.buffer.lineno); + $$.parcelable = NULL; + } + | PARCELABLE error ';' { + fprintf(stderr, "%s:%d syntax error in parcelable declaration. Expected type name, saw \"%s\".\n", + g_currentFilename, $2.buffer.lineno, $2.buffer.data); + $$.parcelable = NULL; + } + ; + +interface_header: + INTERFACE { + interface_type* c = (interface_type*)malloc(sizeof(interface_type)); + c->interface_token = $1.buffer; + c->oneway = false; + memset(&c->oneway_token, 0, sizeof(buffer_type)); + c->comments_token = &c->interface_token; + $$.interface_obj = c; + } + | ONEWAY INTERFACE { + interface_type* c = (interface_type*)malloc(sizeof(interface_type)); + c->interface_token = $2.buffer; + c->oneway = true; + c->oneway_token = $1.buffer; + c->comments_token = &c->oneway_token; + $$.interface_obj = c; + } + ; + +interface_decl: + interface_header IDENTIFIER '{' interface_items '}' { + interface_type* c = $1.interface_obj; + c->document_item.item_type = INTERFACE_TYPE; + c->document_item.next = NULL; + c->name = $2.buffer; + c->package = g_currentPackage ? strdup(g_currentPackage) : NULL; + c->open_brace_token = $3.buffer; + c->interface_items = $4.interface_item; + c->close_brace_token = $5.buffer; + $$.interface_obj = c; + } + | INTERFACE error '{' interface_items '}' { + fprintf(stderr, "%s:%d: syntax error in interface declaration. Expected type name, saw \"%s\"\n", + g_currentFilename, $2.buffer.lineno, $2.buffer.data); + $$.document_item = NULL; + } + | INTERFACE error '}' { + fprintf(stderr, "%s:%d: syntax error in interface declaration. Expected type name, saw \"%s\"\n", + g_currentFilename, $2.buffer.lineno, $2.buffer.data); + $$.document_item = NULL; + } + + ; + +interface_items: + { $$.interface_item = NULL; } + | interface_items method_decl { + interface_item_type* p=$1.interface_item; + while (p && p->next) { + p=p->next; + } + if (p) { + p->next = (interface_item_type*)$2.method; + $$ = $1; + } else { + $$.interface_item = (interface_item_type*)$2.method; + } + } + | interface_items error ';' { + fprintf(stderr, "%s:%d: syntax error before ';' (expected method declaration)\n", + g_currentFilename, $3.buffer.lineno); + $$ = $1; + } + ; + +method_decl: + type IDENTIFIER '(' arg_list ')' ';' { + method_type *method = (method_type*)malloc(sizeof(method_type)); + method->interface_item.item_type = METHOD_TYPE; + method->interface_item.next = NULL; + method->type = $1.type; + method->oneway = false; + memset(&method->oneway_token, 0, sizeof(buffer_type)); + method->name = $2.buffer; + method->open_paren_token = $3.buffer; + method->args = $4.arg; + method->close_paren_token = $5.buffer; + method->semicolon_token = $6.buffer; + method->comments_token = &method->type.type; + $$.method = method; + } + | ONEWAY type IDENTIFIER '(' arg_list ')' ';' { + method_type *method = (method_type*)malloc(sizeof(method_type)); + method->interface_item.item_type = METHOD_TYPE; + method->interface_item.next = NULL; + method->oneway = true; + method->oneway_token = $1.buffer; + method->type = $2.type; + method->name = $3.buffer; + method->open_paren_token = $4.buffer; + method->args = $5.arg; + method->close_paren_token = $6.buffer; + method->semicolon_token = $7.buffer; + method->comments_token = &method->oneway_token; + $$.method = method; + } + ; + +arg_list: + { $$.arg = NULL; } + | arg { $$ = $1; } + | arg_list ',' arg { + if ($$.arg != NULL) { + // only NULL on error + $$ = $1; + arg_type *p = $1.arg; + while (p && p->next) { + p=p->next; + } + $3.arg->comma_token = $2.buffer; + p->next = $3.arg; + } + } + | error { + fprintf(stderr, "%s:%d: syntax error in parameter list\n", g_currentFilename, $1.buffer.lineno); + $$.arg = NULL; + } + ; + +arg: + direction type IDENTIFIER { + arg_type* arg = (arg_type*)malloc(sizeof(arg_type)); + memset(&arg->comma_token, 0, sizeof(buffer_type)); + arg->direction = $1.buffer; + arg->type = $2.type; + arg->name = $3.buffer; + arg->next = NULL; + $$.arg = arg; + } + ; + +type: + IDENTIFIER { + $$.type.type = $1.buffer; + init_buffer_type(&$$.type.array_token, yylineno); + $$.type.dimension = 0; + } + | IDENTIFIER ARRAY { + $$.type.type = $1.buffer; + $$.type.array_token = $2.buffer; + $$.type.dimension = count_brackets($2.buffer.data); + } + | GENERIC { + $$.type.type = $1.buffer; + init_buffer_type(&$$.type.array_token, yylineno); + $$.type.dimension = 0; + } + ; + +direction: + { init_buffer_type(&$$.buffer, yylineno); } + | IN { $$.buffer = $1.buffer; } + | OUT { $$.buffer = $1.buffer; } + | INOUT { $$.buffer = $1.buffer; } + ; + +%% + +#include <ctype.h> +#include <stdio.h> + +int g_error = 0; + +int yyerror(char* errstr) +{ + fprintf(stderr, "%s:%d: %s\n", g_currentFilename, yylineno, errstr); + g_error = 1; + return 1; +} + +void init_buffer_type(buffer_type* buf, int lineno) +{ + buf->lineno = lineno; + buf->token = 0; + buf->data = NULL; + buf->extra = NULL; +} + +static int count_brackets(const char* s) +{ + int n=0; + while (*s) { + if (*s == '[') n++; + s++; + } + return n; +} diff --git a/tools/aidl/generate_java.cpp b/tools/aidl/generate_java.cpp new file mode 100644 index 0000000..e3c0af0 --- /dev/null +++ b/tools/aidl/generate_java.cpp @@ -0,0 +1,652 @@ +#include "generate_java.h" +#include "AST.h" +#include "Type.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +// ================================================= +class VariableFactory +{ +public: + VariableFactory(const string& base); // base must be short + Variable* Get(Type* type); + Variable* Get(int index); +private: + vector<Variable*> m_vars; + string m_base; + int m_index; +}; + +VariableFactory::VariableFactory(const string& base) + :m_base(base), + m_index(0) +{ +} + +Variable* +VariableFactory::Get(Type* type) +{ + char name[100]; + sprintf(name, "%s%d", m_base.c_str(), m_index); + m_index++; + Variable* v = new Variable(type, name); + m_vars.push_back(v); + return v; +} + +Variable* +VariableFactory::Get(int index) +{ + return m_vars[index]; +} + +// ================================================= +class StubClass : public Class +{ +public: + StubClass(Type* type, Type* interfaceType); + virtual ~StubClass(); + + Variable* transact_code; + Variable* transact_data; + Variable* transact_reply; + Variable* transact_flags; + SwitchStatement* transact_switch; +private: + void make_as_interface(Type* interfaceType); +}; + +StubClass::StubClass(Type* type, Type* interfaceType) + :Class() +{ + this->comment = "/** Local-side IPC implementation stub class. */"; + this->modifiers = PUBLIC | ABSTRACT | STATIC; + this->what = Class::CLASS; + this->type = type; + this->extends = BINDER_NATIVE_TYPE; + this->interfaces.push_back(interfaceType); + + // descriptor + Field* descriptor = new Field(STATIC | FINAL | PRIVATE, + new Variable(STRING_TYPE, "DESCRIPTOR")); + descriptor->value = "\"" + interfaceType->QualifiedName() + "\""; + this->elements.push_back(descriptor); + + // ctor + Method* ctor = new Method; + ctor->modifiers = PUBLIC; + ctor->comment = "/** Construct the stub at attach it to the " + "interface. */"; + ctor->name = "Stub"; + ctor->statements = new StatementBlock; + MethodCall* attach = new MethodCall(THIS_VALUE, "attachInterface", + 2, THIS_VALUE, new LiteralExpression("DESCRIPTOR")); + ctor->statements->Add(attach); + this->elements.push_back(ctor); + + // asInterface + make_as_interface(interfaceType); + + // asBinder + Method* asBinder = new Method; + asBinder->modifiers = PUBLIC; + asBinder->returnType = IBINDER_TYPE; + asBinder->name = "asBinder"; + asBinder->statements = new StatementBlock; + asBinder->statements->Add(new ReturnStatement(THIS_VALUE)); + this->elements.push_back(asBinder); + + // onTransact + this->transact_code = new Variable(INT_TYPE, "code"); + this->transact_data = new Variable(PARCEL_TYPE, "data"); + this->transact_reply = new Variable(PARCEL_TYPE, "reply"); + this->transact_flags = new Variable(INT_TYPE, "flags"); + Method* onTransact = new Method; + onTransact->modifiers = PUBLIC; + onTransact->returnType = BOOLEAN_TYPE; + onTransact->name = "onTransact"; + onTransact->parameters.push_back(this->transact_code); + onTransact->parameters.push_back(this->transact_data); + onTransact->parameters.push_back(this->transact_reply); + onTransact->parameters.push_back(this->transact_flags); + onTransact->statements = new StatementBlock; + onTransact->exceptions.push_back(REMOTE_EXCEPTION_TYPE); + this->elements.push_back(onTransact); + this->transact_switch = new SwitchStatement(this->transact_code); + + onTransact->statements->Add(this->transact_switch); + MethodCall* superCall = new MethodCall(SUPER_VALUE, "onTransact", 4, + this->transact_code, this->transact_data, + this->transact_reply, this->transact_flags); + onTransact->statements->Add(new ReturnStatement(superCall)); +} + +StubClass::~StubClass() +{ +} + +void +StubClass::make_as_interface(Type *interfaceType) +{ + Variable* obj = new Variable(IBINDER_TYPE, "obj"); + + Method* m = new Method; + m->comment = "/**\n * Cast an IBinder object into an "; + m->comment += interfaceType->Name(); + m->comment += " interface,\n"; + m->comment += " * generating a proxy if needed.\n */"; + m->modifiers = PUBLIC | STATIC; + m->returnType = interfaceType; + m->name = "asInterface"; + m->parameters.push_back(obj); + m->statements = new StatementBlock; + + IfStatement* ifstatement = new IfStatement(); + ifstatement->expression = new Comparison(obj, "==", NULL_VALUE); + ifstatement->statements = new StatementBlock; + ifstatement->statements->Add(new ReturnStatement(NULL_VALUE)); + m->statements->Add(ifstatement); + + // IInterface iin = obj.queryLocalInterface(DESCRIPTOR) + MethodCall* queryLocalInterface = new MethodCall(obj, "queryLocalInterface"); + queryLocalInterface->arguments.push_back(new LiteralExpression("DESCRIPTOR")); + IInterfaceType* iinType = new IInterfaceType(); + Variable *iin = new Variable(iinType, "iin"); + VariableDeclaration* iinVd = new VariableDeclaration(iin, queryLocalInterface, iinType); + m->statements->Add(iinVd); + + // Ensure the instance type of the local object is as expected. + // One scenario where this is needed is if another package (with a + // different class loader) runs in the same process as the service. + + // if (iin != null && iin instanceof <interfaceType>) return (<interfaceType>) iin; + Comparison* iinNotNull = new Comparison(iin, "!=", NULL_VALUE); + Comparison* instOfCheck = new Comparison(iin, " instanceof ", + new LiteralExpression(interfaceType->QualifiedName())); + IfStatement* instOfStatement = new IfStatement(); + instOfStatement->expression = new Comparison(iinNotNull, "&&", instOfCheck); + instOfStatement->statements = new StatementBlock; + instOfStatement->statements->Add(new ReturnStatement(new Cast(interfaceType, iin))); + m->statements->Add(instOfStatement); + + string proxyType = interfaceType->QualifiedName(); + proxyType += ".Stub.Proxy"; + NewExpression* ne = new NewExpression(NAMES.Find(proxyType)); + ne->arguments.push_back(obj); + m->statements->Add(new ReturnStatement(ne)); + + this->elements.push_back(m); +} + + + +// ================================================= +class ProxyClass : public Class +{ +public: + ProxyClass(Type* type, InterfaceType* interfaceType); + virtual ~ProxyClass(); + + Variable* mRemote; + bool mOneWay; +}; + +ProxyClass::ProxyClass(Type* type, InterfaceType* interfaceType) + :Class() +{ + this->modifiers = PRIVATE | STATIC; + this->what = Class::CLASS; + this->type = type; + this->interfaces.push_back(interfaceType); + + mOneWay = interfaceType->OneWay(); + + // IBinder mRemote + mRemote = new Variable(IBINDER_TYPE, "mRemote"); + this->elements.push_back(new Field(PRIVATE, mRemote)); + + // Proxy() + Variable* remote = new Variable(IBINDER_TYPE, "remote"); + Method* ctor = new Method; + ctor->name = "Proxy"; + ctor->statements = new StatementBlock; + ctor->parameters.push_back(remote); + ctor->statements->Add(new Assignment(mRemote, remote)); + this->elements.push_back(ctor); + + // IBinder asBinder() + Method* asBinder = new Method; + asBinder->modifiers = PUBLIC; + asBinder->returnType = IBINDER_TYPE; + asBinder->name = "asBinder"; + asBinder->statements = new StatementBlock; + asBinder->statements->Add(new ReturnStatement(mRemote)); + this->elements.push_back(asBinder); +} + +ProxyClass::~ProxyClass() +{ +} + +// ================================================= +static string +gather_comments(extra_text_type* extra) +{ + string s; + while (extra) { + if (extra->which == SHORT_COMMENT) { + s += extra->data; + } + else if (extra->which == LONG_COMMENT) { + s += "/*"; + s += extra->data; + s += "*/"; + } + extra = extra->next; + } + return s; +} + +static string +append(const char* a, const char* b) +{ + string s = a; + s += b; + return s; +} + +static void +generate_new_array(Type* t, StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + Variable* len = new Variable(INT_TYPE, v->name + "_length"); + addTo->Add(new VariableDeclaration(len, new MethodCall(parcel, "readInt"))); + IfStatement* lencheck = new IfStatement(); + lencheck->expression = new Comparison(len, "<", new LiteralExpression("0")); + lencheck->statements->Add(new Assignment(v, NULL_VALUE)); + lencheck->elseif = new IfStatement(); + lencheck->elseif->statements->Add(new Assignment(v, + new NewArrayExpression(t, len))); + addTo->Add(lencheck); +} + +static void +generate_write_to_parcel(Type* t, StatementBlock* addTo, Variable* v, + Variable* parcel, int flags) +{ + if (v->dimension == 0) { + t->WriteToParcel(addTo, v, parcel, flags); + } + if (v->dimension == 1) { + t->WriteArrayToParcel(addTo, v, parcel, flags); + } +} + +static void +generate_create_from_parcel(Type* t, StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + if (v->dimension == 0) { + t->CreateFromParcel(addTo, v, parcel); + } + if (v->dimension == 1) { + t->CreateArrayFromParcel(addTo, v, parcel); + } +} + +static void +generate_read_from_parcel(Type* t, StatementBlock* addTo, Variable* v, + Variable* parcel) +{ + if (v->dimension == 0) { + t->ReadFromParcel(addTo, v, parcel); + } + if (v->dimension == 1) { + t->ReadArrayFromParcel(addTo, v, parcel); + } +} + + +static void +generate_method(const method_type* method, Class* interface, + StubClass* stubClass, ProxyClass* proxyClass, int index) +{ + arg_type* arg; + int i; + bool hasOutParams = false; + + const bool oneway = proxyClass->mOneWay || method->oneway; + + // == the TRANSACT_ constant ============================================= + string transactCodeName = "TRANSACTION_"; + transactCodeName += method->name.data; + + char transactCodeValue[50]; + sprintf(transactCodeValue, "(IBinder.FIRST_CALL_TRANSACTION + %d)", index); + + Field* transactCode = new Field(STATIC | FINAL, + new Variable(INT_TYPE, transactCodeName)); + transactCode->value = transactCodeValue; + stubClass->elements.push_back(transactCode); + + // == the declaration in the interface =================================== + Method* decl = new Method; + decl->comment = gather_comments(method->comments_token->extra); + decl->modifiers = PUBLIC; + decl->returnType = NAMES.Search(method->type.type.data); + decl->returnTypeDimension = method->type.dimension; + decl->name = method->name.data; + + arg = method->args; + while (arg != NULL) { + decl->parameters.push_back(new Variable( + NAMES.Search(arg->type.type.data), arg->name.data, + arg->type.dimension)); + arg = arg->next; + } + + decl->exceptions.push_back(REMOTE_EXCEPTION_TYPE); + + interface->elements.push_back(decl); + + // == the stub method ==================================================== + + Case* c = new Case(transactCodeName); + + MethodCall* realCall = new MethodCall(THIS_VALUE, method->name.data); + + // interface token validation is the very first thing we do + c->statements->Add(new MethodCall(stubClass->transact_data, + "enforceInterface", 1, new LiteralExpression("DESCRIPTOR"))); + + // args + VariableFactory stubArgs("_arg"); + arg = method->args; + while (arg != NULL) { + Type* t = NAMES.Search(arg->type.type.data); + Variable* v = stubArgs.Get(t); + v->dimension = arg->type.dimension; + + c->statements->Add(new VariableDeclaration(v)); + + if (convert_direction(arg->direction.data) & IN_PARAMETER) { + generate_create_from_parcel(t, c->statements, v, + stubClass->transact_data); + } else { + if (arg->type.dimension == 0) { + c->statements->Add(new Assignment( + v, new NewExpression(v->type))); + } + else if (arg->type.dimension == 1) { + generate_new_array(v->type, c->statements, v, + stubClass->transact_data); + } + else { + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, + __LINE__); + } + } + + realCall->arguments.push_back(v); + + arg = arg->next; + } + + // the real call + Variable* _result = NULL; + if (0 == strcmp(method->type.type.data, "void")) { + c->statements->Add(realCall); + + if (!oneway) { + // report that there were no exceptions + MethodCall* ex = new MethodCall(stubClass->transact_reply, + "writeNoException", 0); + c->statements->Add(ex); + } + } else { + _result = new Variable(decl->returnType, "_result", + decl->returnTypeDimension); + c->statements->Add(new VariableDeclaration(_result, realCall)); + + if (!oneway) { + // report that there were no exceptions + MethodCall* ex = new MethodCall(stubClass->transact_reply, + "writeNoException", 0); + c->statements->Add(ex); + } + + // marshall the return value + generate_write_to_parcel(decl->returnType, c->statements, _result, + stubClass->transact_reply, + Type::PARCELABLE_WRITE_RETURN_VALUE); + } + + // out parameters + i = 0; + arg = method->args; + while (arg != NULL) { + Type* t = NAMES.Search(arg->type.type.data); + Variable* v = stubArgs.Get(i++); + + if (convert_direction(arg->direction.data) & OUT_PARAMETER) { + generate_write_to_parcel(t, c->statements, v, + stubClass->transact_reply, + Type::PARCELABLE_WRITE_RETURN_VALUE); + hasOutParams = true; + } + + arg = arg->next; + } + + // return true + c->statements->Add(new ReturnStatement(TRUE_VALUE)); + stubClass->transact_switch->cases.push_back(c); + + // == the proxy method =================================================== + Method* proxy = new Method; + proxy->comment = gather_comments(method->comments_token->extra); + proxy->modifiers = PUBLIC; + proxy->returnType = NAMES.Search(method->type.type.data); + proxy->returnTypeDimension = method->type.dimension; + proxy->name = method->name.data; + proxy->statements = new StatementBlock; + arg = method->args; + while (arg != NULL) { + proxy->parameters.push_back(new Variable( + NAMES.Search(arg->type.type.data), arg->name.data, + arg->type.dimension)); + arg = arg->next; + } + proxy->exceptions.push_back(REMOTE_EXCEPTION_TYPE); + proxyClass->elements.push_back(proxy); + + // the parcels + Variable* _data = new Variable(PARCEL_TYPE, "_data"); + proxy->statements->Add(new VariableDeclaration(_data, + new MethodCall(PARCEL_TYPE, "obtain"))); + Variable* _reply = NULL; + if (!oneway) { + _reply = new Variable(PARCEL_TYPE, "_reply"); + proxy->statements->Add(new VariableDeclaration(_reply, + new MethodCall(PARCEL_TYPE, "obtain"))); + } + + // the return value + _result = NULL; + if (0 != strcmp(method->type.type.data, "void")) { + _result = new Variable(proxy->returnType, "_result", + method->type.dimension); + proxy->statements->Add(new VariableDeclaration(_result)); + } + + // try and finally + TryStatement* tryStatement = new TryStatement(); + proxy->statements->Add(tryStatement); + FinallyStatement* finallyStatement = new FinallyStatement(); + proxy->statements->Add(finallyStatement); + + // the interface identifier token: the DESCRIPTOR constant, marshalled as a string + tryStatement->statements->Add(new MethodCall(_data, "writeInterfaceToken", + 1, new LiteralExpression("DESCRIPTOR"))); + + // the parameters + arg = method->args; + while (arg != NULL) { + Type* t = NAMES.Search(arg->type.type.data); + Variable* v = new Variable(t, arg->name.data, arg->type.dimension); + int dir = convert_direction(arg->direction.data); + if (dir == OUT_PARAMETER && arg->type.dimension != 0) { + IfStatement* checklen = new IfStatement(); + checklen->expression = new Comparison(v, "==", NULL_VALUE); + checklen->statements->Add(new MethodCall(_data, "writeInt", 1, + new LiteralExpression("-1"))); + checklen->elseif = new IfStatement(); + checklen->elseif->statements->Add(new MethodCall(_data, "writeInt", + 1, new FieldVariable(v, "length"))); + tryStatement->statements->Add(checklen); + } + else if (dir & IN_PARAMETER) { + generate_write_to_parcel(t, tryStatement->statements, v, _data, 0); + } + arg = arg->next; + } + + // the transact call + MethodCall* call = new MethodCall(proxyClass->mRemote, "transact", 4, + new LiteralExpression("Stub." + transactCodeName), + _data, _reply ? _reply : NULL_VALUE, + new LiteralExpression( + oneway ? "IBinder.FLAG_ONEWAY" : "0")); + tryStatement->statements->Add(call); + + // throw back exceptions. + if (_reply) { + MethodCall* ex = new MethodCall(_reply, "readException", 0); + tryStatement->statements->Add(ex); + } + + // returning and cleanup + if (_reply != NULL) { + if (_result != NULL) { + generate_create_from_parcel(proxy->returnType, + tryStatement->statements, _result, _reply); + } + + // the out/inout parameters + arg = method->args; + while (arg != NULL) { + Type* t = NAMES.Search(arg->type.type.data); + Variable* v = new Variable(t, arg->name.data, arg->type.dimension); + if (convert_direction(arg->direction.data) & OUT_PARAMETER) { + generate_read_from_parcel(t, tryStatement->statements, + v, _reply); + } + arg = arg->next; + } + + finallyStatement->statements->Add(new MethodCall(_reply, "recycle")); + } + finallyStatement->statements->Add(new MethodCall(_data, "recycle")); + + if (_result != NULL) { + proxy->statements->Add(new ReturnStatement(_result)); + } +} + +static void +generate_interface_descriptors(StubClass* stub, ProxyClass* proxy) +{ + // the interface descriptor transaction handler + Case* c = new Case("INTERFACE_TRANSACTION"); + c->statements->Add(new MethodCall(stub->transact_reply, "writeString", + 1, new LiteralExpression("DESCRIPTOR"))); + c->statements->Add(new ReturnStatement(TRUE_VALUE)); + stub->transact_switch->cases.push_back(c); + + // and the proxy-side method returning the descriptor directly + Method* getDesc = new Method; + getDesc->modifiers = PUBLIC; + getDesc->returnType = STRING_TYPE; + getDesc->returnTypeDimension = 0; + getDesc->name = "getInterfaceDescriptor"; + getDesc->statements = new StatementBlock; + getDesc->statements->Add(new ReturnStatement(new LiteralExpression("DESCRIPTOR"))); + proxy->elements.push_back(getDesc); +} + +static Class* +generate_interface_class(const interface_type* iface) +{ + InterfaceType* interfaceType = static_cast<InterfaceType*>( + NAMES.Find(iface->package, iface->name.data)); + + // the interface class + Class* interface = new Class; + interface->comment = gather_comments(iface->comments_token->extra); + interface->modifiers = PUBLIC; + interface->what = Class::INTERFACE; + interface->type = interfaceType; + interface->interfaces.push_back(IINTERFACE_TYPE); + + // the stub inner class + StubClass* stub = new StubClass( + NAMES.Find(iface->package, append(iface->name.data, ".Stub").c_str()), + interfaceType); + interface->elements.push_back(stub); + + // the proxy inner class + ProxyClass* proxy = new ProxyClass( + NAMES.Find(iface->package, + append(iface->name.data, ".Stub.Proxy").c_str()), + interfaceType); + stub->elements.push_back(proxy); + + // stub and proxy support for getInterfaceDescriptor() + generate_interface_descriptors(stub, proxy); + + // all the declared methods of the interface + int index = 0; + interface_item_type* item = iface->interface_items; + while (item != NULL) { + if (item->item_type == METHOD_TYPE) { + generate_method((method_type*)item, interface, stub, proxy, index); + } + item = item->next; + index++; + } + + return interface; +} + +int +generate_java(const string& filename, const string& originalSrc, + interface_type* iface) +{ + Document* document = new Document; + document->comment = ""; + if (iface->package) document->package = iface->package; + document->originalSrc = originalSrc; + document->classes.push_back(generate_interface_class(iface)); + +// printf("outputting... filename=%s\n", filename.c_str()); + FILE* to; + if (filename == "-") { + to = stdout; + } else { + /* open file in binary mode to ensure that the tool produces the + * same output on all platforms !! + */ + to = fopen(filename.c_str(), "wb"); + if (to == NULL) { + fprintf(stderr, "unable to open %s for write\n", filename.c_str()); + return 1; + } + } + + document->Write(to); + + fclose(to); + return 0; +} + diff --git a/tools/aidl/generate_java.h b/tools/aidl/generate_java.h new file mode 100644 index 0000000..203fe23 --- /dev/null +++ b/tools/aidl/generate_java.h @@ -0,0 +1,14 @@ +#ifndef GENERATE_JAVA_H +#define GENERATE_JAVA_H + +#include "aidl_language.h" + +#include <string> + +using namespace std; + +int generate_java(const string& filename, const string& originalSrc, + interface_type* iface); + +#endif // GENERATE_JAVA_H + diff --git a/tools/aidl/options.cpp b/tools/aidl/options.cpp new file mode 100644 index 0000000..57b10ae --- /dev/null +++ b/tools/aidl/options.cpp @@ -0,0 +1,138 @@ + +#include "options.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static int +usage() +{ + fprintf(stderr, + "usage: aidl OPTIONS INPUT [OUTPUT]\n" + " aidl --preprocess OUTPUT INPUT...\n" + "\n" + "OPTIONS:\n" + " -I<DIR> search path for import statements.\n" + " -d<FILE> generate dependency file.\n" + " -p<FILE> file created by --preprocess to import.\n" + " -b fail when trying to compile a parcelable.\n" + "\n" + "INPUT:\n" + " An aidl interface file.\n" + "\n" + "OUTPUT:\n" + " The generated interface files. If omitted, the input filename is used, with the .aidl extension changed to a .java extension.\n" + ); + return 1; +} + +int +parse_options(int argc, const char* const* argv, Options *options) +{ + int i = 1; + + if (argc >= 2 && 0 == strcmp(argv[1], "--preprocess")) { + if (argc < 4) { + return usage(); + } + options->outputFileName = argv[2]; + for (int i=3; i<argc; i++) { + options->filesToPreprocess.push_back(argv[i]); + } + options->task = PREPROCESS_AIDL; + return 0; + } + + options->task = COMPILE_AIDL; + options->failOnParcelable = false; + + // OPTIONS + while (i < argc) { + const char* s = argv[i]; + int len = strlen(s); + if (s[0] == '-') { + if (len > 1) { + // -I<system-import-path> + if (s[1] == 'I') { + if (len > 2) { + options->importPaths.push_back(s+2); + } else { + fprintf(stderr, "-I option (%d) requires a path.\n", i); + return usage(); + } + } + else if (s[1] == 'd') { + if (len > 2) { + options->depFileName = s+2; + } else { + fprintf(stderr, "-d option (%d) requires a file.\n", i); + return usage(); + } + } + else if (s[1] == 'p') { + if (len > 2) { + options->preprocessedFiles.push_back(s+2); + } else { + fprintf(stderr, "-p option (%d) requires a file.\n", i); + return usage(); + } + } + else if (len == 2 && s[1] == 'b') { + options->failOnParcelable = true; + } + else { + // s[1] is not known + fprintf(stderr, "unknown option (%d): %s\n", i, s); + return usage(); + } + } else { + // len <= 1 + fprintf(stderr, "unknown option (%d): %s\n", i, s); + return usage(); + } + } else { + // s[0] != '-' + break; + } + i++; + } + + // INPUT + if (i < argc) { + options->inputFileName = argv[i]; + i++; + } else { + fprintf(stderr, "INPUT required\n"); + return usage(); + } + + // OUTPUT + if (i < argc) { + options->outputFileName = argv[i]; + i++; + } else { + // copy input into output and change the extension from .aidl to .java + options->outputFileName = options->inputFileName; + string::size_type pos = options->outputFileName.size()-5; + if (options->outputFileName.compare(pos, 5, ".aidl") == 0) { // 5 = strlen(".aidl") + options->outputFileName.replace(pos, 5, ".java"); // 5 = strlen(".aidl") + } else { + fprintf(stderr, "INPUT is not an .aidl file.\n"); + return usage(); + } + } + + // anything remaining? + if (i != argc) { + fprintf(stderr, "unknown option%s:", (i==argc-1?(const char*)"":(const char*)"s")); + for (; i<argc-1; i++) { + fprintf(stderr, " %s", argv[i]); + } + fprintf(stderr, "\n"); + return usage(); + } + + return 0; +} + diff --git a/tools/aidl/options.h b/tools/aidl/options.h new file mode 100644 index 0000000..dc3c45a --- /dev/null +++ b/tools/aidl/options.h @@ -0,0 +1,33 @@ +#ifndef DEVICE_TOOLS_AIDL_H +#define DEVICE_TOOLS_AIDL_H + +#include <string> +#include <vector> + +using namespace std; + +enum { + COMPILE_AIDL, + PREPROCESS_AIDL +}; + +// This struct is the parsed version of the command line options +struct Options +{ + int task; + bool failOnParcelable; + vector<string> importPaths; + vector<string> preprocessedFiles; + string inputFileName; + string outputFileName; + string depFileName; + + vector<string> filesToPreprocess; +}; + +// takes the inputs from the command line and fills in the Options struct +// Returns 0 on success, and nonzero on failure. +// It also prints the usage statement on failure. +int parse_options(int argc, const char* const* argv, Options *options); + +#endif // DEVICE_TOOLS_AIDL_H diff --git a/tools/aidl/options_test.cpp b/tools/aidl/options_test.cpp new file mode 100644 index 0000000..bd106ce --- /dev/null +++ b/tools/aidl/options_test.cpp @@ -0,0 +1,291 @@ +#include <iostream> +#include "options.h" + +const bool VERBOSE = false; + +using namespace std; + +struct Answer { + const char* argv[8]; + int result; + const char* systemSearchPath[8]; + const char* localSearchPath[8]; + const char* inputFileName; + language_t nativeLanguage; + const char* outputH; + const char* outputCPP; + const char* outputJava; +}; + +bool +match_arrays(const char* const*expected, const vector<string> &got) +{ + int count = 0; + while (expected[count] != NULL) { + count++; + } + if (got.size() != count) { + return false; + } + for (int i=0; i<count; i++) { + if (got[i] != expected[i]) { + return false; + } + } + return true; +} + +void +print_array(const char* prefix, const char* const*expected) +{ + while (*expected) { + cout << prefix << *expected << endl; + expected++; + } +} + +void +print_array(const char* prefix, const vector<string> &got) +{ + size_t count = got.size(); + for (size_t i=0; i<count; i++) { + cout << prefix << got[i] << endl; + } +} + +static int +test(const Answer& answer) +{ + int argc = 0; + while (answer.argv[argc]) { + argc++; + } + + int err = 0; + + Options options; + int result = parse_options(argc, answer.argv, &options); + + // result + if (((bool)result) != ((bool)answer.result)) { + cout << "mismatch: result: got " << result << " expected " << + answer.result << endl; + err = 1; + } + + if (result != 0) { + // if it failed, everything is invalid + return err; + } + + // systemSearchPath + if (!match_arrays(answer.systemSearchPath, options.systemSearchPath)) { + cout << "mismatch: systemSearchPath: got" << endl; + print_array(" ", options.systemSearchPath); + cout << " expected" << endl; + print_array(" ", answer.systemSearchPath); + err = 1; + } + + // localSearchPath + if (!match_arrays(answer.localSearchPath, options.localSearchPath)) { + cout << "mismatch: localSearchPath: got" << endl; + print_array(" ", options.localSearchPath); + cout << " expected" << endl; + print_array(" ", answer.localSearchPath); + err = 1; + } + + // inputFileName + if (answer.inputFileName != options.inputFileName) { + cout << "mismatch: inputFileName: got " << options.inputFileName + << " expected " << answer.inputFileName << endl; + err = 1; + } + + // nativeLanguage + if (answer.nativeLanguage != options.nativeLanguage) { + cout << "mismatch: nativeLanguage: got " << options.nativeLanguage + << " expected " << answer.nativeLanguage << endl; + err = 1; + } + + // outputH + if (answer.outputH != options.outputH) { + cout << "mismatch: outputH: got " << options.outputH + << " expected " << answer.outputH << endl; + err = 1; + } + + // outputCPP + if (answer.outputCPP != options.outputCPP) { + cout << "mismatch: outputCPP: got " << options.outputCPP + << " expected " << answer.outputCPP << endl; + err = 1; + } + + // outputJava + if (answer.outputJava != options.outputJava) { + cout << "mismatch: outputJava: got " << options.outputJava + << " expected " << answer.outputJava << endl; + err = 1; + } + + return err; +} + +const Answer g_tests[] = { + + { + /* argv */ { "test", "-i/moof", "-I/blah", "-Ibleh", "-imoo", "inputFileName.aidl_cpp", NULL, NULL }, + /* result */ 0, + /* systemSearchPath */ { "/blah", "bleh", NULL, NULL, NULL, NULL, NULL, NULL }, + /* localSearchPath */ { "/moof", "moo", NULL, NULL, NULL, NULL, NULL, NULL }, + /* inputFileName */ "inputFileName.aidl_cpp", + /* nativeLanguage */ CPP, + /* outputH */ "", + /* outputCPP */ "", + /* outputJava */ "" + }, + + { + /* argv */ { "test", "inputFileName.aidl_cpp", "-oh", "outputH", NULL, NULL, NULL, NULL }, + /* result */ 0, + /* systemSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* localSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* inputFileName */ "inputFileName.aidl_cpp", + /* nativeLanguage */ CPP, + /* outputH */ "outputH", + /* outputCPP */ "", + /* outputJava */ "" + }, + + { + /* argv */ { "test", "inputFileName.aidl_cpp", "-ocpp", "outputCPP", NULL, NULL, NULL, NULL }, + /* result */ 0, + /* systemSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* localSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* inputFileName */ "inputFileName.aidl_cpp", + /* nativeLanguage */ CPP, + /* outputH */ "", + /* outputCPP */ "outputCPP", + /* outputJava */ "" + }, + + { + /* argv */ { "test", "inputFileName.aidl_cpp", "-ojava", "outputJava", NULL, NULL, NULL, NULL }, + /* result */ 0, + /* systemSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* localSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* inputFileName */ "inputFileName.aidl_cpp", + /* nativeLanguage */ CPP, + /* outputH */ "", + /* outputCPP */ "", + /* outputJava */ "outputJava" + }, + + { + /* argv */ { "test", "inputFileName.aidl_cpp", "-oh", "outputH", "-ocpp", "outputCPP", "-ojava", "outputJava" }, + /* result */ 0, + /* systemSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* localSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* inputFileName */ "inputFileName.aidl_cpp", + /* nativeLanguage */ CPP, + /* outputH */ "outputH", + /* outputCPP */ "outputCPP", + /* outputJava */ "outputJava" + }, + + { + /* argv */ { "test", "inputFileName.aidl_cpp", "-oh", "outputH", "-oh", "outputH1", NULL, NULL }, + /* result */ 1, + /* systemSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* localSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* inputFileName */ "", + /* nativeLanguage */ CPP, + /* outputH */ "", + /* outputCPP */ "", + /* outputJava */ "" + }, + + { + /* argv */ { "test", "inputFileName.aidl_cpp", "-ocpp", "outputCPP", "-ocpp", "outputCPP1", NULL, NULL }, + /* result */ 1, + /* systemSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* localSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* inputFileName */ "", + /* nativeLanguage */ CPP, + /* outputH */ "", + /* outputCPP */ "", + /* outputJava */ "" + }, + + { + /* argv */ { "test", "inputFileName.aidl_cpp", "-ojava", "outputJava", "-ojava", "outputJava1", NULL, NULL }, + /* result */ 1, + /* systemSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* localSearchPath */ { NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL }, + /* inputFileName */ "", + /* nativeLanguage */ CPP, + /* outputH */ "", + /* outputCPP */ "", + /* outputJava */ "" + }, + +}; + +int +main(int argc, const char** argv) +{ + const int count = sizeof(g_tests)/sizeof(g_tests[0]); + int matches[count]; + + int result = 0; + for (int i=0; i<count; i++) { + if (VERBOSE) { + cout << endl; + cout << "---------------------------------------------" << endl; + const char* const* p = g_tests[i].argv; + while (*p) { + cout << " " << *p; + p++; + } + cout << endl; + cout << "---------------------------------------------" << endl; + } + matches[i] = test(g_tests[i]); + if (VERBOSE) { + if (0 == matches[i]) { + cout << "passed" << endl; + } else { + cout << "failed" << endl; + } + result |= matches[i]; + } + } + + cout << endl; + cout << "=============================================" << endl; + cout << "options_test summary" << endl; + cout << "=============================================" << endl; + + if (!result) { + cout << "passed" << endl; + } else { + cout << "failed the following tests:" << endl; + for (int i=0; i<count; i++) { + if (matches[i]) { + cout << " "; + const char* const* p = g_tests[i].argv; + while (*p) { + cout << " " << *p; + p++; + } + cout << endl; + } + } + } + + return result; +} + diff --git a/tools/aidl/search_path.cpp b/tools/aidl/search_path.cpp new file mode 100644 index 0000000..ffb6cb2 --- /dev/null +++ b/tools/aidl/search_path.cpp @@ -0,0 +1,57 @@ +#include <unistd.h> +#include "search_path.h" +#include "options.h" +#include <string.h> + +#ifdef HAVE_MS_C_RUNTIME +#include <io.h> +#endif + +static vector<string> g_importPaths; + +void +set_import_paths(const vector<string>& importPaths) +{ + g_importPaths = importPaths; +} + +char* +find_import_file(const char* given) +{ + string expected = given; + + int N = expected.length(); + for (int i=0; i<N; i++) { + char c = expected[i]; + if (c == '.') { + expected[i] = OS_PATH_SEPARATOR; + } + } + expected += ".aidl"; + + vector<string>& paths = g_importPaths; + for (vector<string>::iterator it=paths.begin(); it!=paths.end(); it++) { + string f = *it; + if (f.size() == 0) { + f = "."; + f += OS_PATH_SEPARATOR; + } + else if (f[f.size()-1] != OS_PATH_SEPARATOR) { + f += OS_PATH_SEPARATOR; + } + f.append(expected); + +#ifdef HAVE_MS_C_RUNTIME + /* check that the file exists and is not write-only */ + if (0 == _access(f.c_str(), 0) && /* mode 0=exist */ + 0 == _access(f.c_str(), 4) ) { /* mode 4=readable */ +#else + if (0 == access(f.c_str(), R_OK)) { +#endif + return strdup(f.c_str()); + } + } + + return NULL; +} + diff --git a/tools/aidl/search_path.h b/tools/aidl/search_path.h new file mode 100644 index 0000000..2bf94b1 --- /dev/null +++ b/tools/aidl/search_path.h @@ -0,0 +1,23 @@ +#ifndef DEVICE_TOOLS_AIDL_SEARCH_PATH_H +#define DEVICE_TOOLS_AIDL_SEARCH_PATH_H + +#include <stdio.h> + +#if __cplusplus +#include <vector> +#include <string> +using namespace std; +extern "C" { +#endif + +// returns a FILE* and the char* for the file that it found +// given is the class name we're looking for +char* find_import_file(const char* given); + +#if __cplusplus +}; // extern "C" +void set_import_paths(const vector<string>& importPaths); +#endif + +#endif // DEVICE_TOOLS_AIDL_SEARCH_PATH_H + diff --git a/tools/layoutlib/Android.mk b/tools/layoutlib/Android.mk new file mode 100644 index 0000000..6d606a9 --- /dev/null +++ b/tools/layoutlib/Android.mk @@ -0,0 +1,67 @@ +# +# Copyright (C) 2008 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. +# +LOCAL_PATH := $(my-dir) +include $(CLEAR_VARS) + +# +# Define rules to build temp_layoutlib.jar, which contains a subset of +# the classes in framework.jar. The layoutlib_create tool is used to +# transform the framework jar into the temp_layoutlib jar. +# + +# We need to process the framework classes.jar file, but we can't +# depend directly on it (private vars won't be inherited correctly). +# So, we depend on framework's BUILT file. +built_framework_dep := \ + $(call intermediates-dir-for,JAVA_LIBRARIES,framework)/javalib.jar +built_framework_classes := \ + $(call intermediates-dir-for,JAVA_LIBRARIES,framework)/classes.jar + +built_core_dep := \ + $(call intermediates-dir-for,JAVA_LIBRARIES,core)/javalib.jar +built_core_classes := \ + $(call intermediates-dir-for,JAVA_LIBRARIES,core)/classes.jar + +built_layoutlib_create_jar := $(call intermediates-dir-for, \ + JAVA_LIBRARIES,layoutlib_create,HOST)/javalib.jar + +# This is mostly a copy of config/host_java_library.mk +LOCAL_MODULE := temp_layoutlib +LOCAL_MODULE_CLASS := JAVA_LIBRARIES +LOCAL_MODULE_SUFFIX := $(COMMON_JAVA_PACKAGE_SUFFIX) +LOCAL_IS_HOST_MODULE := true +LOCAL_BUILT_MODULE_STEM := javalib.jar + +####################################### +include $(BUILD_SYSTEM)/base_rules.mk +####################################### + +$(LOCAL_BUILT_MODULE): $(built_core_dep) \ + $(built_framework_dep) \ + $(built_layoutlib_create_jar) + @echo "host layoutlib_create: $@" + @mkdir -p $(dir $@) + @rm -f $@ + $(hide) java -jar $(built_layoutlib_create_jar) \ + $@ \ + $(built_core_classes) \ + $(built_framework_classes) + + +# +# Include the subdir makefiles. +# +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tools/layoutlib/api/.classpath b/tools/layoutlib/api/.classpath new file mode 100644 index 0000000..a09ce5f --- /dev/null +++ b/tools/layoutlib/api/.classpath @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilt/common/kxml2/kxml2-2.3.0.jar" sourcepath="/ANDROID_SRC/dalvik/libcore/xml/src/main/java"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/tools/layoutlib/api/.project b/tools/layoutlib/api/.project new file mode 100644 index 0000000..4e4ca3b --- /dev/null +++ b/tools/layoutlib/api/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>layoutlib_api</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/tools/layoutlib/api/Android.mk b/tools/layoutlib/api/Android.mk new file mode 100644 index 0000000..d60987c --- /dev/null +++ b/tools/layoutlib/api/Android.mk @@ -0,0 +1,26 @@ +# +# Copyright (C) 2008 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under,src) + +LOCAL_JAVA_LIBRARIES := \ + kxml2-2.3.0 + +LOCAL_MODULE := layoutlib_api + +include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutBridge.java b/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutBridge.java new file mode 100644 index 0000000..df1876d --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutBridge.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2008 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.layoutlib.api; + +import java.util.Map; + +/** + * Entry point of the Layout Lib. Implementations of this interface provide a method to compute + * and render a layout. + * <p/> + * <p/>{@link #getApiLevel()} gives the ability to know which methods are available. + * <p/> + * Changes in API level 3: + * <ul> + * <li>{@link #computeLayout(IXmlPullParser, Object, int, int, int, float, float, String, boolean, Map, Map, IProjectCallback, ILayoutLog)}</li> + * <li> deprecated {@link #computeLayout(IXmlPullParser, Object, int, int, String, boolean, Map, Map, IProjectCallback, ILayoutLog)}</li> + * </ul> + * Changes in API level 2: + * <ul> + * <li>{@link #getApiLevel()}</li> + * <li>{@link #computeLayout(IXmlPullParser, Object, int, int, String, boolean, Map, Map, IProjectCallback, ILayoutLog)}</li> + * <li>deprecated {@link #computeLayout(IXmlPullParser, Object, int, int, String, Map, Map, IProjectCallback, ILayoutLog)}</li> + * </ul> + */ +public interface ILayoutBridge { + + final int API_CURRENT = 3; + + /** + * Returns the API level of the layout library. + * While no methods will ever be removed, some may become deprecated, and some new ones + * will appear. + * <p/>If calling this method throws an {@link AbstractMethodError}, then the API level + * should be considered to be 1. + */ + int getApiLevel(); + + /** + * Initializes the Bridge object. + * @param fontOsLocation the location of the fonts. + * @param enumValueMap map attrName => { map enumFlagName => Integer value }. + * @return true if success. + * @since 1 + */ + boolean init(String fontOsLocation, Map<String, Map<String, Integer>> enumValueMap); + + /** + * Computes and renders a layout + * @param layoutDescription the {@link IXmlPullParser} letting the LayoutLib Bridge visit the + * layout file. + * @param projectKey An Object identifying the project. This is used for the cache mechanism. + * @param screenWidth the screen width + * @param screenHeight the screen height + * @param density the density factor for the screen. + * @param xdpi the screen actual dpi in X + * @param ydpi the screen actual dpi in Y + * @param themeName The name of the theme to use. + * @param isProjectTheme true if the theme is a project theme, false if it is a framework theme. + * @param projectResources the resources of the project. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the + * map contains (String, {@link IResourceValue}) pairs where the key is the resource name, + * and the value is the resource value. + * @param frameworkResources the framework resources. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the map + * contains (String, {@link IResourceValue}) pairs where the key is the resource name, and the + * value is the resource value. + * @param projectCallback The {@link IProjectCallback} object to get information from + * the project. + * @param logger the object responsible for displaying warning/errors to the user. + * @return an {@link ILayoutResult} object that contains the result of the layout. + * @since 3 + */ + ILayoutResult computeLayout(IXmlPullParser layoutDescription, + Object projectKey, + int screenWidth, int screenHeight, int density, float xdpi, float ydpi, + String themeName, boolean isProjectTheme, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + IProjectCallback projectCallback, ILayoutLog logger); + + /** + * Computes and renders a layout + * @param layoutDescription the {@link IXmlPullParser} letting the LayoutLib Bridge visit the + * layout file. + * @param projectKey An Object identifying the project. This is used for the cache mechanism. + * @param screenWidth the screen width + * @param screenHeight the screen height + * @param themeName The name of the theme to use. + * @param isProjectTheme true if the theme is a project theme, false if it is a framework theme. + * @param projectResources the resources of the project. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the + * map contains (String, {@link IResourceValue}) pairs where the key is the resource name, + * and the value is the resource value. + * @param frameworkResources the framework resources. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the map + * contains (String, {@link IResourceValue}) pairs where the key is the resource name, and the + * value is the resource value. + * @param projectCallback The {@link IProjectCallback} object to get information from + * the project. + * @param logger the object responsible for displaying warning/errors to the user. + * @return an {@link ILayoutResult} object that contains the result of the layout. + * @deprecated Use {@link #computeLayout(IXmlPullParser, Object, int, int, int, float, float, String, boolean, Map, Map, IProjectCallback, ILayoutLog)} + * @since 2 + */ + @Deprecated + ILayoutResult computeLayout(IXmlPullParser layoutDescription, + Object projectKey, + int screenWidth, int screenHeight, String themeName, boolean isProjectTheme, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + IProjectCallback projectCallback, ILayoutLog logger); + + /** + * Computes and renders a layout + * @param layoutDescription the {@link IXmlPullParser} letting the LayoutLib Bridge visit the + * layout file. + * @param projectKey An Object identifying the project. This is used for the cache mechanism. + * @param screenWidth + * @param screenHeight + * @param themeName The name of the theme to use. In order to differentiate project and platform + * themes sharing the same name, all project themes must be prepended with a '*' character. + * @param projectResources the resources of the project. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the + * map contains (String, {@link IResourceValue}) pairs where the key is the resource name, + * and the value is the resource value. + * @param frameworkResources the framework resources. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the map + * contains (String, {@link IResourceValue}) pairs where the key is the resource name, and the + * value is the resource value. + * @param projectCallback The {@link IProjectCallback} object to get information from + * the project. + * @param logger the object responsible for displaying warning/errors to the user. + * @return an {@link ILayoutResult} object that contains the result of the layout. + * @deprecated Use {@link #computeLayout(IXmlPullParser, Object, int, int, int, float, float, String, boolean, Map, Map, IProjectCallback, ILayoutLog)} + * @since 1 + */ + @Deprecated + ILayoutResult computeLayout(IXmlPullParser layoutDescription, + Object projectKey, + int screenWidth, int screenHeight, String themeName, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + IProjectCallback projectCallback, ILayoutLog logger); + + /** + * Clears the resource cache for a specific project. + * <p/>This cache contains bitmaps and nine patches that are loaded from the disk and reused + * until this method is called. + * <p/>The cache is not configuration dependent and should only be cleared when a + * resource changes (at this time only bitmaps and 9 patches go into the cache). + * @param objectKey the key for the project. + * @since 1 + */ + void clearCaches(Object projectKey); +} diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutLog.java b/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutLog.java new file mode 100644 index 0000000..cae15d3 --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutLog.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2008 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.layoutlib.api; + +/** + * Callback interface to display warnings/errors that happened during the computation and + * rendering of the layout. + */ +public interface ILayoutLog { + + /** + * Displays a warning message. + * @param message the message to display. + */ + void warning(String message); + + /** + * Displays an error message. + * @param message the message to display. + */ + void error(String message); + + /** + * Displays an exception + * @param t the {@link Throwable} to display. + */ + void error(Throwable t); +} diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutResult.java b/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutResult.java new file mode 100644 index 0000000..5a06349 --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/ILayoutResult.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2008 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.layoutlib.api; + +import java.awt.image.BufferedImage; + +/** + * The result of a layout computation through + * {@link ILayoutLibBridge#computeLayout(IXmlPullParser, int, int, String, java.util.Map, java.util.Map, java.util.Map, IFontLoader, ILayoutLibLog, ICustomViewLoader)} + */ +public interface ILayoutResult { + /** Sucess return code */ + final static int SUCCESS = 0; + /** Error return code. + * <p/>See {@link #getErrorMessage()} + */ + final static int ERROR = 1; + + /** + * Returns the result code. + * @see #SUCCESS + * @see #ERROR + */ + int getSuccess(); + + /** + * Returns the {@link ILayoutViewInfo} object for the top level view. + */ + ILayoutViewInfo getRootView(); + + /** + * Returns the rendering of the full layout. + */ + BufferedImage getImage(); + + /** + * Returns the error message. + * <p/>Only valid when {@link #getSuccess()} returns {@link #ERROR} + */ + String getErrorMessage(); + + /** + * Layout information for a specific view. + */ + public interface ILayoutViewInfo { + + /** + * Returns the list of children views. + */ + ILayoutViewInfo[] getChildren(); + + /** + * Returns the key associated with the node. + * @see IXmlPullParser#getViewKey() + */ + Object getViewKey(); + + /** + * Returns the name of the view. + */ + String getName(); + + /** + * Returns the left of the view bounds. + */ + int getLeft(); + + /** + * Returns the top of the view bounds. + */ + int getTop(); + + /** + * Returns the right of the view bounds. + */ + int getRight(); + + /** + * Returns the bottom of the view bounds. + */ + int getBottom(); + } +} diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/IProjectCallback.java b/tools/layoutlib/api/src/com/android/layoutlib/api/IProjectCallback.java new file mode 100644 index 0000000..5ad5082 --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/IProjectCallback.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2008 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.layoutlib.api; + +/** + * Callback for project information needed by the Layout Library. + * Classes implementing this interface provide methods giving access to some project data, like + * resource resolution, namespace information, and instantiation of custom view. + */ +public interface IProjectCallback { + + /** + * Loads a custom view with the given constructor signature and arguments. + * @param name The fully qualified name of the class. + * @param constructorSignature The signature of the class to use + * @param constructorArgs The arguments to use on the constructor + * @return A newly instantiated android.view.View object. + * @throws ClassNotFoundException. + * @throws Exception + */ + @SuppressWarnings("unchecked") + Object loadView(String name, Class[] constructorSignature, Object[] constructorArgs) + throws ClassNotFoundException, Exception; + + /** + * Returns the namespace of the application. + * <p/>This lets the Layout Lib load custom attributes for custom views. + */ + String getNamespace(); + + /** + * Resolves the id of a resource Id. + * <p/>The resource id is the value of a <code>R.<type>.<name></code>, and + * this method will return both the type and name of the resource. + * @param id the Id to resolve. + * @return an array of 2 strings containing the resource name and type, or null if the id + * does not match any resource. + */ + String[] resolveResourceValue(int id); + + /** + * Resolves the id of a resource Id of type int[] + * <p/>The resource id is the value of a R.styleable.<name>, and this method will + * return the name of the resource. + * @param id the Id to resolve. + * @return the name of the resource or <code>null</code> if not found. + */ + String resolveResourceValue(int[] id); + + /** + * Returns the id of a resource. + * <p/>The provided type and name must match an existing constant defined as + * <code>R.<type>.<name></code>. + * @param type the type of the resource + * @param name the name of the resource + * @return an Integer containing the resource Id, or <code>null</code> if not found. + */ + Integer getResourceValue(String type, String name); + +} diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/IResourceValue.java b/tools/layoutlib/api/src/com/android/layoutlib/api/IResourceValue.java new file mode 100644 index 0000000..1da9508 --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/IResourceValue.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2008 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.layoutlib.api; + +/** + * Represents an android resource with a name and a string value. + */ +public interface IResourceValue { + + /** + * Returns the type of the resource. For instance "drawable", "color", etc... + */ + String getType(); + + /** + * Returns the name of the resource, as defined in the XML. + */ + String getName(); + + /** + * Returns the value of the resource, as defined in the XML. This can be <code>null</code> + */ + String getValue(); + + /** + * Returns whether the resource is a framework resource (<code>true</code>) or a project + * resource (<code>false</false>). + */ + boolean isFramework(); +} diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/IStyleResourceValue.java b/tools/layoutlib/api/src/com/android/layoutlib/api/IStyleResourceValue.java new file mode 100644 index 0000000..2f17e69 --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/IStyleResourceValue.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2008 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.layoutlib.api; + + +/** + * Represents an android style resources with a name and a list of children {@link IResourceValue}. + */ +public interface IStyleResourceValue extends IResourceValue { + + /** + * Returns the parent style name or <code>null</code> if unknown. + */ + String getParentStyle(); + + /** + * Find an item in the list by name + * @param name + */ + IResourceValue findItem(String name); +} diff --git a/tools/layoutlib/api/src/com/android/layoutlib/api/IXmlPullParser.java b/tools/layoutlib/api/src/com/android/layoutlib/api/IXmlPullParser.java new file mode 100644 index 0000000..cd43c56 --- /dev/null +++ b/tools/layoutlib/api/src/com/android/layoutlib/api/IXmlPullParser.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2008 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.layoutlib.api; + +import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; + +import org.xmlpull.v1.XmlPullParser; + +/** + * Extended version of {@link XmlPullParser} to use with + * {@link ILayoutLibBridge#computeLayout(XmlPullParser, int, int, String, java.util.Map, java.util.Map, java.util.Map, com.android.layoutlib.api.ILayoutLibBridge.IFontInfo)} + */ +public interface IXmlPullParser extends XmlPullParser { + + /** + * Returns a key for the current XML node. + * <p/>This key will be passed back in the {@link ILayoutViewInfo} objects, allowing association + * of a particular XML node with its result from the layout computation. + */ + Object getViewKey(); +} + diff --git a/tools/layoutlib/bridge/.classpath b/tools/layoutlib/bridge/.classpath new file mode 100644 index 0000000..175a98b --- /dev/null +++ b/tools/layoutlib/bridge/.classpath @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry excluding="org/kxml2/io/" kind="src" path="src"/> + <classpathentry kind="src" path="tests"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/> + <classpathentry combineaccessrules="false" kind="src" path="/layoutlib_api"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilt/common/kxml2/kxml2-2.3.0.jar" sourcepath="/ANDROID_SRC/dalvik/libcore/xml/src/main/java"/> + <classpathentry kind="var" path="ANDROID_OUT_FRAMEWORK/layoutlib.jar" sourcepath="/ANDROID_SRC/frameworks/base/core/java"/> + <classpathentry kind="var" path="ANDROID_OUT_FRAMEWORK/ninepatch.jar" sourcepath="/ANDROID_SRC/development/tools/ninepatch/src"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/tools/layoutlib/bridge/.project b/tools/layoutlib/bridge/.project new file mode 100644 index 0000000..e36e71b --- /dev/null +++ b/tools/layoutlib/bridge/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>layoutlib_bridge</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/tools/layoutlib/bridge/Android.mk b/tools/layoutlib/bridge/Android.mk new file mode 100644 index 0000000..b2010d5 --- /dev/null +++ b/tools/layoutlib/bridge/Android.mk @@ -0,0 +1,31 @@ +# +# Copyright (C) 2008 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under,src) + +LOCAL_JAVA_LIBRARIES := \ + kxml2-2.3.0 \ + layoutlib_api \ + ninepatch + +LOCAL_STATIC_JAVA_LIBRARIES := temp_layoutlib + +LOCAL_MODULE := layoutlib + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/tools/layoutlib/bridge/src/android/graphics/Bitmap.java b/tools/layoutlib/bridge/src/android/graphics/Bitmap.java new file mode 100644 index 0000000..6bc01b1 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Bitmap.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2008 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 android.graphics; + +import com.android.layoutlib.bridge.BridgeCanvas; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import javax.imageio.ImageIO; + +public final class Bitmap extends _Original_Bitmap { + + private BufferedImage mImage; + + public Bitmap(File input) throws IOException { + super(1, true, null); + + mImage = ImageIO.read(input); + } + + Bitmap(BufferedImage image) { + super(1, true, null); + mImage = image; + } + + public BufferedImage getImage() { + return mImage; + } + + // ----- overriden methods + + public enum Config { + // these native values must match up with the enum in SkBitmap.h + ALPHA_8 (2), + RGB_565 (4), + ARGB_4444 (5), + ARGB_8888 (6); + + Config(int ni) { + this.nativeInt = ni; + } + final int nativeInt; + + /* package */ static Config nativeToConfig(int ni) { + return sConfigs[ni]; + } + + private static Config sConfigs[] = { + null, null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888 + }; + } + + + @Override + public int getWidth() { + return mImage.getWidth(); + } + + @Override + public int getHeight() { + return mImage.getHeight(); + } + + /** + * Returns an immutable bitmap from the source bitmap. The new bitmap may + * be the same object as source, or a copy may have been made. + */ + public static Bitmap createBitmap(Bitmap src) { + return createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), null, false); + } + + /** + * Returns an immutable bitmap from the specified subset of the source + * bitmap. The new bitmap may be the same object as source, or a copy may + * have been made. + * + * @param source The bitmap we are subsetting + * @param x The x coordinate of the first pixel in source + * @param y The y coordinate of the first pixel in source + * @param width The number of pixels in each row + * @param height The number of rows + */ + public static Bitmap createBitmap(Bitmap source, int x, int y, + int width, int height) { + return new Bitmap(source.mImage.getSubimage(x, y, width, height)); + } + + /** + * Returns an immutable bitmap from subset of the source bitmap, + * transformed by the optional matrix. + * + * @param source The bitmap we are subsetting + * @param x The x coordinate of the first pixel in source + * @param y The y coordinate of the first pixel in source + * @param width The number of pixels in each row + * @param height The number of rows + * @param m Option matrix to be applied to the pixels + * @param filter true if the source should be filtered. + * Only applies if the matrix contains more than just + * translation. + * @return A bitmap that represents the specified subset of source + * @throws IllegalArgumentException if the x, y, width, height values are + * outside of the dimensions of the source bitmap. + */ + public static Bitmap createBitmap(Bitmap source, int x, int y, int width, + int height, Matrix m, boolean filter) { + checkXYSign(x, y); + checkWidthHeight(width, height); + if (x + width > source.getWidth()) { + throw new IllegalArgumentException( + "x + width must be <= bitmap.width()"); + } + if (y + height > source.getHeight()) { + throw new IllegalArgumentException( + "y + height must be <= bitmap.height()"); + } + + // check if we can just return our argument unchanged + if (!source.isMutable() && x == 0 && y == 0 + && width == source.getWidth() && height == source.getHeight() + && (m == null || m.isIdentity())) { + return source; + } + + if (m == null || m.isIdentity()) { + return new Bitmap(source.mImage.getSubimage(x, y, width, height)); + } + + int neww = width; + int newh = height; + Paint paint; + + Rect srcR = new Rect(x, y, x + width, y + height); + RectF dstR = new RectF(0, 0, width, height); + + /* the dst should have alpha if the src does, or if our matrix + doesn't preserve rectness + */ + boolean hasAlpha = source.hasAlpha() || !m.rectStaysRect(); + RectF deviceR = new RectF(); + m.mapRect(deviceR, dstR); + neww = Math.round(deviceR.width()); + newh = Math.round(deviceR.height()); + + BridgeCanvas canvas = new BridgeCanvas(neww, newh); + + canvas.translate(-deviceR.left, -deviceR.top); + canvas.concat(m); + paint = new Paint(); + paint.setFilterBitmap(filter); + if (!m.rectStaysRect()) { + paint.setAntiAlias(true); + } + + canvas.drawBitmap(source, srcR, dstR, paint); + + return new Bitmap(canvas.getImage()); + } + + /** + * Returns a mutable bitmap with the specified width and height. + * + * @param width The width of the bitmap + * @param height The height of the bitmap + * @param config The bitmap config to create. + * @throws IllegalArgumentException if the width or height are <= 0 + */ + public static Bitmap createBitmap(int width, int height, Config config) { + return new Bitmap(new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)); + } + + /** + * Returns a immutable bitmap with the specified width and height, with each + * pixel value set to the corresponding value in the colors array. + * + * @param colors Array of {@link Color} used to initialize the pixels. + * @param offset Number of values to skip before the first color in the + * array of colors. + * @param stride Number of colors in the array between rows (must be >= + * width or <= -width). + * @param width The width of the bitmap + * @param height The height of the bitmap + * @param config The bitmap config to create. If the config does not + * support per-pixel alpha (e.g. RGB_565), then the alpha + * bytes in the colors[] will be ignored (assumed to be FF) + * @throws IllegalArgumentException if the width or height are <= 0, or if + * the color array's length is less than the number of pixels. + */ + public static Bitmap createBitmap(int colors[], int offset, int stride, + int width, int height, Config config) { + checkWidthHeight(width, height); + if (Math.abs(stride) < width) { + throw new IllegalArgumentException("abs(stride) must be >= width"); + } + int lastScanline = offset + (height - 1) * stride; + int length = colors.length; + if (offset < 0 || (offset + width > length) + || lastScanline < 0 + || (lastScanline + width > length)) { + throw new ArrayIndexOutOfBoundsException(); + } + + // TODO: create an immutable bitmap... + throw new UnsupportedOperationException(); + } + + /** + * Returns a immutable bitmap with the specified width and height, with each + * pixel value set to the corresponding value in the colors array. + * + * @param colors Array of {@link Color} used to initialize the pixels. + * This array must be at least as large as width * height. + * @param width The width of the bitmap + * @param height The height of the bitmap + * @param config The bitmap config to create. If the config does not + * support per-pixel alpha (e.g. RGB_565), then the alpha + * bytes in the colors[] will be ignored (assumed to be FF) + * @throws IllegalArgumentException if the width or height are <= 0, or if + * the color array's length is less than the number of pixels. + */ + public static Bitmap createBitmap(int colors[], int width, int height, + Config config) { + return createBitmap(colors, 0, width, width, height, config); + } + +} diff --git a/tools/layoutlib/bridge/src/android/graphics/BitmapShader.java b/tools/layoutlib/bridge/src/android/graphics/BitmapShader.java new file mode 100644 index 0000000..8bf7fcc --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/BitmapShader.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2008 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 android.graphics; + +public class BitmapShader extends Shader { + + // we hold on just for the GC, since our native counterpart is using it + private final Bitmap mBitmap; + + /** + * Call this to create a new shader that will draw with a bitmap. + * + * @param bitmap The bitmap to use inside the shader + * @param tileX The tiling mode for x to draw the bitmap in. + * @param tileY The tiling mode for y to draw the bitmap in. + */ + public BitmapShader(Bitmap bitmap, TileMode tileX, TileMode tileY) { + mBitmap = bitmap; + } + + //---------- Custom methods + + public Bitmap getBitmap() { + return mBitmap; + } +} + diff --git a/tools/layoutlib/bridge/src/android/graphics/ComposeShader.java b/tools/layoutlib/bridge/src/android/graphics/ComposeShader.java new file mode 100644 index 0000000..968a597 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/ComposeShader.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2008 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 android.graphics; + +/** A subclass of shader that returns the composition of two other shaders, combined by + an {@link android.graphics.Xfermode} subclass. +*/ +public class ComposeShader extends Shader { + /** Create a new compose shader, given shaders A, B, and a combining mode. + When the mode is applied, it will be given the result from shader A as its + "dst", and the result of from shader B as its "src". + @param shaderA The colors from this shader are seen as the "dst" by the mode + @param shaderB The colors from this shader are seen as the "src" by the mode + @param mode The mode that combines the colors from the two shaders. If mode + is null, then SRC_OVER is assumed. + */ + public ComposeShader(Shader shaderA, Shader shaderB, Xfermode mode) { + // FIXME Implement shader + } + + /** Create a new compose shader, given shaders A, B, and a combining PorterDuff mode. + When the mode is applied, it will be given the result from shader A as its + "dst", and the result of from shader B as its "src". + @param shaderA The colors from this shader are seen as the "dst" by the mode + @param shaderB The colors from this shader are seen as the "src" by the mode + @param mode The PorterDuff mode that combines the colors from the two shaders. + */ + public ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode) { + // FIXME Implement shader + } +} + diff --git a/tools/layoutlib/bridge/src/android/graphics/LinearGradient.java b/tools/layoutlib/bridge/src/android/graphics/LinearGradient.java new file mode 100644 index 0000000..1a0dc05 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/LinearGradient.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2008 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 android.graphics; + +import java.awt.GradientPaint; +import java.awt.Color; +import java.awt.Paint; + +public class LinearGradient extends Shader { + + private GradientPaint mGradientPaint; + + /** Create a shader that draws a linear gradient along a line. + @param x0 The x-coordinate for the start of the gradient line + @param y0 The y-coordinate for the start of the gradient line + @param x1 The x-coordinate for the end of the gradient line + @param y1 The y-coordinate for the end of the gradient line + @param colors The colors to be distributed along the gradient line + @param positions May be null. The relative positions [0..1] of + each corresponding color in the colors array. If this is null, + the the colors are distributed evenly along the gradient line. + @param tile The Shader tiling mode + */ + public LinearGradient(float x0, float y0, float x1, float y1, + int colors[], float positions[], TileMode tile) { + if (colors.length < 2) { + throw new IllegalArgumentException("needs >= 2 number of colors"); + } + if (positions != null && colors.length != positions.length) { + throw new IllegalArgumentException("color and position arrays must be of equal length"); + } + + // FIXME implement multi color linear gradient + } + + /** Create a shader that draws a linear gradient along a line. + @param x0 The x-coordinate for the start of the gradient line + @param y0 The y-coordinate for the start of the gradient line + @param x1 The x-coordinate for the end of the gradient line + @param y1 The y-coordinate for the end of the gradient line + @param color0 The color at the start of the gradient line. + @param color1 The color at the end of the gradient line. + @param tile The Shader tiling mode + */ + public LinearGradient(float x0, float y0, float x1, float y1, + int color0, int color1, TileMode tile) { + mGradientPaint = new GradientPaint(x0, y0, new Color(color0, true /* hasalpha */), + x1,y1, new Color(color1, true /* hasalpha */), tile != TileMode.CLAMP); + } + + //---------- Custom Methods + + public Paint getPaint() { + return mGradientPaint; + } +} + diff --git a/tools/layoutlib/bridge/src/android/graphics/Matrix.java b/tools/layoutlib/bridge/src/android/graphics/Matrix.java new file mode 100644 index 0000000..3f9a993 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Matrix.java @@ -0,0 +1,984 @@ +/* + * Copyright (C) 2008 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 android.graphics; + +import java.awt.geom.AffineTransform; + + +/** + * A matrix implementation overridden by the LayoutLib bridge. + */ +public class Matrix extends _Original_Matrix { + + float mValues[] = new float[9]; + + /** + * Create an identity matrix + */ + public Matrix() { + reset(); + } + + /** + * Create a matrix that is a (deep) copy of src + * @param src The matrix to copy into this matrix + */ + public Matrix(Matrix src) { + set(src); + } + + /** + * Creates a Matrix object from the float array. The array becomes the internal storage + * of the object. + * @param data + */ + private Matrix(float[] data) { + assert data.length != 9; + mValues = data; + } + + @Override + public void finalize() throws Throwable { + // pass + } + + //---------- Custom Methods + + /** + * Adds the given transformation to the current Matrix + * <p/>This in effect does this = this*matrix + * @param matrix + */ + private void addTransform(float[] matrix) { + float[] tmp = new float[9]; + + // first row + tmp[0] = matrix[0] * mValues[0] + matrix[1] * mValues[3] + matrix[2] * mValues[6]; + tmp[1] = matrix[0] * mValues[1] + matrix[1] * mValues[4] + matrix[2] * mValues[7]; + tmp[2] = matrix[0] * mValues[2] + matrix[1] * mValues[5] + matrix[2] * mValues[8]; + + // 2nd row + tmp[3] = matrix[3] * mValues[0] + matrix[4] * mValues[3] + matrix[5] * mValues[6]; + tmp[4] = matrix[3] * mValues[1] + matrix[4] * mValues[4] + matrix[5] * mValues[7]; + tmp[5] = matrix[3] * mValues[2] + matrix[4] * mValues[5] + matrix[5] * mValues[8]; + + // 3rd row + tmp[6] = matrix[6] * mValues[0] + matrix[7] * mValues[3] + matrix[8] * mValues[6]; + tmp[7] = matrix[6] * mValues[1] + matrix[7] * mValues[4] + matrix[8] * mValues[7]; + tmp[8] = matrix[6] * mValues[2] + matrix[7] * mValues[5] + matrix[8] * mValues[8]; + + // copy the result over to mValues + mValues = tmp; + } + + public AffineTransform getTransform() { + return new AffineTransform(mValues[0], mValues[1], mValues[2], + mValues[3], mValues[4], mValues[5]); + } + + public boolean hasPerspective() { + return (mValues[6] != 0 || mValues[7] != 0 || mValues[8] != 1); + } + + //---------- + + /** + * Returns true if the matrix is identity. + * This maybe faster than testing if (getType() == 0) + */ + @Override + public boolean isIdentity() { + for (int i = 0, k = 0; i < 3; i++) { + for (int j = 0; j < 3; j++, k++) { + if (mValues[k] != ((i==j) ? 1 : 0)) { + return false; + } + } + } + + return true; + } + + /** + * Returns true if will map a rectangle to another rectangle. This can be + * true if the matrix is identity, scale-only, or rotates a multiple of 90 + * degrees. + */ + @Override + public boolean rectStaysRect() { + return (computeTypeMask() & kRectStaysRect_Mask) != 0; + } + + /** + * (deep) copy the src matrix into this matrix. If src is null, reset this + * matrix to the identity matrix. + */ + public void set(Matrix src) { + if (src == null) { + reset(); + } else { + System.arraycopy(src.mValues, 0, mValues, 0, mValues.length); + } + } + + @Override + public void set(_Original_Matrix src) { + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + /** Returns true if obj is a Matrix and its values equal our values. + */ + @Override + public boolean equals(Object obj) { + if (obj != null && obj instanceof Matrix) { + Matrix matrix = (Matrix)obj; + for (int i = 0 ; i < 9 ; i++) { + if (mValues[i] != matrix.mValues[i]) { + return false; + } + } + + return true; + } + + return false; + } + + /** Set the matrix to identity */ + @Override + public void reset() { + for (int i = 0, k = 0; i < 3; i++) { + for (int j = 0; j < 3; j++, k++) { + mValues[k] = ((i==j) ? 1 : 0); + } + } + } + + /** Set the matrix to translate by (dx, dy). */ + @Override + public void setTranslate(float dx, float dy) { + mValues[0] = 1; + mValues[1] = 0; + mValues[2] = dx; + mValues[3] = 0; + mValues[4] = 1; + mValues[5] = dy; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + } + + /** + * Set the matrix to scale by sx and sy, with a pivot point at (px, py). + * The pivot point is the coordinate that should remain unchanged by the + * specified transformation. + */ + @Override + public void setScale(float sx, float sy, float px, float py) { + // TODO: do it in one pass + + // translate so that the pivot is in 0,0 + mValues[0] = 1; + mValues[1] = 0; + mValues[2] = -px; + mValues[3] = 0; + mValues[4] = 1; + mValues[5] = -py; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + + // scale + addTransform(new float[] { sx, 0, 0, 0, sy, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + } + + /** Set the matrix to scale by sx and sy. */ + @Override + public void setScale(float sx, float sy) { + mValues[0] = sx; + mValues[1] = 0; + mValues[2] = 0; + mValues[3] = 0; + mValues[4] = sy; + mValues[5] = 0; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + } + + /** + * Set the matrix to rotate by the specified number of degrees, with a pivot + * point at (px, py). The pivot point is the coordinate that should remain + * unchanged by the specified transformation. + */ + @Override + public void setRotate(float degrees, float px, float py) { + // TODO: do it in one pass + + // translate so that the pivot is in 0,0 + mValues[0] = 1; + mValues[1] = 0; + mValues[2] = -px; + mValues[3] = 0; + mValues[4] = 1; + mValues[5] = -py; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + + // scale + double rad = Math.toRadians(degrees); + float cos = (float)Math.cos(rad); + float sin = (float)Math.sin(rad); + addTransform(new float[] { cos, -sin, 0, sin, cos, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + } + + /** + * Set the matrix to rotate about (0,0) by the specified number of degrees. + */ + @Override + public void setRotate(float degrees) { + double rad = Math.toRadians(degrees); + float cos = (float)Math.cos(rad); + float sin = (float)Math.sin(rad); + + mValues[0] = cos; + mValues[1] = -sin; + mValues[2] = 0; + mValues[3] = sin; + mValues[4] = cos; + mValues[5] = 0; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + } + + /** + * Set the matrix to rotate by the specified sine and cosine values, with a + * pivot point at (px, py). The pivot point is the coordinate that should + * remain unchanged by the specified transformation. + */ + @Override + public void setSinCos(float sinValue, float cosValue, float px, float py) { + // TODO: do it in one pass + + // translate so that the pivot is in 0,0 + mValues[0] = 1; + mValues[1] = 0; + mValues[2] = -px; + mValues[3] = 0; + mValues[4] = 1; + mValues[5] = -py; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + + // scale + addTransform(new float[] { cosValue, -sinValue, 0, sinValue, cosValue, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + } + + /** Set the matrix to rotate by the specified sine and cosine values. */ + @Override + public void setSinCos(float sinValue, float cosValue) { + mValues[0] = cosValue; + mValues[1] = -sinValue; + mValues[2] = 0; + mValues[3] = sinValue; + mValues[4] = cosValue; + mValues[5] = 0; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + } + + /** + * Set the matrix to skew by sx and sy, with a pivot point at (px, py). + * The pivot point is the coordinate that should remain unchanged by the + * specified transformation. + */ + @Override + public void setSkew(float kx, float ky, float px, float py) { + // TODO: do it in one pass + + // translate so that the pivot is in 0,0 + mValues[0] = 1; + mValues[1] = 0; + mValues[2] = -px; + mValues[3] = 0; + mValues[4] = 1; + mValues[5] = -py; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + + // scale + addTransform(new float[] { 1, kx, 0, ky, 1, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + } + + /** Set the matrix to skew by sx and sy. */ + @Override + public void setSkew(float kx, float ky) { + mValues[0] = 1; + mValues[1] = kx; + mValues[2] = -0; + mValues[3] = ky; + mValues[4] = 1; + mValues[5] = 0; + mValues[6] = 0; + mValues[7] = 0; + mValues[7] = 1; + } + + /** + * Set the matrix to the concatenation of the two specified matrices, + * returning true if the the result can be represented. Either of the two + * matrices may also be the target matrix. this = a * b + */ + public boolean setConcat(Matrix a, Matrix b) { + if (a == this) { + preConcat(b); + } else if (b == this) { + postConcat(b); + } else { + Matrix tmp = new Matrix(b); + tmp.addTransform(a.mValues); + set(tmp); + } + + return true; + } + + @Override + public boolean setConcat(_Original_Matrix a, _Original_Matrix b) { + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + /** + * Preconcats the matrix with the specified translation. + * M' = M * T(dx, dy) + */ + @Override + public boolean preTranslate(float dx, float dy) { + // create a matrix that will be multiply by this + Matrix m = new Matrix(new float[] { 1, 0, dx, 0, 1, dy, 0, 0, 1 }); + m.addTransform(this.mValues); + + System.arraycopy(m.mValues, 0, mValues, 0, 9); + return true; + } + + /** + * Preconcats the matrix with the specified scale. + * M' = M * S(sx, sy, px, py) + */ + @Override + public boolean preScale(float sx, float sy, float px, float py) { + Matrix m = new Matrix(); + m.setScale(sx, sy, px, py); + m.addTransform(mValues); + set(m); + + return true; + } + + /** + * Preconcats the matrix with the specified scale. + * M' = M * S(sx, sy) + */ + @Override + public boolean preScale(float sx, float sy) { + Matrix m = new Matrix(); + m.setScale(sx, sy); + m.addTransform(mValues); + set(m); + + return true; + } + + /** + * Preconcats the matrix with the specified rotation. + * M' = M * R(degrees, px, py) + */ + @Override + public boolean preRotate(float degrees, float px, float py) { + Matrix m = new Matrix(); + m.setRotate(degrees, px, py); + m.addTransform(mValues); + set(m); + + return true; + } + + /** + * Preconcats the matrix with the specified rotation. + * M' = M * R(degrees) + */ + @Override + public boolean preRotate(float degrees) { + Matrix m = new Matrix(); + m.setRotate(degrees); + m.addTransform(mValues); + set(m); + + return true; + } + + /** + * Preconcats the matrix with the specified skew. + * M' = M * K(kx, ky, px, py) + */ + @Override + public boolean preSkew(float kx, float ky, float px, float py) { + Matrix m = new Matrix(); + m.setSkew(kx, ky, px, py); + m.addTransform(mValues); + set(m); + + return true; + } + + /** + * Preconcats the matrix with the specified skew. + * M' = M * K(kx, ky) + */ + @Override + public boolean preSkew(float kx, float ky) { + Matrix m = new Matrix(); + m.setSkew(kx, ky); + m.addTransform(mValues); + set(m); + + return true; + } + + /** + * Preconcats the matrix with the specified matrix. + * M' = M * other + */ + public boolean preConcat(Matrix other) { + Matrix m = new Matrix(other); + other.addTransform(mValues); + set(m); + + return true; + } + + @Override + public boolean preConcat(_Original_Matrix other) { + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + /** + * Postconcats the matrix with the specified translation. + * M' = T(dx, dy) * M + */ + @Override + public boolean postTranslate(float dx, float dy) { + addTransform(new float[] { 1, 0, dx, 0, 1, dy, 0, 0, 1 }); + return true; + } + + /** + * Postconcats the matrix with the specified scale. + * M' = S(sx, sy, px, py) * M + */ + @Override + public boolean postScale(float sx, float sy, float px, float py) { + // TODO: do it in one pass + // translate so that the pivot is in 0,0 + addTransform(new float[] { 1, 0, -px, 0, 1, py, 0, 0, 1 }); + // scale + addTransform(new float[] { sx, 0, 0, 0, sy, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + + return true; + } + + /** + * Postconcats the matrix with the specified scale. + * M' = S(sx, sy) * M + */ + @Override + public boolean postScale(float sx, float sy) { + addTransform(new float[] { sx, 0, 0, 0, sy, 0, 0, 0, 1 }); + return true; + } + + /** + * Postconcats the matrix with the specified rotation. + * M' = R(degrees, px, py) * M + */ + @Override + public boolean postRotate(float degrees, float px, float py) { + // TODO: do it in one pass + // translate so that the pivot is in 0,0 + addTransform(new float[] { 1, 0, -px, 0, 1, py, 0, 0, 1 }); + // scale + double rad = Math.toRadians(degrees); + float cos = (float)Math.cos(rad); + float sin = (float)Math.sin(rad); + addTransform(new float[] { cos, -sin, 0, sin, cos, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + + return true; + } + + /** + * Postconcats the matrix with the specified rotation. + * M' = R(degrees) * M + */ + @Override + public boolean postRotate(float degrees) { + double rad = Math.toRadians(degrees); + float cos = (float)Math.cos(rad); + float sin = (float)Math.sin(rad); + addTransform(new float[] { cos, -sin, 0, sin, cos, 0, 0, 0, 1 }); + + return true; + } + + /** + * Postconcats the matrix with the specified skew. + * M' = K(kx, ky, px, py) * M + */ + @Override + public boolean postSkew(float kx, float ky, float px, float py) { + // TODO: do it in one pass + // translate so that the pivot is in 0,0 + addTransform(new float[] { 1, 0, -px, 0, 1, py, 0, 0, 1 }); + // scale + addTransform(new float[] { 1, kx, 0, ky, 1, 0, 0, 0, 1 }); + // translate back the pivot + addTransform(new float[] { 1, 0, px, 0, 1, py, 0, 0, 1 }); + + return true; + } + + /** + * Postconcats the matrix with the specified skew. + * M' = K(kx, ky) * M + */ + @Override + public boolean postSkew(float kx, float ky) { + addTransform(new float[] { 1, kx, 0, ky, 1, 0, 0, 0, 1 }); + + return true; + } + + /** + * Postconcats the matrix with the specified matrix. + * M' = other * M + */ + public boolean postConcat(Matrix other) { + addTransform(other.mValues); + + return true; + } + + @Override + public boolean postConcat(_Original_Matrix other) { + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + /** Controlls how the src rect should align into the dst rect for + setRectToRect(). + */ + public enum ScaleToFit { + /** + * Scale in X and Y independently, so that src matches dst exactly. + * This may change the aspect ratio of the src. + */ + FILL (0), + /** + * Compute a scale that will maintain the original src aspect ratio, + * but will also ensure that src fits entirely inside dst. At least one + * axis (X or Y) will fit exactly. START aligns the result to the + * left and top edges of dst. + */ + START (1), + /** + * Compute a scale that will maintain the original src aspect ratio, + * but will also ensure that src fits entirely inside dst. At least one + * axis (X or Y) will fit exactly. The result is centered inside dst. + */ + CENTER (2), + /** + * Compute a scale that will maintain the original src aspect ratio, + * but will also ensure that src fits entirely inside dst. At least one + * axis (X or Y) will fit exactly. END aligns the result to the + * right and bottom edges of dst. + */ + END (3); + + // the native values must match those in SkMatrix.h + ScaleToFit(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + /** + * Set the matrix to the scale and translate values that map the source + * rectangle to the destination rectangle, returning true if the result + * can be represented. + * + * @param src the source rectangle to map from. + * @param dst the destination rectangle to map to. + * @param stf the ScaleToFit option + * @return true if the matrix can be represented by the rectangle mapping. + */ + public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) { + if (dst == null || src == null) { + throw new NullPointerException(); + } + + if (src.isEmpty()) { + reset(); + return false; + } + + if (dst.isEmpty()) { + mValues[0] = mValues[1] = mValues[2] = mValues[3] = mValues[4] = mValues[5] + = mValues[6] = mValues[7] = 0; + mValues[8] = 1; + } else { + float tx, sx = dst.width() / src.width(); + float ty, sy = dst.height() / src.height(); + boolean xLarger = false; + + if (stf != ScaleToFit.FILL) { + if (sx > sy) { + xLarger = true; + sx = sy; + } else { + sy = sx; + } + } + + tx = dst.left - src.left * sx; + ty = dst.top - src.top * sy; + if (stf == ScaleToFit.CENTER || stf == ScaleToFit.END) { + float diff; + + if (xLarger) { + diff = dst.width() - src.width() * sy; + } else { + diff = dst.height() - src.height() * sy; + } + + if (stf == ScaleToFit.CENTER) { + diff = diff / 2; + } + + if (xLarger) { + tx += diff; + } else { + ty += diff; + } + } + + mValues[0] = sx; + mValues[4] = sy; + mValues[2] = tx; + mValues[5] = ty; + mValues[1] = mValues[3] = mValues[6] = mValues[7] = 0; + + } + // shared cleanup + mValues[8] = 1; + return true; + } + + @Override + public boolean setRectToRect(RectF src, RectF dst, _Original_Matrix.ScaleToFit stf) { + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + /** + * Set the matrix such that the specified src points would map to the + * specified dst points. The "points" are represented as an array of floats, + * order [x0, y0, x1, y1, ...], where each "point" is 2 float values. + * + * @param src The array of src [x,y] pairs (points) + * @param srcIndex Index of the first pair of src values + * @param dst The array of dst [x,y] pairs (points) + * @param dstIndex Index of the first pair of dst values + * @param pointCount The number of pairs/points to be used. Must be [0..4] + * @return true if the matrix was set to the specified transformation + */ + @Override + public boolean setPolyToPoly(float[] src, int srcIndex, + float[] dst, int dstIndex, + int pointCount) { + if (pointCount > 4) { + throw new IllegalArgumentException(); + } + checkPointArrays(src, srcIndex, dst, dstIndex, pointCount); + throw new UnsupportedOperationException("STUB NEEDED"); + } + + /** + * If this matrix can be inverted, return true and if inverse is not null, + * set inverse to be the inverse of this matrix. If this matrix cannot be + * inverted, ignore inverse and return false. + */ + public boolean invert(Matrix inverse) { + throw new UnsupportedOperationException("STUB NEEDED"); + } + + @Override + public boolean invert(_Original_Matrix inverse) { + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + /** + * Apply this matrix to the array of 2D points specified by src, and write + * the transformed points into the array of points specified by dst. The + * two arrays represent their "points" as pairs of floats [x, y]. + * + * @param dst The array of dst points (x,y pairs) + * @param dstIndex The index of the first [x,y] pair of dst floats + * @param src The array of src points (x,y pairs) + * @param srcIndex The index of the first [x,y] pair of src floats + * @param pointCount The number of points (x,y pairs) to transform + */ + @Override + public void mapPoints(float[] dst, int dstIndex, float[] src, int srcIndex, + int pointCount) { + checkPointArrays(src, srcIndex, dst, dstIndex, pointCount); + throw new UnsupportedOperationException("STUB NEEDED"); + } + + /** + * Apply this matrix to the array of 2D vectors specified by src, and write + * the transformed vectors into the array of vectors specified by dst. The + * two arrays represent their "vectors" as pairs of floats [x, y]. + * + * @param dst The array of dst vectors (x,y pairs) + * @param dstIndex The index of the first [x,y] pair of dst floats + * @param src The array of src vectors (x,y pairs) + * @param srcIndex The index of the first [x,y] pair of src floats + * @param vectorCount The number of vectors (x,y pairs) to transform + */ + @Override + public void mapVectors(float[] dst, int dstIndex, float[] src, int srcIndex, + int vectorCount) { + checkPointArrays(src, srcIndex, dst, dstIndex, vectorCount); + throw new UnsupportedOperationException("STUB NEEDED"); + } + + /** + * Apply this matrix to the array of 2D points specified by src, and write + * the transformed points into the array of points specified by dst. The + * two arrays represent their "points" as pairs of floats [x, y]. + * + * @param dst The array of dst points (x,y pairs) + * @param src The array of src points (x,y pairs) + */ + @Override + public void mapPoints(float[] dst, float[] src) { + if (dst.length != src.length) { + throw new ArrayIndexOutOfBoundsException(); + } + mapPoints(dst, 0, src, 0, dst.length >> 1); + } + + /** + * Apply this matrix to the array of 2D vectors specified by src, and write + * the transformed vectors into the array of vectors specified by dst. The + * two arrays represent their "vectors" as pairs of floats [x, y]. + * + * @param dst The array of dst vectors (x,y pairs) + * @param src The array of src vectors (x,y pairs) + */ + @Override + public void mapVectors(float[] dst, float[] src) { + if (dst.length != src.length) { + throw new ArrayIndexOutOfBoundsException(); + } + mapVectors(dst, 0, src, 0, dst.length >> 1); + } + + /** + * Apply this matrix to the array of 2D points, and write the transformed + * points back into the array + * + * @param pts The array [x0, y0, x1, y1, ...] of points to transform. + */ + @Override + public void mapPoints(float[] pts) { + mapPoints(pts, 0, pts, 0, pts.length >> 1); + } + + /** + * Apply this matrix to the array of 2D vectors, and write the transformed + * vectors back into the array. + * @param vecs The array [x0, y0, x1, y1, ...] of vectors to transform. + */ + @Override + public void mapVectors(float[] vecs) { + mapVectors(vecs, 0, vecs, 0, vecs.length >> 1); + } + + /** + * Apply this matrix to the src rectangle, and write the transformed + * rectangle into dst. This is accomplished by transforming the 4 corners of + * src, and then setting dst to the bounds of those points. + * + * @param dst Where the transformed rectangle is written. + * @param src The original rectangle to be transformed. + * @return the result of calling rectStaysRect() + */ + @Override + public boolean mapRect(RectF dst, RectF src) { + if (dst == null || src == null) { + throw new NullPointerException(); + } + throw new UnsupportedOperationException("STUB NEEDED"); + } + + /** + * Apply this matrix to the rectangle, and write the transformed rectangle + * back into it. This is accomplished by transforming the 4 corners of rect, + * and then setting it to the bounds of those points + * + * @param rect The rectangle to transform. + * @return the result of calling rectStaysRect() + */ + @Override + public boolean mapRect(RectF rect) { + return mapRect(rect, rect); + } + + /** + * Return the mean radius of a circle after it has been mapped by + * this matrix. NOTE: in perspective this value assumes the circle + * has its center at the origin. + */ + @Override + public float mapRadius(float radius) { + throw new UnsupportedOperationException("STUB NEEDED"); + } + + /** Copy 9 values from the matrix into the array. + */ + @Override + public void getValues(float[] values) { + if (values.length < 9) { + throw new ArrayIndexOutOfBoundsException(); + } + System.arraycopy(mValues, 0, values, 0, mValues.length); + } + + /** Copy 9 values from the array into the matrix. + Depending on the implementation of Matrix, these may be + transformed into 16.16 integers in the Matrix, such that + a subsequent call to getValues() will not yield exactly + the same values. + */ + @Override + public void setValues(float[] values) { + if (values.length < 9) { + throw new ArrayIndexOutOfBoundsException(); + } + System.arraycopy(values, 0, mValues, 0, mValues.length); + } + + @SuppressWarnings("unused") + private final static int kIdentity_Mask = 0; + private final static int kTranslate_Mask = 0x01; //!< set if the matrix has translation + private final static int kScale_Mask = 0x02; //!< set if the matrix has X or Y scale + private final static int kAffine_Mask = 0x04; //!< set if the matrix skews or rotates + private final static int kPerspective_Mask = 0x08; //!< set if the matrix is in perspective + private final static int kRectStaysRect_Mask = 0x10; + @SuppressWarnings("unused") + private final static int kUnknown_Mask = 0x80; + + @SuppressWarnings("unused") + private final static int kAllMasks = kTranslate_Mask | + kScale_Mask | + kAffine_Mask | + kPerspective_Mask | + kRectStaysRect_Mask; + + // these guys align with the masks, so we can compute a mask from a variable 0/1 + @SuppressWarnings("unused") + private final static int kTranslate_Shift = 0; + @SuppressWarnings("unused") + private final static int kScale_Shift = 1; + @SuppressWarnings("unused") + private final static int kAffine_Shift = 2; + @SuppressWarnings("unused") + private final static int kPerspective_Shift = 3; + private final static int kRectStaysRect_Shift = 4; + + private int computeTypeMask() { + int mask = 0; + + if (mValues[6] != 0. || mValues[7] != 0. || mValues[8] != 1.) { + mask |= kPerspective_Mask; + } + + if (mValues[2] != 0. || mValues[5] != 0.) { + mask |= kTranslate_Mask; + } + + float m00 = mValues[0]; + float m01 = mValues[1]; + float m10 = mValues[3]; + float m11 = mValues[4]; + + if (m01 != 0. || m10 != 0.) { + mask |= kAffine_Mask; + } + + if (m00 != 1. || m11 != 1.) { + mask |= kScale_Mask; + } + + if ((mask & kPerspective_Mask) == 0) { + // map non-zero to 1 + int im00 = m00 != 0 ? 1 : 0; + int im01 = m01 != 0 ? 1 : 0; + int im10 = m10 != 0 ? 1 : 0; + int im11 = m11 != 0 ? 1 : 0; + + // record if the (p)rimary and (s)econdary diagonals are all 0 or + // all non-zero (answer is 0 or 1) + int dp0 = (im00 | im11) ^ 1; // true if both are 0 + int dp1 = im00 & im11; // true if both are 1 + int ds0 = (im01 | im10) ^ 1; // true if both are 0 + int ds1 = im01 & im10; // true if both are 1 + + // return 1 if primary is 1 and secondary is 0 or + // primary is 0 and secondary is 1 + mask |= ((dp0 & ds1) | (dp1 & ds0)) << kRectStaysRect_Shift; + } + + return mask; + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Paint.java b/tools/layoutlib/bridge/src/android/graphics/Paint.java new file mode 100644 index 0000000..ade07d6 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Paint.java @@ -0,0 +1,864 @@ +/* + * Copyright (C) 2008 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 android.graphics; + +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.SpannedString; +import android.text.TextUtils; + +import java.awt.Font; +import java.awt.Toolkit; +import java.awt.font.FontRenderContext; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; + +/** + * A paint implementation overridden by the LayoutLib bridge. + */ +public class Paint extends _Original_Paint { + + private int mColor = 0xFFFFFFFF; + private float mTextSize = 20; + private float mScaleX = 1; + private float mSkewX = 0; + private Align mAlign = Align.LEFT; + private Style mStyle = Style.FILL; + private int mFlags = 0; + + private Font mFont; + private final FontRenderContext mFontContext = new FontRenderContext( + new AffineTransform(), true, true); + private java.awt.FontMetrics mMetrics; + + @SuppressWarnings("hiding") + public static final int ANTI_ALIAS_FLAG = _Original_Paint.ANTI_ALIAS_FLAG; + @SuppressWarnings("hiding") + public static final int FILTER_BITMAP_FLAG = _Original_Paint.FILTER_BITMAP_FLAG; + @SuppressWarnings("hiding") + public static final int DITHER_FLAG = _Original_Paint.DITHER_FLAG; + @SuppressWarnings("hiding") + public static final int UNDERLINE_TEXT_FLAG = _Original_Paint.UNDERLINE_TEXT_FLAG; + @SuppressWarnings("hiding") + public static final int STRIKE_THRU_TEXT_FLAG = _Original_Paint.STRIKE_THRU_TEXT_FLAG; + @SuppressWarnings("hiding") + public static final int FAKE_BOLD_TEXT_FLAG = _Original_Paint.FAKE_BOLD_TEXT_FLAG; + @SuppressWarnings("hiding") + public static final int LINEAR_TEXT_FLAG = _Original_Paint.LINEAR_TEXT_FLAG; + @SuppressWarnings("hiding") + public static final int SUBPIXEL_TEXT_FLAG = _Original_Paint.SUBPIXEL_TEXT_FLAG; + @SuppressWarnings("hiding") + public static final int DEV_KERN_TEXT_FLAG = _Original_Paint.DEV_KERN_TEXT_FLAG; + + public static class FontMetrics extends _Original_Paint.FontMetrics { + } + + public static class FontMetricsInt extends _Original_Paint.FontMetricsInt { + } + + /** + * The Style specifies if the primitive being drawn is filled, + * stroked, or both (in the same color). The default is FILL. + */ + public enum Style { + /** + * Geometry and text drawn with this style will be filled, ignoring all + * stroke-related settings in the paint. + */ + FILL (0), + /** + * Geometry and text drawn with this style will be stroked, respecting + * the stroke-related fields on the paint. + */ + STROKE (1), + /** + * Geometry and text drawn with this style will be both filled and + * stroked at the same time, respecting the stroke-related fields on + * the paint. + */ + FILL_AND_STROKE (2); + + Style(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + /** + * The Cap specifies the treatment for the beginning and ending of + * stroked lines and paths. The default is BUTT. + */ + public enum Cap { + /** + * The stroke ends with the path, and does not project beyond it. + */ + BUTT (0), + /** + * The stroke projects out as a square, with the center at the end + * of the path. + */ + ROUND (1), + /** + * The stroke projects out as a semicircle, with the center at the + * end of the path. + */ + SQUARE (2); + + private Cap(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + /** + * The Join specifies the treatment where lines and curve segments + * join on a stroked path. The default is MITER. + */ + public enum Join { + /** + * The outer edges of a join meet at a sharp angle + */ + MITER (0), + /** + * The outer edges of a join meet in a circular arc. + */ + ROUND (1), + /** + * The outer edges of a join meet with a straight line + */ + BEVEL (2); + + private Join(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + /** + * Align specifies how drawText aligns its text relative to the + * [x,y] coordinates. The default is LEFT. + */ + public enum Align { + /** + * The text is drawn to the right of the x,y origin + */ + LEFT (0), + /** + * The text is drawn centered horizontally on the x,y origin + */ + CENTER (1), + /** + * The text is drawn to the left of the x,y origin + */ + RIGHT (2); + + private Align(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + public Paint() { + this(0); + } + + public Paint(int flags) { + setFlags(flags | DEFAULT_PAINT_FLAGS); + initFont(); + } + + public Paint(Paint paint) { + set(paint); + initFont(); + } + + @Override + public void finalize() throws Throwable { + // pass + } + + /** + * Returns the {@link Font} object. + */ + public Font getFont() { + return mFont; + } + + private void initFont() { + mTypeface = Typeface.DEFAULT; + updateFontObject(); + } + + /** + * Update the {@link Font} object from the typeface, text size and scaling + */ + private void updateFontObject() { + if (mTypeface != null) { + // get the typeface font object, and get our font object from it, based on the current size + mFont = mTypeface.getFont().deriveFont(mTextSize); + if (mScaleX != 1.0 || mSkewX != 0) { + // TODO: support skew + mFont = mFont.deriveFont(new AffineTransform( + mScaleX, mSkewX, 0, 0, 1, 0)); + } + + mMetrics = Toolkit.getDefaultToolkit().getFontMetrics(mFont); + } + } + + //---------------------------------------- + + public void set(Paint src) { + if (this != src) { + mColor = src.mColor; + mTextSize = src.mTextSize; + mScaleX = src.mScaleX; + mSkewX = src.mSkewX; + mAlign = src.mAlign; + mStyle = src.mStyle; + mFlags = src.mFlags; + + super.set(src); + } + } + + @Override + public int getFlags() { + return mFlags; + } + + @Override + public void setFlags(int flags) { + mFlags = flags; + } + + /** + * Return the font's recommended interline spacing, given the Paint's + * settings for typeface, textSize, etc. If metrics is not null, return the + * fontmetric values in it. + * + * @param metrics If this object is not null, its fields are filled with + * the appropriate values given the paint's text attributes. + * @return the font's recommended interline spacing. + */ + public float getFontMetrics(FontMetrics metrics) { + if (mMetrics != null) { + if (metrics != null) { + // ascent stuff should be negatif, but awt returns them as positive. + metrics.top = - mMetrics.getMaxAscent(); + metrics.ascent = - mMetrics.getAscent(); + metrics.descent = mMetrics.getDescent(); + metrics.bottom = mMetrics.getMaxDescent(); + metrics.leading = mMetrics.getLeading(); + } + + return mMetrics.getHeight(); + } + + return 0; + } + + public int getFontMetricsInt(FontMetricsInt metrics) { + if (mMetrics != null) { + if (metrics != null) { + // ascent stuff should be negatif, but awt returns them as positive. + metrics.top = - mMetrics.getMaxAscent(); + metrics.ascent = - mMetrics.getAscent(); + metrics.descent = mMetrics.getDescent(); + metrics.bottom = mMetrics.getMaxDescent(); + metrics.leading = mMetrics.getLeading(); + } + + return mMetrics.getHeight(); + } + + return 0; + } + + /** + * Reimplemented to return Paint.FontMetrics instead of _Original_Paint.FontMetrics + */ + public FontMetrics getFontMetrics() { + FontMetrics fm = new FontMetrics(); + getFontMetrics(fm); + return fm; + } + + /** + * Reimplemented to return Paint.FontMetricsInt instead of _Original_Paint.FontMetricsInt + */ + public FontMetricsInt getFontMetricsInt() { + FontMetricsInt fm = new FontMetricsInt(); + getFontMetricsInt(fm); + return fm; + } + + + + @Override + public float getFontMetrics(_Original_Paint.FontMetrics metrics) { + // TODO implement if needed + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + @Override + public int getFontMetricsInt(_Original_Paint.FontMetricsInt metrics) { + // TODO implement if needed + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + @Override + public Typeface setTypeface(Typeface typeface) { + if (typeface != null) { + mTypeface = typeface; + } else { + mTypeface = Typeface.DEFAULT; + } + + updateFontObject(); + + return typeface; + } + + @Override + public int getColor() { + return mColor; + } + + @Override + public void setColor(int color) { + mColor = color; + } + + + @Override + public void setAlpha(int alpha) { + mColor = (alpha << 24) | (mColor & 0x00FFFFFF); + } + + @Override + public int getAlpha() { + return mColor >>> 24; + } + + /** + * Set or clear the shader object. + * <p /> + * Pass null to clear any previous shader. + * As a convenience, the parameter passed is also returned. + * + * @param shader May be null. the new shader to be installed in the paint + * @return shader + */ + @Override + public Shader setShader(Shader shader) { + return mShader = shader; + } + + /** + * Set or clear the paint's colorfilter, returning the parameter. + * + * @param filter May be null. The new filter to be installed in the paint + * @return filter + */ + @Override + public ColorFilter setColorFilter(ColorFilter filter) { + int filterNative = 0; + if (filter != null) + filterNative = filter.native_instance; + mColorFilter = filter; + return filter; + } + + /** + * Set or clear the xfermode object. + * <p /> + * Pass null to clear any previous xfermode. + * As a convenience, the parameter passed is also returned. + * + * @param xfermode May be null. The xfermode to be installed in the paint + * @return xfermode + */ + @Override + public Xfermode setXfermode(Xfermode xfermode) { + return mXfermode = xfermode; + } + + public void setTextAlign(Align align) { + mAlign = align; + } + + @Override + public void setTextAlign(android.graphics._Original_Paint.Align align) { + // TODO implement if needed + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + public Align getTextAlign() { + return mAlign; + } + + public void setStyle(Style style) { + mStyle = style; + } + + @Override + public void setStyle(android.graphics._Original_Paint.Style style) { + // TODO implement if needed + throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN"); + } + + public Style getStyle() { + return mStyle; + } + + @Override + public void setDither(boolean dither) { + mFlags |= dither ? DITHER_FLAG : ~DITHER_FLAG; + } + + @Override + public void setAntiAlias(boolean aa) { + mFlags |= aa ? ANTI_ALIAS_FLAG : ~ANTI_ALIAS_FLAG; + } + + @Override + public void setFakeBoldText(boolean flag) { + mFlags |= flag ? FAKE_BOLD_TEXT_FLAG : ~FAKE_BOLD_TEXT_FLAG; + } + + /** + * Return the paint's text size. + * + * @return the paint's text size. + */ + @Override + public float getTextSize() { + return mTextSize; + } + + /** + * Set the paint's text size. This value must be > 0 + * + * @param textSize set the paint's text size. + */ + @Override + public void setTextSize(float textSize) { + mTextSize = textSize; + + updateFontObject(); + } + + /** + * Return the paint's horizontal scale factor for text. The default value + * is 1.0. + * + * @return the paint's scale factor in X for drawing/measuring text + */ + @Override + public float getTextScaleX() { + return mScaleX; + } + + /** + * Set the paint's horizontal scale factor for text. The default value + * is 1.0. Values > 1.0 will stretch the text wider. Values < 1.0 will + * stretch the text narrower. + * + * @param scaleX set the paint's scale in X for drawing/measuring text. + */ + @Override + public void setTextScaleX(float scaleX) { + mScaleX = scaleX; + + updateFontObject(); + } + + /** + * Return the paint's horizontal skew factor for text. The default value + * is 0. + * + * @return the paint's skew factor in X for drawing text. + */ + @Override + public float getTextSkewX() { + return mSkewX; + } + + /** + * Set the paint's horizontal skew factor for text. The default value + * is 0. For approximating oblique text, use values around -0.25. + * + * @param skewX set the paint's skew factor in X for drawing text. + */ + @Override + public void setTextSkewX(float skewX) { + mSkewX = skewX; + + updateFontObject(); + } + + /** + * Return the distance above (negative) the baseline (ascent) based on the + * current typeface and text size. + * + * @return the distance above (negative) the baseline (ascent) based on the + * current typeface and text size. + */ + @Override + public float ascent() { + if (mMetrics != null) { + // ascent stuff should be negatif, but awt returns them as positive. + return - mMetrics.getAscent(); + } + + return 0; + } + + /** + * Return the distance below (positive) the baseline (descent) based on the + * current typeface and text size. + * + * @return the distance below (positive) the baseline (descent) based on + * the current typeface and text size. + */ + @Override + public float descent() { + if (mMetrics != null) { + return mMetrics.getDescent(); + } + + return 0; + } + + /** + * Return the width of the text. + * + * @param text The text to measure + * @param index The index of the first character to start measuring + * @param count THe number of characters to measure, beginning with start + * @return The width of the text + */ + @Override + public float measureText(char[] text, int index, int count) { + if (mFont != null && text != null && text.length > 0) { + Rectangle2D bounds = mFont.getStringBounds(text, index, index + count, mFontContext); + + return (float)bounds.getWidth(); + } + + return 0; + } + + /** + * Return the width of the text. + * + * @param text The text to measure + * @param start The index of the first character to start measuring + * @param end 1 beyond the index of the last character to measure + * @return The width of the text + */ + @Override + public float measureText(String text, int start, int end) { + return measureText(text.toCharArray(), start, end - start); + } + + /** + * Return the width of the text. + * + * @param text The text to measure + * @return The width of the text + */ + @Override + public float measureText(String text) { + return measureText(text.toCharArray(), 0, text.length()); + } + + /* + * re-implement to call SpannableStringBuilder.measureText with a Paint object + * instead of an _Original_Paint + */ + @Override + public float measureText(CharSequence text, int start, int end) { + if (text instanceof String) { + return measureText((String)text, start, end); + } + if (text instanceof SpannedString || + text instanceof SpannableString) { + return measureText(text.toString(), start, end); + } + if (text instanceof SpannableStringBuilder) { + return ((SpannableStringBuilder)text).measureText(start, end, this); + } + + char[] buf = TemporaryBuffer.obtain(end - start); + TextUtils.getChars(text, start, end, buf, 0); + float result = measureText(buf, 0, end - start); + TemporaryBuffer.recycle(buf); + return result; + } + + /** + * Measure the text, stopping early if the measured width exceeds maxWidth. + * Return the number of chars that were measured, and if measuredWidth is + * not null, return in it the actual width measured. + * + * @param text The text to measure + * @param index The offset into text to begin measuring at + * @param count The number of maximum number of entries to measure. If count + * is negative, then the characters before index are measured + * in reverse order. This allows for measuring the end of + * string. + * @param maxWidth The maximum width to accumulate. + * @param measuredWidth Optional. If not null, returns the actual width + * measured. + * @return The number of chars that were measured. Will always be <= + * abs(count). + */ + @Override + public int breakText(char[] text, int index, int count, + float maxWidth, float[] measuredWidth) { + int inc = count > 0 ? 1 : -1; + + int measureIndex = 0; + float measureAcc = 0; + for (int i = index ; i != index + count ; i += inc, measureIndex++) { + int start, end; + if (i < index) { + start = i; + end = index; + } else { + start = index; + end = i; + } + + // measure from start to end + float res = measureText(text, start, end - start + 1); + + if (measuredWidth != null) { + measuredWidth[measureIndex] = res; + } + + measureAcc += res; + if (res > maxWidth) { + // we should not return this char index, but since it's 0-based and we need + // to return a count, we simply return measureIndex; + return measureIndex; + } + + } + + return measureIndex; + } + + /** + * Measure the text, stopping early if the measured width exceeds maxWidth. + * Return the number of chars that were measured, and if measuredWidth is + * not null, return in it the actual width measured. + * + * @param text The text to measure + * @param measureForwards If true, measure forwards, starting at index. + * Otherwise, measure backwards, starting with the + * last character in the string. + * @param maxWidth The maximum width to accumulate. + * @param measuredWidth Optional. If not null, returns the actual width + * measured. + * @return The number of chars that were measured. Will always be <= + * abs(count). + */ + @Override + public int breakText(String text, boolean measureForwards, + float maxWidth, float[] measuredWidth) { + // NOTE: javadoc doesn't match. Just a guess. + return breakText(text, + 0 /* start */, text.length() /* end */, + measureForwards, maxWidth, measuredWidth); + } + + /** + * Return the advance widths for the characters in the string. + * + * @param text The text to measure + * @param index The index of the first char to to measure + * @param count The number of chars starting with index to measure + * @param widths array to receive the advance widths of the characters. + * Must be at least a large as count. + * @return the actual number of widths returned. + */ + @Override + public int getTextWidths(char[] text, int index, int count, + float[] widths) { + if (mMetrics != null) { + if ((index | count) < 0 || index + count > text.length + || count > widths.length) { + throw new ArrayIndexOutOfBoundsException(); + } + + for (int i = 0; i < count; i++) { + widths[i] = mMetrics.charWidth(text[i + index]); + } + + return count; + } + + return 0; + } + + /** + * Return the advance widths for the characters in the string. + * + * @param text The text to measure + * @param start The index of the first char to to measure + * @param end The end of the text slice to measure + * @param widths array to receive the advance widths of the characters. + * Must be at least a large as the text. + * @return the number of unichars in the specified text. + */ + @Override + public int getTextWidths(String text, int start, int end, float[] widths) { + if ((start | end | (end - start) | (text.length() - end)) < 0) { + throw new IndexOutOfBoundsException(); + } + if (end - start > widths.length) { + throw new ArrayIndexOutOfBoundsException(); + } + + return getTextWidths(text.toCharArray(), start, end - start, widths); + } + + /* + * re-implement to call SpannableStringBuilder.getTextWidths with a Paint object + * instead of an _Original_Paint + */ + @Override + public int getTextWidths(CharSequence text, int start, int end, float[] widths) { + if (text instanceof String) { + return getTextWidths((String)text, start, end, widths); + } + if (text instanceof SpannedString || text instanceof SpannableString) { + return getTextWidths(text.toString(), start, end, widths); + } + if (text instanceof SpannableStringBuilder) { + return ((SpannableStringBuilder)text).getTextWidths(start, end, widths, this); + } + + char[] buf = TemporaryBuffer.obtain(end - start); + TextUtils.getChars(text, start, end, buf, 0); + int result = getTextWidths(buf, 0, end - start, widths); + TemporaryBuffer.recycle(buf); + return result; + } + + + /** + * Return the path (outline) for the specified text. + * Note: just like Canvas.drawText, this will respect the Align setting in + * the paint. + * + * @param text The text to retrieve the path from + * @param index The index of the first character in text + * @param count The number of characterss starting with index + * @param x The x coordinate of the text's origin + * @param y The y coordinate of the text's origin + * @param path The path to receive the data describing the text. Must + * be allocated by the caller. + */ + @Override + public void getTextPath(char[] text, int index, int count, + float x, float y, Path path) { + + // TODO this is the ORIGINAL implementation. REPLACE AS NEEDED OR REMOVE + + if ((index | count) < 0 || index + count > text.length) { + throw new ArrayIndexOutOfBoundsException(); + } + + // TODO native_getTextPath(mNativePaint, text, index, count, x, y, path.ni()); + + throw new UnsupportedOperationException("IMPLEMENT AS NEEDED"); + } + + /** + * Return the path (outline) for the specified text. + * Note: just like Canvas.drawText, this will respect the Align setting + * in the paint. + * + * @param text The text to retrieve the path from + * @param start The first character in the text + * @param end 1 past the last charcter in the text + * @param x The x coordinate of the text's origin + * @param y The y coordinate of the text's origin + * @param path The path to receive the data describing the text. Must + * be allocated by the caller. + */ + @Override + public void getTextPath(String text, int start, int end, + float x, float y, Path path) { + if ((start | end | (end - start) | (text.length() - end)) < 0) { + throw new IndexOutOfBoundsException(); + } + + getTextPath(text.toCharArray(), start, end - start, x, y, path); + } + + /** + * Return in bounds (allocated by the caller) the smallest rectangle that + * encloses all of the characters, with an implied origin at (0,0). + * + * @param text String to measure and return its bounds + * @param start Index of the first char in the string to measure + * @param end 1 past the last char in the string measure + * @param bounds Returns the unioned bounds of all the text. Must be + * allocated by the caller. + */ + @Override + public void getTextBounds(String text, int start, int end, Rect bounds) { + if ((start | end | (end - start) | (text.length() - end)) < 0) { + throw new IndexOutOfBoundsException(); + } + if (bounds == null) { + throw new NullPointerException("need bounds Rect"); + } + + getTextBounds(text.toCharArray(), start, end - start, bounds); + } + + /** + * Return in bounds (allocated by the caller) the smallest rectangle that + * encloses all of the characters, with an implied origin at (0,0). + * + * @param text Array of chars to measure and return their unioned bounds + * @param index Index of the first char in the array to measure + * @param count The number of chars, beginning at index, to measure + * @param bounds Returns the unioned bounds of all the text. Must be + * allocated by the caller. + */ + @Override + public void getTextBounds(char[] text, int index, int count, Rect bounds) { + if (mFont != null) { + if ((index | count) < 0 || index + count > text.length) { + throw new ArrayIndexOutOfBoundsException(); + } + if (bounds == null) { + throw new NullPointerException("need bounds Rect"); + } + + Rectangle2D rect = mFont.getStringBounds(text, index, index + count, mFontContext); + bounds.set(0, 0, (int)rect.getWidth(), (int)rect.getHeight()); + } + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Path.java b/tools/layoutlib/bridge/src/android/graphics/Path.java new file mode 100644 index 0000000..12d2cde --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Path.java @@ -0,0 +1,611 @@ +/* + * Copyright (C) 2006 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 android.graphics; + +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.GeneralPath; +import java.awt.geom.PathIterator; +import java.awt.geom.Rectangle2D; + +/** + * The Path class encapsulates compound (multiple contour) geometric paths + * consisting of straight line segments, quadratic curves, and cubic curves. + * It can be drawn with canvas.drawPath(path, paint), either filled or stroked + * (based on the paint's Style), or it can be used for clipping or to draw + * text on a path. + */ +public class Path { + + private FillType mFillType = FillType.WINDING; + private GeneralPath mPath = new GeneralPath(); + + private float mLastX = 0; + private float mLastY = 0; + + //---------- Custom methods ---------- + + public Shape getAwtShape() { + return mPath; + } + + //---------- + + /** + * Create an empty path + */ + public Path() { + } + + /** + * Create a new path, copying the contents from the src path. + * + * @param src The path to copy from when initializing the new path + */ + public Path(Path src) { + mPath.append(src.mPath, false /* connect */); + } + + /** + * Clear any lines and curves from the path, making it empty. + * This does NOT change the fill-type setting. + */ + public void reset() { + mPath = new GeneralPath(); + } + + /** + * Rewinds the path: clears any lines and curves from the path but + * keeps the internal data structure for faster reuse. + */ + public void rewind() { + // FIXME + throw new UnsupportedOperationException(); + } + + /** Replace the contents of this with the contents of src. + */ + public void set(Path src) { + mPath.append(src.mPath, false /* connect */); + } + + /** Enum for the ways a path may be filled + */ + public enum FillType { + // these must match the values in SkPath.h + WINDING (GeneralPath.WIND_NON_ZERO, false), + EVEN_ODD (GeneralPath.WIND_EVEN_ODD, false), + INVERSE_WINDING (GeneralPath.WIND_NON_ZERO, true), + INVERSE_EVEN_ODD(GeneralPath.WIND_EVEN_ODD, true); + + FillType(int rule, boolean inverse) { + this.rule = rule; + this.inverse = inverse; + } + + final int rule; + final boolean inverse; + } + + /** + * Return the path's fill type. This defines how "inside" is + * computed. The default value is WINDING. + * + * @return the path's fill type + */ + public FillType getFillType() { + return mFillType; + } + + /** + * Set the path's fill type. This defines how "inside" is computed. + * + * @param ft The new fill type for this path + */ + public void setFillType(FillType ft) { + mFillType = ft; + mPath.setWindingRule(ft.rule); + } + + /** + * Returns true if the filltype is one of the INVERSE variants + * + * @return true if the filltype is one of the INVERSE variants + */ + public boolean isInverseFillType() { + return mFillType.inverse; + } + + /** + * Toggles the INVERSE state of the filltype + */ + public void toggleInverseFillType() { + switch (mFillType) { + case WINDING: + mFillType = FillType.INVERSE_WINDING; + break; + case EVEN_ODD: + mFillType = FillType.INVERSE_EVEN_ODD; + break; + case INVERSE_WINDING: + mFillType = FillType.WINDING; + break; + case INVERSE_EVEN_ODD: + mFillType = FillType.EVEN_ODD; + break; + } + } + + /** + * Returns true if the path is empty (contains no lines or curves) + * + * @return true if the path is empty (contains no lines or curves) + */ + public boolean isEmpty() { + return mPath.getCurrentPoint() == null; + } + + /** + * Returns true if the path specifies a rectangle. If so, and if rect is + * not null, set rect to the bounds of the path. If the path does not + * specify a rectangle, return false and ignore rect. + * + * @param rect If not null, returns the bounds of the path if it specifies + * a rectangle + * @return true if the path specifies a rectangle + */ + public boolean isRect(RectF rect) { + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Compute the bounds of the path, and write the answer into bounds. If the + * path contains 0 or 1 points, the bounds is set to (0,0,0,0) + * + * @param bounds Returns the computed bounds of the path + * @param exact If true, return the exact (but slower) bounds, else return + * just the bounds of all control points + */ + public void computeBounds(RectF bounds, boolean exact) { + Rectangle2D rect = mPath.getBounds2D(); + bounds.left = (float)rect.getMinX(); + bounds.right = (float)rect.getMaxX(); + bounds.top = (float)rect.getMinY(); + bounds.bottom = (float)rect.getMaxY(); + } + + /** + * Hint to the path to prepare for adding more points. This can allow the + * path to more efficiently allocate its storage. + * + * @param extraPtCount The number of extra points that may be added to this + * path + */ + public void incReserve(int extraPtCount) { + // pass + } + + /** + * Set the beginning of the next contour to the point (x,y). + * + * @param x The x-coordinate of the start of a new contour + * @param y The y-coordinate of the start of a new contour + */ + public void moveTo(float x, float y) { + mPath.moveTo(mLastX = x, mLastY = y); + } + + /** + * Set the beginning of the next contour relative to the last point on the + * previous contour. If there is no previous contour, this is treated the + * same as moveTo(). + * + * @param dx The amount to add to the x-coordinate of the end of the + * previous contour, to specify the start of a new contour + * @param dy The amount to add to the y-coordinate of the end of the + * previous contour, to specify the start of a new contour + */ + public void rMoveTo(float dx, float dy) { + dx += mLastX; + dy += mLastY; + mPath.moveTo(mLastX = dx, mLastY = dy); + } + + /** + * Add a line from the last point to the specified point (x,y). + * If no moveTo() call has been made for this contour, the first point is + * automatically set to (0,0). + * + * @param x The x-coordinate of the end of a line + * @param y The y-coordinate of the end of a line + */ + public void lineTo(float x, float y) { + mPath.lineTo(mLastX = x, mLastY = y); + } + + /** + * Same as lineTo, but the coordinates are considered relative to the last + * point on this contour. If there is no previous point, then a moveTo(0,0) + * is inserted automatically. + * + * @param dx The amount to add to the x-coordinate of the previous point on + * this contour, to specify a line + * @param dy The amount to add to the y-coordinate of the previous point on + * this contour, to specify a line + */ + public void rLineTo(float dx, float dy) { + if (isEmpty()) { + mPath.moveTo(mLastX = 0, mLastY = 0); + } + dx += mLastX; + dy += mLastY; + mPath.lineTo(mLastX = dx, mLastY = dy); + } + + /** + * Add a quadratic bezier from the last point, approaching control point + * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for + * this contour, the first point is automatically set to (0,0). + * + * @param x1 The x-coordinate of the control point on a quadratic curve + * @param y1 The y-coordinate of the control point on a quadratic curve + * @param x2 The x-coordinate of the end point on a quadratic curve + * @param y2 The y-coordinate of the end point on a quadratic curve + */ + public void quadTo(float x1, float y1, float x2, float y2) { + mPath.quadTo(x1, y1, mLastX = x2, mLastY = y2); + } + + /** + * Same as quadTo, but the coordinates are considered relative to the last + * point on this contour. If there is no previous point, then a moveTo(0,0) + * is inserted automatically. + * + * @param dx1 The amount to add to the x-coordinate of the last point on + * this contour, for the control point of a quadratic curve + * @param dy1 The amount to add to the y-coordinate of the last point on + * this contour, for the control point of a quadratic curve + * @param dx2 The amount to add to the x-coordinate of the last point on + * this contour, for the end point of a quadratic curve + * @param dy2 The amount to add to the y-coordinate of the last point on + * this contour, for the end point of a quadratic curve + */ + public void rQuadTo(float dx1, float dy1, float dx2, float dy2) { + if (isEmpty()) { + mPath.moveTo(mLastX = 0, mLastY = 0); + } + dx1 += mLastX; + dy1 += mLastY; + dx2 += mLastX; + dy2 += mLastY; + mPath.quadTo(dx1, dy1, mLastX = dx2, mLastY = dy2); + } + + /** + * Add a cubic bezier from the last point, approaching control points + * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been + * made for this contour, the first point is automatically set to (0,0). + * + * @param x1 The x-coordinate of the 1st control point on a cubic curve + * @param y1 The y-coordinate of the 1st control point on a cubic curve + * @param x2 The x-coordinate of the 2nd control point on a cubic curve + * @param y2 The y-coordinate of the 2nd control point on a cubic curve + * @param x3 The x-coordinate of the end point on a cubic curve + * @param y3 The y-coordinate of the end point on a cubic curve + */ + public void cubicTo(float x1, float y1, float x2, float y2, + float x3, float y3) { + mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3); + } + + /** + * Same as cubicTo, but the coordinates are considered relative to the + * current point on this contour. If there is no previous point, then a + * moveTo(0,0) is inserted automatically. + */ + public void rCubicTo(float dx1, float dy1, float dx2, float dy2, + float dx3, float dy3) { + if (isEmpty()) { + mPath.moveTo(mLastX = 0, mLastY = 0); + } + dx1 += mLastX; + dy1 += mLastY; + dx2 += mLastX; + dy2 += mLastY; + dx3 += mLastX; + dy3 += mLastY; + mPath.curveTo(dx1, dy1, dx2, dy2, mLastX = dx3, mLastY = dy3); + } + + /** + * Append the specified arc to the path as a new contour. If the start of + * the path is different from the path's current last point, then an + * automatic lineTo() is added to connect the current contour to the + * start of the arc. However, if the path is empty, then we call moveTo() + * with the first point of the arc. The sweep angle is tread mod 360. + * + * @param oval The bounds of oval defining shape and size of the arc + * @param startAngle Starting angle (in degrees) where the arc begins + * @param sweepAngle Sweep angle (in degrees) measured clockwise, treated + * mod 360. + * @param forceMoveTo If true, always begin a new contour with the arc + */ + public void arcTo(RectF oval, float startAngle, float sweepAngle, + boolean forceMoveTo) { + throw new UnsupportedOperationException(); + } + + /** + * Append the specified arc to the path as a new contour. If the start of + * the path is different from the path's current last point, then an + * automatic lineTo() is added to connect the current contour to the + * start of the arc. However, if the path is empty, then we call moveTo() + * with the first point of the arc. + * + * @param oval The bounds of oval defining shape and size of the arc + * @param startAngle Starting angle (in degrees) where the arc begins + * @param sweepAngle Sweep angle (in degrees) measured clockwise + */ + public void arcTo(RectF oval, float startAngle, float sweepAngle) { + throw new UnsupportedOperationException(); + } + + /** + * Close the current contour. If the current point is not equal to the + * first point of the contour, a line segment is automatically added. + */ + public void close() { + mPath.closePath(); + } + + /** + * Specifies how closed shapes (e.g. rects, ovals) are oriented when they + * are added to a path. + */ + public enum Direction { + /** clockwise */ + CW (0), // must match enum in SkPath.h + /** counter-clockwise */ + CCW (1); // must match enum in SkPath.h + + Direction(int ni) { + nativeInt = ni; + } + final int nativeInt; + } + + /** + * Add a closed rectangle contour to the path + * + * @param rect The rectangle to add as a closed contour to the path + * @param dir The direction to wind the rectangle's contour + */ + public void addRect(RectF rect, Direction dir) { + if (rect == null) { + throw new NullPointerException("need rect parameter"); + } + + addRect(rect.left, rect.top, rect.right, rect.bottom, dir); + } + + /** + * Add a closed rectangle contour to the path + * + * @param left The left side of a rectangle to add to the path + * @param top The top of a rectangle to add to the path + * @param right The right side of a rectangle to add to the path + * @param bottom The bottom of a rectangle to add to the path + * @param dir The direction to wind the rectangle's contour + */ + public void addRect(float left, float top, float right, float bottom, + Direction dir) { + moveTo(left, top); + + switch (dir) { + case CW: + lineTo(right, top); + lineTo(right, bottom); + lineTo(left, bottom); + break; + case CCW: + lineTo(left, bottom); + lineTo(right, bottom); + lineTo(right, top); + break; + } + + close(); + } + + /** + * Add a closed oval contour to the path + * + * @param oval The bounds of the oval to add as a closed contour to the path + * @param dir The direction to wind the oval's contour + */ + public void addOval(RectF oval, Direction dir) { + if (oval == null) { + throw new NullPointerException("need oval parameter"); + } + + // FIXME Need to support direction + Ellipse2D ovalShape = new Ellipse2D.Float(oval.left, oval.top, oval.width(), oval.height()); + + mPath.append(ovalShape, false /* connect */); + } + + /** + * Add a closed circle contour to the path + * + * @param x The x-coordinate of the center of a circle to add to the path + * @param y The y-coordinate of the center of a circle to add to the path + * @param radius The radius of a circle to add to the path + * @param dir The direction to wind the circle's contour + */ + public void addCircle(float x, float y, float radius, Direction dir) { + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Add the specified arc to the path as a new contour. + * + * @param oval The bounds of oval defining the shape and size of the arc + * @param startAngle Starting angle (in degrees) where the arc begins + * @param sweepAngle Sweep angle (in degrees) measured clockwise + */ + public void addArc(RectF oval, float startAngle, float sweepAngle) { + if (oval == null) { + throw new NullPointerException("need oval parameter"); + } + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Add a closed round-rectangle contour to the path + * + * @param rect The bounds of a round-rectangle to add to the path + * @param rx The x-radius of the rounded corners on the round-rectangle + * @param ry The y-radius of the rounded corners on the round-rectangle + * @param dir The direction to wind the round-rectangle's contour + */ + public void addRoundRect(RectF rect, float rx, float ry, Direction dir) { + if (rect == null) { + throw new NullPointerException("need rect parameter"); + } + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Add a closed round-rectangle contour to the path. Each corner receives + * two radius values [X, Y]. The corners are ordered top-left, top-right, + * bottom-right, bottom-left + * + * @param rect The bounds of a round-rectangle to add to the path + * @param radii Array of 8 values, 4 pairs of [X,Y] radii + * @param dir The direction to wind the round-rectangle's contour + */ + public void addRoundRect(RectF rect, float[] radii, Direction dir) { + if (rect == null) { + throw new NullPointerException("need rect parameter"); + } + if (radii.length < 8) { + throw new ArrayIndexOutOfBoundsException("radii[] needs 8 values"); + } + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Add a copy of src to the path, offset by (dx,dy) + * + * @param src The path to add as a new contour + * @param dx The amount to translate the path in X as it is added + */ + public void addPath(Path src, float dx, float dy) { + PathIterator iterator = src.mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy)); + mPath.append(iterator, false /* connect */); + } + + /** + * Add a copy of src to the path + * + * @param src The path that is appended to the current path + */ + public void addPath(Path src) { + addPath(src, 0, 0); + } + + /** + * Add a copy of src to the path, transformed by matrix + * + * @param src The path to add as a new contour + */ + public void addPath(Path src, Matrix matrix) { + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Offset the path by (dx,dy), returning true on success + * + * @param dx The amount in the X direction to offset the entire path + * @param dy The amount in the Y direction to offset the entire path + * @param dst The translated path is written here. If this is null, then + * the original path is modified. + */ + public void offset(float dx, float dy, Path dst) { + GeneralPath newPath = new GeneralPath(); + + PathIterator iterator = mPath.getPathIterator(new AffineTransform(0, 0, dx, 0, 0, dy)); + + newPath.append(iterator, false /* connect */); + + if (dst != null) { + dst.mPath = newPath; + } else { + mPath = newPath; + } + } + + /** + * Offset the path by (dx,dy), returning true on success + * + * @param dx The amount in the X direction to offset the entire path + * @param dy The amount in the Y direction to offset the entire path + */ + public void offset(float dx, float dy) { + offset(dx, dy, null /* dst */); + } + + /** + * Sets the last point of the path. + * + * @param dx The new X coordinate for the last point + * @param dy The new Y coordinate for the last point + */ + public void setLastPoint(float dx, float dy) { + mLastX = dx; + mLastY = dy; + } + + /** + * Transform the points in this path by matrix, and write the answer + * into dst. If dst is null, then the the original path is modified. + * + * @param matrix The matrix to apply to the path + * @param dst The transformed path is written here. If dst is null, + * then the the original path is modified + */ + public void transform(Matrix matrix, Path dst) { + // FIXME + throw new UnsupportedOperationException(); + } + + /** + * Transform the points in this path by matrix. + * + * @param matrix The matrix to apply to the path + */ + public void transform(Matrix matrix) { + transform(matrix, null /* dst */); + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/PorterDuffXfermode.java b/tools/layoutlib/bridge/src/android/graphics/PorterDuffXfermode.java new file mode 100644 index 0000000..974ae49 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/PorterDuffXfermode.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2008 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 android.graphics; + +import android.graphics.PorterDuff.Mode; + +public class PorterDuffXfermode extends Xfermode { + private final Mode mMode; + + /** + * Create an xfermode that uses the specified porter-duff mode. + * + * @param mode The porter-duff mode that is applied + */ + public PorterDuffXfermode(PorterDuff.Mode mode) { + mMode = mode; + } + + //---------- Custom Methods + + public PorterDuff.Mode getMode() { + return mMode; + } + + //---------- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/RadialGradient.java b/tools/layoutlib/bridge/src/android/graphics/RadialGradient.java new file mode 100644 index 0000000..61b693a --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/RadialGradient.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2008 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 android.graphics; + +public class RadialGradient extends Shader { + + /** Create a shader that draws a radial gradient given the center and radius. + @param x The x-coordinate of the center of the radius + @param y The y-coordinate of the center of the radius + @param radius Must be positive. The radius of the circle for this gradient + @param colors The colors to be distributed between the center and edge of the circle + @param positions May be NULL. The relative position of + each corresponding color in the colors array. If this is NULL, + the the colors are distributed evenly between the center and edge of the circle. + @param tile The Shader tiling mode + */ + public RadialGradient(float x, float y, float radius, + int colors[], float positions[], TileMode tile) { + if (radius <= 0) { + throw new IllegalArgumentException("radius must be > 0"); + } + if (colors.length < 2) { + throw new IllegalArgumentException("needs >= 2 number of colors"); + } + if (positions != null && colors.length != positions.length) { + throw new IllegalArgumentException("color and position arrays must be of equal length"); + } + + // FIXME Implement shader + } + + /** Create a shader that draws a radial gradient given the center and radius. + @param x The x-coordinate of the center of the radius + @param y The y-coordinate of the center of the radius + @param radius Must be positive. The radius of the circle for this gradient + @param color0 The color at the center of the circle. + @param color1 The color at the edge of the circle. + @param tile The Shader tiling mode + */ + public RadialGradient(float x, float y, float radius, + int color0, int color1, TileMode tile) { + if (radius <= 0) { + throw new IllegalArgumentException("radius must be > 0"); + } + // FIXME Implement shader + } +} + diff --git a/tools/layoutlib/bridge/src/android/graphics/Shader.java b/tools/layoutlib/bridge/src/android/graphics/Shader.java new file mode 100644 index 0000000..3a9fda5 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Shader.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2008 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 android.graphics; + +/** + * Shader is the based class for objects that return horizontal spans of colors + * during drawing. A subclass of Shader is installed in a Paint calling + * paint.setShader(shader). After that any object (other than a bitmap) that is + * drawn with that paint will get its color(s) from the shader. + */ +public class Shader { + + private final Matrix mMatrix = new Matrix(); + + public enum TileMode { + /** + * replicate the edge color if the shader draws outside of its + * original bounds + */ + CLAMP (0), + /** + * repeat the shader's image horizontally and vertically + */ + REPEAT (1), + /** + * repeat the shader's image horizontally and vertically, alternating + * mirror images so that adjacent images always seam + */ + MIRROR (2); + + TileMode(int nativeInt) { + this.nativeInt = nativeInt; + } + final int nativeInt; + } + + /** + * Return true if the shader has a non-identity local matrix. + * @param localM If not null, it is set to the shader's local matrix. + * @return true if the shader has a non-identity local matrix + */ + public boolean getLocalMatrix(Matrix localM) { + if (localM != null) { + localM.set(mMatrix); + } + + return !mMatrix.isIdentity(); + } + + /** + * Set the shader's local matrix. Passing null will reset the shader's + * matrix to identity + * @param localM The shader's new local matrix, or null to specify identity + */ + public void setLocalMatrix(Matrix localM) { + if (localM != null) { + mMatrix.set(localM); + } else { + mMatrix.reset(); + } + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/SweepGradient.java b/tools/layoutlib/bridge/src/android/graphics/SweepGradient.java new file mode 100644 index 0000000..e79e970 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/SweepGradient.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2008 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 android.graphics; + +public class SweepGradient extends Shader { + + /** + * A subclass of Shader that draws a sweep gradient around a center point. + * + * @param cx The x-coordinate of the center + * @param cy The y-coordinate of the center + * @param colors The colors to be distributed between around the center. + * There must be at least 2 colors in the array. + * @param positions May be NULL. The relative position of + * each corresponding color in the colors array, beginning + * with 0 and ending with 1.0. If the values are not + * monotonic, the drawing may produce unexpected results. + * If positions is NULL, then the colors are automatically + * spaced evenly. + */ + public SweepGradient(float cx, float cy, + int colors[], float positions[]) { + if (colors.length < 2) { + throw new IllegalArgumentException("needs >= 2 number of colors"); + } + if (positions != null && colors.length != positions.length) { + throw new IllegalArgumentException( + "color and position arrays must be of equal length"); + } + + // FIXME Implement shader + } + + /** + * A subclass of Shader that draws a sweep gradient around a center point. + * + * @param cx The x-coordinate of the center + * @param cy The y-coordinate of the center + * @param color0 The color to use at the start of the sweep + * @param color1 The color to use at the end of the sweep + */ + public SweepGradient(float cx, float cy, int color0, int color1) { + // FIXME Implement shader + } +} + diff --git a/tools/layoutlib/bridge/src/android/graphics/Typeface.java b/tools/layoutlib/bridge/src/android/graphics/Typeface.java new file mode 100644 index 0000000..e878b04 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Typeface.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2008 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 android.graphics; + +import com.android.layoutlib.bridge.FontLoader; + +import android.content.res.AssetManager; + +import java.awt.Font; + +/** + * Re-implementation of Typeface over java.awt + */ +public class Typeface { + private static final String DEFAULT_FAMILY = "sans-serif"; + private static final int[] styleBuffer = new int[1]; + + /** The default NORMAL typeface object */ + public static Typeface DEFAULT; + /** + * The default BOLD typeface object. Note: this may be not actually be + * bold, depending on what fonts are installed. Call getStyle() to know + * for sure. + */ + public static Typeface DEFAULT_BOLD; + /** The NORMAL style of the default sans serif typeface. */ + public static Typeface SANS_SERIF; + /** The NORMAL style of the default serif typeface. */ + public static Typeface SERIF; + /** The NORMAL style of the default monospace typeface. */ + public static Typeface MONOSPACE; + + private static Typeface[] sDefaults; + private static FontLoader mFontLoader; + + private final int mStyle; + private final Font mFont; + private final String mFamily; + + // Style + public static final int NORMAL = _Original_Typeface.NORMAL; + public static final int BOLD = _Original_Typeface.BOLD; + public static final int ITALIC = _Original_Typeface.ITALIC; + public static final int BOLD_ITALIC = _Original_Typeface.BOLD_ITALIC; + + /** + * Returns the underlying {@link Font} object. + */ + public Font getFont() { + return mFont; + } + + /** Returns the typeface's intrinsic style attributes */ + public int getStyle() { + return mStyle; + } + + /** Returns true if getStyle() has the BOLD bit set. */ + public final boolean isBold() { + return (getStyle() & BOLD) != 0; + } + + /** Returns true if getStyle() has the ITALIC bit set. */ + public final boolean isItalic() { + return (getStyle() & ITALIC) != 0; + } + + /** + * Create a typeface object given a family name, and option style information. + * If null is passed for the name, then the "default" font will be chosen. + * The resulting typeface object can be queried (getStyle()) to discover what + * its "real" style characteristics are. + * + * @param familyName May be null. The name of the font family. + * @param style The style (normal, bold, italic) of the typeface. + * e.g. NORMAL, BOLD, ITALIC, BOLD_ITALIC + * @return The best matching typeface. + */ + public static Typeface create(String familyName, int style) { + styleBuffer[0] = style; + Font font = mFontLoader.getFont(familyName, styleBuffer); + if (font != null) { + return new Typeface(familyName, styleBuffer[0], font); + } + + return null; + } + + /** + * Create a typeface object that best matches the specified existing + * typeface and the specified Style. Use this call if you want to pick a new + * style from the same family of an existing typeface object. If family is + * null, this selects from the default font's family. + * + * @param family May be null. The name of the existing type face. + * @param style The style (normal, bold, italic) of the typeface. + * e.g. NORMAL, BOLD, ITALIC, BOLD_ITALIC + * @return The best matching typeface. + */ + public static Typeface create(Typeface family, int style) { + styleBuffer[0] = style; + Font font = mFontLoader.getFont(family.mFamily, styleBuffer); + if (font != null) { + return new Typeface(family.mFamily, styleBuffer[0], font); + } + + return null; + } + + /** + * Returns one of the default typeface objects, based on the specified style + * + * @return the default typeface that corresponds to the style + */ + public static Typeface defaultFromStyle(int style) { + return sDefaults[style]; + } + + /** + * Create a new typeface from the specified font data. + * @param mgr The application's asset manager + * @param path The file name of the font data in the assets directory + * @return The new typeface. + */ + public static Typeface createFromAsset(AssetManager mgr, String path) { + return null; + //return new Typeface(nativeCreateFromAsset(mgr, path)); + } + + // don't allow clients to call this directly + private Typeface(String family, int style, Font f) { + mFamily = family; + mFont = f; + mStyle = style; + } + + public static void init(FontLoader fontLoader) { + mFontLoader = fontLoader; + + DEFAULT = create(DEFAULT_FAMILY, NORMAL); + DEFAULT_BOLD = create(DEFAULT_FAMILY, BOLD); + SANS_SERIF = create("sans-serif", NORMAL); + SERIF = create("serif", NORMAL); + MONOSPACE = create("monospace", NORMAL); + sDefaults = new Typeface[] { + DEFAULT, + DEFAULT_BOLD, + create(DEFAULT_FAMILY, ITALIC), + create(DEFAULT_FAMILY, BOLD_ITALIC), + }; + + /* + DEFAULT = create((String)null, 0); + DEFAULT_BOLD = create((String)null, Typeface.BOLD); + SANS_SERIF = create("sans-serif", 0); + SERIF = create("serif", 0); + MONOSPACE = create("monospace", 0); + + sDefaults = new Typeface[] { + DEFAULT, + DEFAULT_BOLD, + create((String)null, Typeface.ITALIC), + create((String)null, Typeface.BOLD_ITALIC), + };*/ + } +} diff --git a/tools/layoutlib/bridge/src/android/util/FloatMath.java b/tools/layoutlib/bridge/src/android/util/FloatMath.java new file mode 100644 index 0000000..aae44f2 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/util/FloatMath.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2007 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 android.util; + +/** + * Reimplements _Original_FloatMath with the standard libraries. + * + * Math routines similar to those found in {@link java.lang.Math}. Performs + * computations on {@code float} values directly without incurring the overhead + * of conversions to and from {@code double}. + * + * <p>On one platform, {@code FloatMath.sqrt(100)} executes in one third of the + * time required by {@code java.lang.Math.sqrt(100)}.</p> + */ +public class FloatMath { + + /** Prevents instantiation. */ + private FloatMath() {} + + /** + * Returns the float conversion of the most positive (i.e. closest to + * positive infinity) integer value which is less than the argument. + * + * @param value to be converted + * @return the floor of value + */ + public static float floor(float value) { + return (float)Math.floor(value); + } + + /** + * Returns the float conversion of the most negative (i.e. closest to + * negative infinity) integer value which is greater than the argument. + * + * @param value to be converted + * @return the ceiling of value + */ + public static float ceil(float value) { + return (float)Math.ceil(value); + } + + /** + * Returns the closest float approximation of the sine of the argument. + * + * @param angle to compute the cosine of, in radians + * @return the sine of angle + */ + public static float sin(float angle) { + return (float)Math.sin(angle); + } + + /** + * Returns the closest float approximation of the cosine of the argument. + * + * @param angle to compute the cosine of, in radians + * @return the cosine of angle + */ + public static float cos(float angle) { + return (float)Math.cos(angle); + } + + /** + * Returns the closest float approximation of the square root of the + * argument. + * + * @param value to compute sqrt of + * @return the square root of value + */ + public static float sqrt(float value) { + return (float)Math.sqrt(value); + } +} diff --git a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java new file mode 100644 index 0000000..0910d79 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2008 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 android.view; + +import com.android.layoutlib.api.IProjectCallback; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.BridgeConstants; +import com.android.layoutlib.bridge.BridgeContext; +import com.android.layoutlib.bridge.BridgeXmlBlockParser; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; +import android.util.AttributeSet; + +import java.io.File; +import java.io.FileReader; + +/** + * Custom implementation of {@link LayoutInflater} to handle custom views. + */ +public final class BridgeInflater extends LayoutInflater { + + private final IProjectCallback mProjectCallback; + + /** + * List of class prefixes which are tried first by default. + * <p/> + * This should match the list in com.android.internal.policy.impl.PhoneLayoutInflater. + */ + private static final String[] sClassPrefixList = { + "android.widget.", + "android.webkit." + }; + + protected BridgeInflater(LayoutInflater original, Context newContext) { + super(original, newContext); + mProjectCallback = null; + } + + /** + * Instantiate a new BridgeInflater with an {@link IProjectCallback} object. + * + * @param context The Android application context. + * @param projectCallback the {@link IProjectCallback} object. + */ + public BridgeInflater(Context context, IProjectCallback projectCallback) { + super(context); + mProjectCallback = projectCallback; + mConstructorArgs[0] = context; + } + + @Override + public View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { + View view = null; + + try { + // First try to find a class using the default Android prefixes + for (String prefix : sClassPrefixList) { + try { + view = createView(name, prefix, attrs); + if (view != null) { + break; + } + } catch (ClassNotFoundException e) { + // Ignore. We'll try again using the base class below. + } + } + + // Next try using the parent loader. This will most likely only work for + // fully-qualified class names. + try { + if (view == null) { + view = super.onCreateView(name, attrs); + } + } catch (ClassNotFoundException e) { + // Ignore. We'll try again using the custom view loader below. + } + + // Finally try again using the custom view loader + try { + if (view == null) { + view = loadCustomView(name, attrs); + } + } catch (ClassNotFoundException e) { + // If the class was not found, we throw the exception directly, because this + // method is already expected to throw it. + throw e; + } + } catch (Exception e) { + // Wrap the real exception in a ClassNotFoundException, so that the calling method + // can deal with it. + ClassNotFoundException exception = new ClassNotFoundException("onCreateView", e); + throw exception; + } + + setupViewInContext(view, attrs); + + return view; + } + + @Override + public View createViewFromTag(String name, AttributeSet attrs) { + View view = null; + try { + view = super.createViewFromTag(name, attrs); + } catch (InflateException e) { + // try to load the class from using the custom view loader + try { + view = loadCustomView(name, attrs); + } catch (Exception e2) { + // Wrap the real exception in an InflateException so that the calling + // method can deal with it. + InflateException exception = new InflateException(); + if (e2.getClass().equals(ClassNotFoundException.class) == false) { + exception.initCause(e2); + } else { + exception.initCause(e); + } + throw exception; + } + } + + setupViewInContext(view, attrs); + + return view; + } + + @Override + public View inflate(int resource, ViewGroup root) { + Context context = getContext(); + if (context instanceof BridgeContext) { + BridgeContext bridgeContext = (BridgeContext)context; + + IResourceValue value = null; + + String[] layoutInfo = Bridge.resolveResourceValue(resource); + if (layoutInfo != null) { + value = bridgeContext.getFrameworkResource(BridgeConstants.RES_LAYOUT, + layoutInfo[0]); + } else { + layoutInfo = mProjectCallback.resolveResourceValue(resource); + + if (layoutInfo != null) { + value = bridgeContext.getProjectResource(BridgeConstants.RES_LAYOUT, + layoutInfo[0]); + } + } + + if (value != null) { + File f = new File(value.getValue()); + if (f.isFile()) { + try { + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(f)); + + BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser( + parser, bridgeContext, false); + + return inflate(bridgeParser, root); + } catch (Exception e) { + bridgeContext.getLogger().error(e); + // return null below. + } + } + } + } + return null; + } + + private View loadCustomView(String name, AttributeSet attrs) throws ClassNotFoundException, + Exception{ + if (mProjectCallback != null) { + // first get the classname in case it's not the node name + if (name.equals("view")) { + name = attrs.getAttributeValue(null, "class"); + } + + mConstructorArgs[1] = attrs; + + Object customView = mProjectCallback.loadView(name, mConstructorSignature, + mConstructorArgs); + + if (customView instanceof View) { + return (View)customView; + } + } + + return null; + } + + + + private void setupViewInContext(View view, AttributeSet attrs) { + if (getContext() instanceof BridgeContext) { + BridgeContext bc = (BridgeContext) getContext(); + if (attrs instanceof BridgeXmlBlockParser) { + Object viewKey = ((BridgeXmlBlockParser) attrs).getViewKey(); + if (viewKey != null) { + bc.addViewKey(view, viewKey); + } + } + } + } + + @Override + public LayoutInflater cloneInContext(Context newContext) { + return new BridgeInflater(this, newContext); + } +} diff --git a/tools/layoutlib/bridge/src/android/view/SurfaceView.java b/tools/layoutlib/bridge/src/android/view/SurfaceView.java new file mode 100644 index 0000000..ce32da9 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/SurfaceView.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2006 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 android.view; + +import com.android.layoutlib.bridge.MockView; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.util.AttributeSet; + +/** + * Mock version of the SurfaceView. + * Only non override public methods from the real SurfaceView have been added in there. + * Methods that take an unknown class as parameter or as return object, have been removed for now. + * + * TODO: generate automatically. + * + */ +public class SurfaceView extends MockView { + + public SurfaceView(Context context) { + this(context, null); + } + + public SurfaceView(Context context, AttributeSet attrs) { + this(context, attrs , 0); + } + + public SurfaceView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public SurfaceHolder getHolder() { + return mSurfaceHolder; + } + + private SurfaceHolder mSurfaceHolder = new SurfaceHolder() { + + public boolean isCreating() { + return false; + } + + public void addCallback(Callback callback) { + } + + public void removeCallback(Callback callback) { + } + + public void setFixedSize(int width, int height) { + } + + public void setSizeFromLayout() { + } + + public void setFormat(int format) { + } + + public void setType(int type) { + } + + public void setKeepScreenOn(boolean screenOn) { + } + + public Canvas lockCanvas() { + return null; + } + + public Canvas lockCanvas(Rect dirty) { + return null; + } + + public void unlockCanvasAndPost(Canvas canvas) { + } + + public Surface getSurface() { + return null; + } + + public Rect getSurfaceFrame() { + return null; + } + }; +} + diff --git a/tools/layoutlib/bridge/src/android/webkit/WebView.java b/tools/layoutlib/bridge/src/android/webkit/WebView.java new file mode 100644 index 0000000..42e4dfa --- /dev/null +++ b/tools/layoutlib/bridge/src/android/webkit/WebView.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2008 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 android.webkit; + +import com.android.layoutlib.bridge.MockView; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Picture; +import android.os.Bundle; +import android.os.Message; +import android.util.AttributeSet; +import android.view.View; + +/** + * Mock version of the WebView. + * Only non override public methods from the real WebView have been added in there. + * Methods that take an unknown class as parameter or as return object, have been removed for now. + * + * TODO: generate automatically. + * + */ +public class WebView extends MockView { + + /** + * Construct a new WebView with a Context object. + * @param context A Context object used to access application assets. + */ + public WebView(Context context) { + this(context, null); + } + + /** + * Construct a new WebView with layout parameters. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + */ + public WebView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.webViewStyle); + } + + /** + * Construct a new WebView with layout parameters and a default style. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + * @param defStyle The default style resource ID. + */ + public WebView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + // START FAKE PUBLIC METHODS + + public void setHorizontalScrollbarOverlay(boolean overlay) { + } + + public void setVerticalScrollbarOverlay(boolean overlay) { + } + + public boolean overlayHorizontalScrollbar() { + return false; + } + + public boolean overlayVerticalScrollbar() { + return false; + } + + public void savePassword(String host, String username, String password) { + } + + public void setHttpAuthUsernamePassword(String host, String realm, + String username, String password) { + } + + public String[] getHttpAuthUsernamePassword(String host, String realm) { + return null; + } + + public void destroy() { + } + + public static void enablePlatformNotifications() { + } + + public static void disablePlatformNotifications() { + } + + public WebBackForwardList saveState(Bundle outState) { + return null; + } + + public WebBackForwardList restoreState(Bundle inState) { + return null; + } + + public void loadUrl(String url) { + } + + public void loadData(String data, String mimeType, String encoding) { + } + + public void loadDataWithBaseURL(String baseUrl, String data, + String mimeType, String encoding, String failUrl) { + } + + public void stopLoading() { + } + + public void reload() { + } + + public boolean canGoBack() { + return false; + } + + public void goBack() { + } + + public boolean canGoForward() { + return false; + } + + public void goForward() { + } + + public boolean canGoBackOrForward(int steps) { + return false; + } + + public void goBackOrForward(int steps) { + } + + public boolean pageUp(boolean top) { + return false; + } + + public boolean pageDown(boolean bottom) { + return false; + } + + public void clearView() { + } + + public Picture capturePicture() { + return null; + } + + public float getScale() { + return 0; + } + + public void setInitialScale(int scaleInPercent) { + } + + public void invokeZoomPicker() { + } + + public void requestFocusNodeHref(Message hrefMsg) { + } + + public void requestImageRef(Message msg) { + } + + public String getUrl() { + return null; + } + + public String getTitle() { + return null; + } + + public Bitmap getFavicon() { + return null; + } + + public int getProgress() { + return 0; + } + + public int getContentHeight() { + return 0; + } + + public void pauseTimers() { + } + + public void resumeTimers() { + } + + public void clearCache() { + } + + public void clearFormData() { + } + + public void clearHistory() { + } + + public void clearSslPreferences() { + } + + public WebBackForwardList copyBackForwardList() { + return null; + } + + public static String findAddress(String addr) { + return null; + } + + public void documentHasImages(Message response) { + } + + public void setWebViewClient(WebViewClient client) { + } + + public void setDownloadListener(DownloadListener listener) { + } + + public void setWebChromeClient(WebChromeClient client) { + } + + public void addJavascriptInterface(Object obj, String interfaceName) { + } + + public WebSettings getSettings() { + return null; + } + + public static synchronized PluginList getPluginList() { + return null; + } + + public void refreshPlugins(boolean reloadOpenPages) { + } + + public View getZoomControls() { + return null; + } + + public boolean zoomIn() { + return false; + } + + public boolean zoomOut() { + return false; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java new file mode 100644 index 0000000..6abc452d --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java @@ -0,0 +1,932 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.internal.util.XmlUtils; +import com.android.layoutlib.api.ILayoutBridge; +import com.android.layoutlib.api.ILayoutLog; +import com.android.layoutlib.api.ILayoutResult; +import com.android.layoutlib.api.IProjectCallback; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.api.IStyleResourceValue; +import com.android.layoutlib.api.IXmlPullParser; +import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; +import com.android.layoutlib.bridge.LayoutResult.LayoutViewInfo; +import com.android.ninepatch.NinePatch; +import com.android.tools.layoutlib.create.MethodAdapter; +import com.android.tools.layoutlib.create.OverrideMethod; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.BridgeInflater; +import android.view.IWindow; +import android.view.IWindowSession; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.AttachInfo; +import android.view.View.MeasureSpec; +import android.view.WindowManager.LayoutParams; +import android.widget.FrameLayout; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * Main entry point of the LayoutLib Bridge. + * <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call + * {@link #computeLayout(IXmlPullParser, Object, int, int, String, boolean, Map, Map, IProjectCallback, ILayoutLog)}. + */ +public final class Bridge implements ILayoutBridge { + + private static final int DEFAULT_TITLE_BAR_HEIGHT = 25; + private static final int DEFAULT_STATUS_BAR_HEIGHT = 25; + + public static class StaticMethodNotImplementedException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public StaticMethodNotImplementedException(String msg) { + super(msg); + } + } + + /** + * Maps from id to resource name/type. + */ + private final static Map<Integer, String[]> sRMap = new HashMap<Integer, String[]>(); + /** + * Same as sRMap except for int[] instead of int resources. + */ + private final static Map<int[], String> sRArrayMap = new HashMap<int[], String>(); + /** + * Reverse map compared to sRMap, resource type -> (resource name -> id) + */ + private final static Map<String, Map<String, Integer>> sRFullMap = + new HashMap<String, Map<String,Integer>>(); + + private final static Map<Object, Map<String, Bitmap>> sProjectBitmapCache = + new HashMap<Object, Map<String, Bitmap>>(); + private final static Map<Object, Map<String, NinePatch>> sProject9PatchCache = + new HashMap<Object, Map<String, NinePatch>>(); + + private final static Map<String, Bitmap> sFrameworkBitmapCache = new HashMap<String, Bitmap>(); + private final static Map<String, NinePatch> sFramework9PatchCache = + new HashMap<String, NinePatch>(); + + private static Map<String, Map<String, Integer>> sEnumValueMap; + + /** + * A default logger than prints to stdout/stderr. + */ + private final static ILayoutLog sDefaultLogger = new ILayoutLog() { + public void error(String message) { + System.err.println(message); + } + + public void error(Throwable t) { + String message = t.getMessage(); + if (message == null) { + message = t.getClass().getName(); + } + + System.err.println(message); + } + + public void warning(String message) { + System.out.println(message); + } + }; + + /** + * Logger defined during a compute layout operation. + * <p/> + * This logger is generally set to {@link #sDefaultLogger} except during rendering + * operations when it might be set to a specific provided logger. + * <p/> + * To change this value, use a block synchronized on {@link #sDefaultLogger}. + */ + private static ILayoutLog sLogger = sDefaultLogger; + + /* + * (non-Javadoc) + * @see com.android.layoutlib.api.ILayoutBridge#getApiLevel() + */ + public int getApiLevel() { + return API_CURRENT; + } + + /* + * (non-Javadoc) + * @see com.android.layoutlib.api.ILayoutLibBridge#init(java.lang.String, java.util.Map) + */ + public boolean init( + String fontOsLocation, Map<String, Map<String, Integer>> enumValueMap) { + + return sinit(fontOsLocation, enumValueMap); + } + + private static synchronized boolean sinit(String fontOsLocation, + Map<String, Map<String, Integer>> enumValueMap) { + + // When DEBUG_LAYOUT is set and is not 0 or false, setup a default listener + // on static (native) methods which prints the signature on the console and + // throws an exception. + // This is useful when testing the rendering in ADT to identify static native + // methods that are ignored -- layoutlib_create makes them returns 0/false/null + // which is generally OK yet might be a problem, so this is how you'd find out. + // + // Currently layoutlib_create only overrides static native method. + // Static non-natives are not overridden and thus do not get here. + final String debug = System.getenv("DEBUG_LAYOUT"); + if (debug != null && !debug.equals("0") && !debug.equals("false")) { + + OverrideMethod.setDefaultListener(new MethodAdapter() { + @Override + public void onInvokeV(String signature, boolean isNative, Object caller) { + if (sLogger != null) { + synchronized (sDefaultLogger) { + sLogger.error("Missing Stub: " + signature + + (isNative ? " (native)" : "")); + } + } + + if (debug.equalsIgnoreCase("throw")) { + // Throwing this exception doesn't seem that useful. It breaks + // the layout editor yet doesn't display anything meaningful to the + // user. Having the error in the console is just as useful. We'll + // throw it only if the environment variable is "throw" or "THROW". + throw new StaticMethodNotImplementedException(signature); + } + } + }); + } + + // Override View.isInEditMode to return true. + // + // This allows custom views that are drawn in the Graphical Layout Editor to adapt their + // rendering for preview. Most important this let custom views know that they can't expect + // the rest of their activities to be alive. + OverrideMethod.setMethodListener("android.view.View#isInEditMode()Z", + new MethodAdapter() { + @Override + public int onInvokeI(String signature, boolean isNative, Object caller) { + return 1; + } + } + ); + + // load the fonts. + FontLoader fontLoader = FontLoader.create(fontOsLocation); + if (fontLoader != null) { + Typeface.init(fontLoader); + } else { + return false; + } + + sEnumValueMap = enumValueMap; + + // now parse com.android.internal.R (and only this one as android.R is a subset of + // the internal version), and put the content in the maps. + try { + // WARNING: this only works because the class is already loaded, and therefore + // the objects returned by Field.get() are the same as the ones used by + // the code accessing the R class. + // int[] does not implement equals/hashCode, and if the parsing used a different class + // loader for the R class, this would NOT work. + Class<?> r = com.android.internal.R.class; + + for (Class<?> inner : r.getDeclaredClasses()) { + String resType = inner.getSimpleName(); + + Map<String, Integer> fullMap = new HashMap<String, Integer>(); + sRFullMap.put(resType, fullMap); + + for (Field f : inner.getDeclaredFields()) { + // only process static final fields. Since the final attribute may have + // been altered by layoutlib_create, we only check static + int modifiers = f.getModifiers(); + if (Modifier.isStatic(modifiers)) { + Class<?> type = f.getType(); + if (type.isArray() && type.getComponentType() == int.class) { + // if the object is an int[] we put it in sRArrayMap + sRArrayMap.put((int[]) f.get(null), f.getName()); + } else if (type == int.class) { + Integer value = (Integer) f.get(null); + sRMap.put(value, new String[] { f.getName(), resType }); + fullMap.put(f.getName(), value); + } else { + assert false; + } + } + } + } + } catch (IllegalArgumentException e) { + // FIXME: log/return the error (there's no logger object at this point!) + e.printStackTrace(); + return false; + } catch (IllegalAccessException e) { + e.printStackTrace(); + return false; + } + + return true; + } + + /* + * For compatilibty purposes, we implement the old deprecated version of computeLayout. + * (non-Javadoc) + * @see com.android.layoutlib.api.ILayoutBridge#computeLayout(com.android.layoutlib.api.IXmlPullParser, java.lang.Object, int, int, java.lang.String, java.util.Map, java.util.Map, com.android.layoutlib.api.IProjectCallback, com.android.layoutlib.api.ILayoutLog) + */ + @Deprecated + public ILayoutResult computeLayout(IXmlPullParser layoutDescription, + Object projectKey, + int screenWidth, int screenHeight, String themeName, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + IProjectCallback customViewLoader, ILayoutLog logger) { + boolean isProjectTheme = false; + if (themeName.charAt(0) == '*') { + themeName = themeName.substring(1); + isProjectTheme = true; + } + + return computeLayout(layoutDescription, projectKey, + screenWidth, screenHeight, DisplayMetrics.DEFAULT_DENSITY, + DisplayMetrics.DEFAULT_DENSITY, DisplayMetrics.DEFAULT_DENSITY, + themeName, isProjectTheme, + projectResources, frameworkResources, customViewLoader, logger); + } + + /* + * For compatilibty purposes, we implement the old deprecated version of computeLayout. + * (non-Javadoc) + * @see com.android.layoutlib.api.ILayoutBridge#computeLayout(com.android.layoutlib.api.IXmlPullParser, java.lang.Object, int, int, java.lang.String, boolean, java.util.Map, java.util.Map, com.android.layoutlib.api.IProjectCallback, com.android.layoutlib.api.ILayoutLog) + */ + public ILayoutResult computeLayout(IXmlPullParser layoutDescription, Object projectKey, + int screenWidth, int screenHeight, String themeName, boolean isProjectTheme, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + IProjectCallback customViewLoader, ILayoutLog logger) { + return computeLayout(layoutDescription, projectKey, + screenWidth, screenHeight, DisplayMetrics.DEFAULT_DENSITY, + DisplayMetrics.DEFAULT_DENSITY, DisplayMetrics.DEFAULT_DENSITY, + themeName, isProjectTheme, + projectResources, frameworkResources, customViewLoader, logger); + } + + /* + * (non-Javadoc) + * @see com.android.layoutlib.api.ILayoutBridge#computeLayout(com.android.layoutlib.api.IXmlPullParser, java.lang.Object, int, int, int, float, float, java.lang.String, boolean, java.util.Map, java.util.Map, com.android.layoutlib.api.IProjectCallback, com.android.layoutlib.api.ILayoutLog) + */ + public ILayoutResult computeLayout(IXmlPullParser layoutDescription, Object projectKey, + int screenWidth, int screenHeight, int density, float xdpi, float ydpi, + String themeName, boolean isProjectTheme, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + IProjectCallback customViewLoader, ILayoutLog logger) { + if (logger == null) { + logger = sDefaultLogger; + } + + synchronized (sDefaultLogger) { + sLogger = logger; + } + + // find the current theme and compute the style inheritance map + Map<IStyleResourceValue, IStyleResourceValue> styleParentMap = + new HashMap<IStyleResourceValue, IStyleResourceValue>(); + + IStyleResourceValue currentTheme = computeStyleMaps(themeName, isProjectTheme, + projectResources.get(BridgeConstants.RES_STYLE), + frameworkResources.get(BridgeConstants.RES_STYLE), styleParentMap); + + BridgeContext context = null; + try { + // setup the display Metrics. + DisplayMetrics metrics = new DisplayMetrics(); + metrics.density = density / (float) DisplayMetrics.DEFAULT_DENSITY; + metrics.scaledDensity = metrics.density; + metrics.widthPixels = screenWidth; + metrics.heightPixels = screenHeight; + metrics.xdpi = xdpi; + metrics.ydpi = ydpi; + + context = new BridgeContext(projectKey, metrics, currentTheme, projectResources, + frameworkResources, styleParentMap, customViewLoader, logger); + BridgeInflater inflater = new BridgeInflater(context, customViewLoader); + context.setBridgeInflater(inflater); + + IResourceValue windowBackground = null; + int screenOffset = 0; + if (currentTheme != null) { + windowBackground = context.findItemInStyle(currentTheme, "windowBackground"); + windowBackground = context.resolveResValue(windowBackground); + + screenOffset = getScreenOffset(currentTheme, context); + } + + // we need to make sure the Looper has been initialized for this thread. + // this is required for View that creates Handler objects. + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + BridgeXmlBlockParser parser = new BridgeXmlBlockParser(layoutDescription, + context, false /* platformResourceFlag */); + + ViewGroup root = new FrameLayout(context); + + View view = inflater.inflate(parser, root); + + // set the AttachInfo on the root view. + AttachInfo info = new AttachInfo(new WindowSession(), new Window(), + new Handler(), null); + info.mHasWindowFocus = true; + info.mWindowVisibility = View.VISIBLE; + info.mInTouchMode = false; // this is so that we can display selections. + root.dispatchAttachedToWindow(info, 0); + + // get the background drawable + if (windowBackground != null) { + Drawable d = ResourceHelper.getDrawable(windowBackground.getValue(), + context, true /* isFramework */); + root.setBackgroundDrawable(d); + } + + int w_spec = MeasureSpec.makeMeasureSpec(screenWidth, MeasureSpec.EXACTLY); + int h_spec = MeasureSpec.makeMeasureSpec(screenHeight - screenOffset, + MeasureSpec.EXACTLY); + + // measure the views + view.measure(w_spec, h_spec); + view.layout(0, screenOffset, screenWidth, screenHeight); + + // draw them + BridgeCanvas canvas = new BridgeCanvas(screenWidth, screenHeight - screenOffset, + logger); + + root.draw(canvas); + canvas.dispose(); + + return new LayoutResult(visit(((ViewGroup)view).getChildAt(0), context), + canvas.getImage()); + } catch (Throwable e) { + // get the real cause of the exception. + Throwable t = e; + while (t.getCause() != null) { + t = t.getCause(); + } + + // log it + logger.error(t); + + // then return with an ERROR status and the message from the real exception + return new LayoutResult(ILayoutResult.ERROR, + t.getClass().getSimpleName() + ": " + t.getMessage()); + } finally { + // Make sure to remove static references, otherwise we could not unload the lib + BridgeResources.clearSystem(); + BridgeAssetManager.clearSystem(); + + // Remove the global logger + synchronized (sDefaultLogger) { + sLogger = sDefaultLogger; + } + } + } + + /* + * (non-Javadoc) + * @see com.android.layoutlib.api.ILayoutLibBridge#clearCaches(java.lang.Object) + */ + public void clearCaches(Object projectKey) { + if (projectKey != null) { + sProjectBitmapCache.remove(projectKey); + sProject9PatchCache.remove(projectKey); + } + } + + /** + * Returns details of a framework resource from its integer value. + * @param value the integer value + * @return an array of 2 strings containing the resource name and type, or null if the id + * does not match any resource. + */ + public static String[] resolveResourceValue(int value) { + return sRMap.get(value); + + } + + /** + * Returns the name of a framework resource whose value is an int array. + * @param array + */ + public static String resolveResourceValue(int[] array) { + return sRArrayMap.get(array); + } + + /** + * Returns the integer id of a framework resource, from a given resource type and resource name. + * @param type the type of the resource + * @param name the name of the resource. + * @return an {@link Integer} containing the resource id, or null if no resource were found. + */ + public static Integer getResourceValue(String type, String name) { + Map<String, Integer> map = sRFullMap.get(type); + if (map != null) { + return map.get(name); + } + + return null; + } + + static Map<String, Integer> getEnumValues(String attributeName) { + if (sEnumValueMap != null) { + return sEnumValueMap.get(attributeName); + } + + return null; + } + + /** + * Visits a View and its children and generate a {@link ILayoutViewInfo} containing the + * bounds of all the views. + * @param view the root View + * @param context the context. + */ + private ILayoutViewInfo visit(View view, BridgeContext context) { + if (view == null) { + return null; + } + + LayoutViewInfo result = new LayoutViewInfo(view.getClass().getName(), + context.getViewKey(view), + view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); + + if (view instanceof ViewGroup) { + ViewGroup group = ((ViewGroup) view); + int n = group.getChildCount(); + ILayoutViewInfo[] children = new ILayoutViewInfo[n]; + for (int i = 0; i < group.getChildCount(); i++) { + children[i] = visit(group.getChildAt(i), context); + } + result.setChildren(children); + } + + return result; + } + + /** + * Compute style information from the given list of style for the project and framework. + * @param themeName the name of the current theme. In order to differentiate project and + * platform themes sharing the same name, all project themes must be prepended with + * a '*' character. + * @param isProjectTheme Is this a project theme + * @param inProjectStyleMap the project style map + * @param inFrameworkStyleMap the framework style map + * @param outInheritanceMap the map of style inheritance. This is filled by the method + * @return the {@link IStyleResourceValue} matching <var>themeName</var> + */ + private IStyleResourceValue computeStyleMaps( + String themeName, boolean isProjectTheme, Map<String, + IResourceValue> inProjectStyleMap, Map<String, IResourceValue> inFrameworkStyleMap, + Map<IStyleResourceValue, IStyleResourceValue> outInheritanceMap) { + + if (inProjectStyleMap != null && inFrameworkStyleMap != null) { + // first, get the theme + IResourceValue theme = null; + + // project theme names have been prepended with a * + if (isProjectTheme) { + theme = inProjectStyleMap.get(themeName); + } else { + theme = inFrameworkStyleMap.get(themeName); + } + + if (theme instanceof IStyleResourceValue) { + // compute the inheritance map for both the project and framework styles + computeStyleInheritance(inProjectStyleMap.values(), inProjectStyleMap, + inFrameworkStyleMap, outInheritanceMap); + + // Compute the style inheritance for the framework styles/themes. + // Since, for those, the style parent values do not contain 'android:' + // we want to force looking in the framework style only to avoid using + // similarly named styles from the project. + // To do this, we pass null in lieu of the project style map. + computeStyleInheritance(inFrameworkStyleMap.values(), null /*inProjectStyleMap */, + inFrameworkStyleMap, outInheritanceMap); + + return (IStyleResourceValue)theme; + } + } + + return null; + } + + /** + * Compute the parent style for all the styles in a given list. + * @param styles the styles for which we compute the parent. + * @param inProjectStyleMap the map of project styles. + * @param inFrameworkStyleMap the map of framework styles. + * @param outInheritanceMap the map of style inheritance. This is filled by the method. + */ + private void computeStyleInheritance(Collection<IResourceValue> styles, + Map<String, IResourceValue> inProjectStyleMap, + Map<String, IResourceValue> inFrameworkStyleMap, + Map<IStyleResourceValue, IStyleResourceValue> outInheritanceMap) { + for (IResourceValue value : styles) { + if (value instanceof IStyleResourceValue) { + IStyleResourceValue style = (IStyleResourceValue)value; + IStyleResourceValue parentStyle = null; + + // first look for a specified parent. + String parentName = style.getParentStyle(); + + // no specified parent? try to infer it from the name of the style. + if (parentName == null) { + parentName = getParentName(value.getName()); + } + + if (parentName != null) { + parentStyle = getStyle(parentName, inProjectStyleMap, inFrameworkStyleMap); + + if (parentStyle != null) { + outInheritanceMap.put(style, parentStyle); + } + } + } + } + } + + /** + * Searches for and returns the {@link IStyleResourceValue} from a given name. + * <p/>The format of the name can be: + * <ul> + * <li>[android:]<name></li> + * <li>[android:]style/<name></li> + * <li>@[android:]style/<name></li> + * </ul> + * @param parentName the name of the style. + * @param inProjectStyleMap the project style map. Can be <code>null</code> + * @param inFrameworkStyleMap the framework style map. + * @return The matching {@link IStyleResourceValue} object or <code>null</code> if not found. + */ + private IStyleResourceValue getStyle(String parentName, + Map<String, IResourceValue> inProjectStyleMap, + Map<String, IResourceValue> inFrameworkStyleMap) { + boolean frameworkOnly = false; + + String name = parentName; + + // remove the useless @ if it's there + if (name.startsWith(BridgeConstants.PREFIX_RESOURCE_REF)) { + name = name.substring(BridgeConstants.PREFIX_RESOURCE_REF.length()); + } + + // check for framework identifier. + if (name.startsWith(BridgeConstants.PREFIX_ANDROID)) { + frameworkOnly = true; + name = name.substring(BridgeConstants.PREFIX_ANDROID.length()); + } + + // at this point we could have the format style/<name>. we want only the name + if (name.startsWith(BridgeConstants.REFERENCE_STYLE)) { + name = name.substring(BridgeConstants.REFERENCE_STYLE.length()); + } + + IResourceValue parent = null; + + // if allowed, search in the project resources. + if (frameworkOnly == false && inProjectStyleMap != null) { + parent = inProjectStyleMap.get(name); + } + + // if not found, then look in the framework resources. + if (parent == null) { + parent = inFrameworkStyleMap.get(name); + } + + // make sure the result is the proper class type and return it. + if (parent instanceof IStyleResourceValue) { + return (IStyleResourceValue)parent; + } + + sLogger.error(String.format("Unable to resolve parent style name: ", parentName)); + + return null; + } + + /** + * Computes the name of the parent style, or <code>null</code> if the style is a root style. + */ + private String getParentName(String styleName) { + int index = styleName.lastIndexOf('.'); + if (index != -1) { + return styleName.substring(0, index); + } + + return null; + } + + /** + * Returns the top screen offset. This depends on whether the current theme defines the user + * of the title and status bars. + * @return the pixel height offset + */ + private int getScreenOffset(IStyleResourceValue currentTheme, BridgeContext context) { + int offset = 0; + + // get the title bar flag from the current theme. + IResourceValue value = context.findItemInStyle(currentTheme, "windowNoTitle"); + + // because it may reference something else, we resolve it. + value = context.resolveResValue(value); + + // if there's a value and it's true (default is false) + if (value == null || value.getValue() == null || + XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) { + // get value from the theme. + value = context.findItemInStyle(currentTheme, "windowTitleSize"); + + // resolve it + value = context.resolveResValue(value); + + // default value + offset = DEFAULT_TITLE_BAR_HEIGHT; + + // get the real value; + if (value != null) { + TypedValue typedValue = ResourceHelper.getValue(value.getValue()); + if (typedValue != null) { + offset = (int)typedValue.getDimension(context.getResources().mMetrics); + } + } + } + + // get the fullscreen flag from the current theme. + value = context.findItemInStyle(currentTheme, "windowFullscreen"); + + // because it may reference something else, we resolve it. + value = context.resolveResValue(value); + + if (value == null || value.getValue() == null || + XmlUtils.convertValueToBoolean(value.getValue(), false /* defValue */) == false) { + // FIXME: Right now this is hard-coded in the platform, but once there's a constant, we'll need to use it. + offset += DEFAULT_STATUS_BAR_HEIGHT; + } + + return offset; + } + + /** + * Returns the bitmap for a specific path, from a specific project cache, or from the + * framework cache. + * @param value the path of the bitmap + * @param projectKey the key of the project, or null to query the framework cache. + * @return the cached Bitmap or null if not found. + */ + static Bitmap getCachedBitmap(String value, Object projectKey) { + if (projectKey != null) { + Map<String, Bitmap> map = sProjectBitmapCache.get(projectKey); + if (map != null) { + return map.get(value); + } + + return null; + } + + return sFrameworkBitmapCache.get(value); + } + + /** + * Sets a bitmap in a project cache or in the framework cache. + * @param value the path of the bitmap + * @param bmp the Bitmap object + * @param projectKey the key of the project, or null to put the bitmap in the framework cache. + */ + static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) { + if (projectKey != null) { + Map<String, Bitmap> map = sProjectBitmapCache.get(projectKey); + + if (map == null) { + map = new HashMap<String, Bitmap>(); + sProjectBitmapCache.put(projectKey, map); + } + + map.put(value, bmp); + } + + sFrameworkBitmapCache.put(value, bmp); + } + + /** + * Returns the 9 patch for a specific path, from a specific project cache, or from the + * framework cache. + * @param value the path of the 9 patch + * @param projectKey the key of the project, or null to query the framework cache. + * @return the cached 9 patch or null if not found. + */ + static NinePatch getCached9Patch(String value, Object projectKey) { + if (projectKey != null) { + Map<String, NinePatch> map = sProject9PatchCache.get(projectKey); + + if (map != null) { + return map.get(value); + } + + return null; + } + + return sFramework9PatchCache.get(value); + } + + /** + * Sets a 9 patch in a project cache or in the framework cache. + * @param value the path of the 9 patch + * @param ninePatch the 9 patch object + * @param projectKey the key of the project, or null to put the bitmap in the framework cache. + */ + static void setCached9Patch(String value, NinePatch ninePatch, Object projectKey) { + if (projectKey != null) { + Map<String, NinePatch> map = sProject9PatchCache.get(projectKey); + + if (map == null) { + map = new HashMap<String, NinePatch>(); + sProject9PatchCache.put(projectKey, map); + } + + map.put(value, ninePatch); + } + + sFramework9PatchCache.put(value, ninePatch); + } + + /** + * Implementation of {@link IWindowSession} so that mSession is not null in + * the {@link SurfaceView}. + */ + private static final class WindowSession implements IWindowSession { + + @SuppressWarnings("unused") + public int add(IWindow arg0, LayoutParams arg1, int arg2, Rect arg3) + throws RemoteException { + // pass for now. + return 0; + } + + @SuppressWarnings("unused") + public void finishDrawing(IWindow arg0) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void finishKey(IWindow arg0) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public boolean getInTouchMode() throws RemoteException { + // pass for now. + return false; + } + + @SuppressWarnings("unused") + public boolean performHapticFeedback(IWindow window, int effectId, boolean always) { + // pass for now. + return false; + } + + @SuppressWarnings("unused") + public MotionEvent getPendingPointerMove(IWindow arg0) throws RemoteException { + // pass for now. + return null; + } + + @SuppressWarnings("unused") + public MotionEvent getPendingTrackballMove(IWindow arg0) throws RemoteException { + // pass for now. + return null; + } + + @SuppressWarnings("unused") + public int relayout(IWindow arg0, LayoutParams arg1, int arg2, int arg3, int arg4, + boolean arg4_5, Rect arg5, Rect arg6, Rect arg7, Surface arg8) + throws RemoteException { + // pass for now. + return 0; + } + + public void getDisplayFrame(IWindow window, Rect outDisplayFrame) { + // pass for now. + } + + @SuppressWarnings("unused") + public void remove(IWindow arg0) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void setInTouchMode(boolean arg0) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void setTransparentRegion(IWindow arg0, Region arg1) throws RemoteException { + // pass for now. + } + + public void setInsets(IWindow window, int touchable, Rect contentInsets, + Rect visibleInsets) { + // pass for now. + } + + public IBinder asBinder() { + // pass for now. + return null; + } + } + + /** + * Implementation of {@link IWindow} to pass to the {@link AttachInfo}. + */ + private static final class Window implements IWindow { + + @SuppressWarnings("unused") + public void dispatchAppVisibility(boolean arg0) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void dispatchGetNewSurface() throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void dispatchKey(KeyEvent arg0) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void dispatchPointer(MotionEvent arg0, long arg1) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void dispatchTrackball(MotionEvent arg0, long arg1) throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void executeCommand(String arg0, String arg1, ParcelFileDescriptor arg2) + throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void resized(int arg0, int arg1, Rect arg2, Rect arg3, boolean arg4) + throws RemoteException { + // pass for now. + } + + @SuppressWarnings("unused") + public void windowFocusChanged(boolean arg0, boolean arg1) throws RemoteException { + // pass for now. + } + + public IBinder asBinder() { + // pass for now. + return null; + } + } + +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeAssetManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeAssetManager.java new file mode 100644 index 0000000..1fa11af --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeAssetManager.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import android.content.res.AssetManager; +import android.content.res.Configuration; + +import java.util.Locale; + +public class BridgeAssetManager extends AssetManager { + + /** + * This initializes the static field {@link AssetManager#mSystem} which is used + * by methods who get a global asset manager using {@link AssetManager#getSystem()}. + * <p/> + * They will end up using our bridge asset manager. + * <p/> + * {@link Bridge} calls this method after setting up a new bridge. + */ + /*package*/ static AssetManager initSystem() { + if (!(AssetManager.mSystem instanceof BridgeAssetManager)) { + // Note that AssetManager() creates a system AssetManager and we override it + // with our BridgeAssetManager. + AssetManager.mSystem = new BridgeAssetManager(); + AssetManager.mSystem.makeStringBlocks(false); + } + return AssetManager.mSystem; + } + + /** + * Clears the static {@link AssetManager#mSystem} to make sure we don't leave objects + * around that would prevent us from unloading the library. + */ + /*package*/ static void clearSystem() { + AssetManager.mSystem = null; + } + + private BridgeAssetManager() { + } + + /** + * Change the configuration used when retrieving resources. Not for use by applications. + */ + @Override + public void setConfiguration(int mcc, int mnc, String locale, + int orientation, int touchscreen, int density, int keyboard, + int keyboardHidden, int navigation, int screenWidth, int screenHeight, + int version) { + + Configuration c = new Configuration(); + c.mcc = mcc; + c.mnc = mnc; + c.locale = new Locale(locale); + c.touchscreen = touchscreen; + c.keyboard = keyboard; + c.keyboardHidden = keyboardHidden; + c.navigation = navigation; + c.orientation = orientation; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeCanvas.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeCanvas.java new file mode 100644 index 0000000..70c26a7 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeCanvas.java @@ -0,0 +1,1099 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.layoutlib.api.ILayoutLog; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.DrawFilter; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Picture; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.Shader; +import android.graphics.Xfermode; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Region.Op; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.util.Stack; + +import javax.microedition.khronos.opengles.GL; + +/** + * Re-implementation of the Canvas, 100% in java on top of a BufferedImage. + */ +public class BridgeCanvas extends Canvas { + + private BufferedImage mBufferedImage; + private final Stack<Graphics2D> mGraphicsStack = new Stack<Graphics2D>(); + private final ILayoutLog mLogger; + + public BridgeCanvas(int width, int height, ILayoutLog logger) { + mLogger = logger; + mBufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + mGraphicsStack.push(mBufferedImage.createGraphics()); + } + + public BridgeCanvas(int width, int height) { + this(width, height, null /* logger*/); + } + + public BufferedImage getImage() { + return mBufferedImage; + } + + Graphics2D getGraphics2d() { + return mGraphicsStack.peek(); + } + + void dispose() { + while (mGraphicsStack.size() > 0) { + mGraphicsStack.pop().dispose(); + } + } + + /** + * Creates a new {@link Graphics2D} based on the {@link Paint} parameters. + * <p/>The object must be disposed ({@link Graphics2D#dispose()}) after being used. + */ + private Graphics2D getNewGraphics(Paint paint, Graphics2D g) { + // make new one + g = (Graphics2D)g.create(); + g.setColor(new Color(paint.getColor())); + int alpha = paint.getAlpha(); + float falpha = alpha / 255.f; + + Xfermode xfermode = paint.getXfermode(); + if (xfermode instanceof PorterDuffXfermode) { + PorterDuff.Mode mode = ((PorterDuffXfermode)xfermode).getMode(); + + setModeInGraphics(mode, g, falpha); + } else { + if (mLogger != null && xfermode != null) { + mLogger.warning(String.format( + "Xfermode '%1$s' is not supported in the Layout Editor.", + xfermode.getClass().getCanonicalName())); + } + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, falpha)); + } + + Shader shader = paint.getShader(); + if (shader instanceof LinearGradient) { + g.setPaint(((LinearGradient)shader).getPaint()); + } else { + if (mLogger != null && shader != null) { + mLogger.warning(String.format( + "Shader '%1$s' is not supported in the Layout Editor.", + shader.getClass().getCanonicalName())); + } + } + + return g; + } + + private void setModeInGraphics(PorterDuff.Mode mode, Graphics2D g, float falpha) { + switch (mode) { + case CLEAR: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR, falpha)); + break; + case DARKEN: + break; + case DST: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.DST, falpha)); + break; + case DST_ATOP: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_ATOP, falpha)); + break; + case DST_IN: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_IN, falpha)); + break; + case DST_OUT: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_OUT, falpha)); + break; + case DST_OVER: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_OVER, falpha)); + break; + case LIGHTEN: + break; + case MULTIPLY: + break; + case SCREEN: + break; + case SRC: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, falpha)); + break; + case SRC_ATOP: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, falpha)); + break; + case SRC_IN: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN, falpha)); + break; + case SRC_OUT: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OUT, falpha)); + break; + case SRC_OVER: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, falpha)); + break; + case XOR: + g.setComposite(AlphaComposite.getInstance(AlphaComposite.XOR, falpha)); + break; + } + } + + // -------------------- + + @Override + public void finalize() throws Throwable { + // pass + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#translate(float, float) + */ + @Override + public void translate(float dx, float dy) { + getGraphics2d().translate(dx, dy); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#save() + */ + @Override + public int save() { + Graphics2D g = (Graphics2D)getGraphics2d().create(); + mGraphicsStack.push(g); + + return mGraphicsStack.size() - 1; + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#save(int) + */ + @Override + public int save(int saveFlags) { + // For now we ignore saveFlags + return save(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#restore() + */ + @Override + public void restore() { + mGraphicsStack.pop(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#restoreToCount(int) + */ + @Override + public void restoreToCount(int saveCount) { + while (mGraphicsStack.size() > saveCount) { + mGraphicsStack.pop(); + } + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#getSaveCount() + */ + @Override + public int getSaveCount() { + return mGraphicsStack.size() - 1; + } + + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(float, float, float, float, android.graphics.Region.Op) + */ + @Override + public boolean clipRect(float left, float top, float right, float bottom, Op op) { + return clipRect(left, top, right, bottom); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(float, float, float, float) + */ + @Override + public boolean clipRect(float left, float top, float right, float bottom) { + getGraphics2d().clipRect((int)left, (int)top, (int)(right-left), (int)(bottom-top)); + return true; + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(int, int, int, int) + */ + @Override + public boolean clipRect(int left, int top, int right, int bottom) { + getGraphics2d().clipRect(left, top, right-left, bottom-top); + return true; + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(android.graphics.Rect, android.graphics.Region.Op) + */ + @Override + public boolean clipRect(Rect rect, Op op) { + return clipRect(rect.left, rect.top, rect.right, rect.bottom); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(android.graphics.Rect) + */ + @Override + public boolean clipRect(Rect rect) { + return clipRect(rect.left, rect.top, rect.right, rect.bottom); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(android.graphics.RectF, android.graphics.Region.Op) + */ + @Override + public boolean clipRect(RectF rect, Op op) { + return clipRect(rect.left, rect.top, rect.right, rect.bottom); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRect(android.graphics.RectF) + */ + @Override + public boolean clipRect(RectF rect) { + return clipRect(rect.left, rect.top, rect.right, rect.bottom); + } + + @Override + public boolean quickReject(RectF rect, EdgeType type) { + return false; + } + + @Override + public boolean quickReject(Path path, EdgeType type) { + return false; + } + + @Override + public boolean quickReject(float left, float top, float right, float bottom, + EdgeType type) { + return false; + } + + /** + * Retrieve the clip bounds, returning true if they are non-empty. + * + * @param bounds Return the clip bounds here. If it is null, ignore it but + * still return true if the current clip is non-empty. + * @return true if the current clip is non-empty. + */ + @Override + public boolean getClipBounds(Rect bounds) { + Rectangle rect = getGraphics2d().getClipBounds(); + if (rect != null) { + bounds.left = rect.x; + bounds.top = rect.y; + bounds.right = rect.x + rect.width; + bounds.bottom = rect.y + rect.height; + return true; + } + return false; + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawColor(int, android.graphics.PorterDuff.Mode) + */ + @Override + public void drawColor(int color, PorterDuff.Mode mode) { + Graphics2D g = getGraphics2d(); + + // save old color + Color c = g.getColor(); + + Composite composite = g.getComposite(); + + // get the alpha from the color + int alpha = color >>> 24; + float falpha = alpha / 255.f; + + setModeInGraphics(mode, g, falpha); + + g.setColor(new Color(color)); + + getGraphics2d().fillRect(0, 0, getWidth(), getHeight()); + + g.setComposite(composite); + + // restore color + g.setColor(c); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawColor(int) + */ + @Override + public void drawColor(int color) { + drawColor(color, PorterDuff.Mode.SRC_OVER); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawARGB(int, int, int, int) + */ + @Override + public void drawARGB(int a, int r, int g, int b) { + drawColor(a << 24 | r << 16 | g << 8 | b, PorterDuff.Mode.SRC_OVER); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawRGB(int, int, int) + */ + @Override + public void drawRGB(int r, int g, int b) { + drawColor(0xFF << 24 | r << 16 | g << 8 | b, PorterDuff.Mode.SRC_OVER); + } + + + /* (non-Javadoc) + * @see android.graphics.Canvas#getWidth() + */ + @Override + public int getWidth() { + return mBufferedImage.getWidth(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#getHeight() + */ + @Override + public int getHeight() { + return mBufferedImage.getHeight(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPaint(android.graphics.Paint) + */ + @Override + public void drawPaint(Paint paint) { + drawColor(paint.getColor()); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawBitmap(android.graphics.Bitmap, float, float, android.graphics.Paint) + */ + @Override + public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) { + drawBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), + (int)left, (int)top, + (int)left+bitmap.getWidth(), (int)top+bitmap.getHeight(), paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawBitmap(android.graphics.Bitmap, android.graphics.Matrix, android.graphics.Paint) + */ + @Override + public void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) { + throw new UnsupportedOperationException(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawBitmap(android.graphics.Bitmap, android.graphics.Rect, android.graphics.Rect, android.graphics.Paint) + */ + @Override + public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) { + if (src == null) { + drawBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), + dst.left, dst.top, dst.right, dst.bottom, paint); + } else { + drawBitmap(bitmap, src.left, src.top, src.width(), src.height(), + dst.left, dst.top, dst.right, dst.bottom, paint); + } + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawBitmap(android.graphics.Bitmap, android.graphics.Rect, android.graphics.RectF, android.graphics.Paint) + */ + @Override + public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) { + if (src == null) { + drawBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), + (int)dst.left, (int)dst.top, (int)dst.right, (int)dst.bottom, paint); + } else { + drawBitmap(bitmap, src.left, src.top, src.width(), src.height(), + (int)dst.left, (int)dst.top, (int)dst.right, (int)dst.bottom, paint); + } + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawBitmap(int[], int, int, int, int, int, int, boolean, android.graphics.Paint) + */ + @Override + public void drawBitmap(int[] colors, int offset, int stride, int x, int y, int width, + int height, boolean hasAlpha, Paint paint) { + throw new UnsupportedOperationException(); + } + + private void drawBitmap(Bitmap bitmap, int sleft, int stop, int sright, int sbottom, int dleft, + int dtop, int dright, int dbottom, Paint paint) { + BufferedImage image = bitmap.getImage(); + + Graphics2D g = getGraphics2d(); + + Composite c = null; + + if (paint.isFilterBitmap()) { + g = (Graphics2D)g.create(); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } + + if (paint.getAlpha() != 0xFF) { + c = g.getComposite(); + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, + paint.getAlpha()/255.f)); + } + + g.drawImage(image, dleft, dtop, dright, dbottom, + sleft, stop, sright, sbottom, null); + + if (paint.isFilterBitmap()) { + g.dispose(); + } + + if (c != null) { + g.setComposite(c); + } + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#rotate(float, float, float) + */ + @Override + public void rotate(float degrees, float px, float py) { + if (degrees != 0) { + Graphics2D g = getGraphics2d(); + g.translate(px, py); + g.rotate(Math.toRadians(degrees)); + g.translate(-px, -py); + } + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#rotate(float) + */ + @Override + public void rotate(float degrees) { + getGraphics2d().rotate(Math.toRadians(degrees)); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#scale(float, float, float, float) + */ + @Override + public void scale(float sx, float sy, float px, float py) { + Graphics2D g = getGraphics2d(); + g.translate(px, py); + g.scale(sx, sy); + g.translate(-px, -py); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#scale(float, float) + */ + @Override + public void scale(float sx, float sy) { + getGraphics2d().scale(sx, sy); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawText(char[], int, int, float, float, android.graphics.Paint) + */ + @Override + public void drawText(char[] text, int index, int count, float x, float y, Paint paint) { + Graphics2D g = getGraphics2d(); + + g = (Graphics2D)g.create(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g.setFont(paint.getFont()); + + // set the color. because this only handles RGB we have to handle the alpha separately + g.setColor(new Color(paint.getColor())); + int alpha = paint.getAlpha(); + float falpha = alpha / 255.f; + g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, falpha)); + + // Paint.TextAlign indicates how the text is positioned relative to X. + // LEFT is the default and there's nothing to do. + if (paint.getTextAlign() != Align.LEFT) { + float m = paint.measureText(text, index, count); + if (paint.getTextAlign() == Align.CENTER) { + x -= m / 2; + } else if (paint.getTextAlign() == Align.RIGHT) { + x -= m; + } + } + + g.drawChars(text, index, count, (int)x, (int)y); + + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint) + */ + @Override + public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) { + drawText(text.toString().toCharArray(), start, end - start, x, y, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawText(java.lang.String, float, float, android.graphics.Paint) + */ + @Override + public void drawText(String text, float x, float y, Paint paint) { + drawText(text.toCharArray(), 0, text.length(), x, y, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawText(java.lang.String, int, int, float, float, android.graphics.Paint) + */ + @Override + public void drawText(String text, int start, int end, float x, float y, Paint paint) { + drawText(text.toCharArray(), start, end - start, x, y, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawRect(android.graphics.RectF, android.graphics.Paint) + */ + @Override + public void drawRect(RectF rect, Paint paint) { + doDrawRect((int)rect.left, (int)rect.top, (int)rect.width(), (int)rect.height(), paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawRect(float, float, float, float, android.graphics.Paint) + */ + @Override + public void drawRect(float left, float top, float right, float bottom, Paint paint) { + doDrawRect((int)left, (int)top, (int)(right-left), (int)(bottom-top), paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawRect(android.graphics.Rect, android.graphics.Paint) + */ + @Override + public void drawRect(Rect r, Paint paint) { + doDrawRect(r.left, r.top, r.width(), r.height(), paint); + } + + private final void doDrawRect(int left, int top, int width, int height, Paint paint) { + // get current graphisc + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + Style style = paint.getStyle(); + + // draw + if (style == Style.FILL || style == Style.FILL_AND_STROKE) { + g.fillRect(left, top, width, height); + } + + if (style == Style.STROKE || style == Style.FILL_AND_STROKE) { + g.drawRect(left, top, width, height); + } + + // dispose Graphics2D object + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawRoundRect(android.graphics.RectF, float, float, android.graphics.Paint) + */ + @Override + public void drawRoundRect(RectF rect, float rx, float ry, Paint paint) { + // get current graphisc + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + Style style = paint.getStyle(); + + // draw + + int arcWidth = (int)(rx * 2); + int arcHeight = (int)(ry * 2); + + if (style == Style.FILL || style == Style.FILL_AND_STROKE) { + g.fillRoundRect((int)rect.left, (int)rect.right, (int)rect.width(), (int)rect.height(), + arcWidth, arcHeight); + } + + if (style == Style.STROKE || style == Style.FILL_AND_STROKE) { + g.drawRoundRect((int)rect.left, (int)rect.right, (int)rect.width(), (int)rect.height(), + arcWidth, arcHeight); + } + + // dispose Graphics2D object + g.dispose(); + } + + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawLine(float, float, float, float, android.graphics.Paint) + */ + @Override + public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) { + // get current graphisc + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + g.drawLine((int)startX, (int)startY, (int)stopX, (int)stopY); + + // dispose Graphics2D object + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawLines(float[], int, int, android.graphics.Paint) + */ + @Override + public void drawLines(float[] pts, int offset, int count, Paint paint) { + // get current graphisc + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + for (int i = 0 ; i < count ; i += 4) { + g.drawLine((int)pts[i + offset], (int)pts[i + offset + 1], + (int)pts[i + offset + 2], (int)pts[i + offset + 3]); + } + + // dispose Graphics2D object + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawLines(float[], android.graphics.Paint) + */ + @Override + public void drawLines(float[] pts, Paint paint) { + drawLines(pts, 0, pts.length, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawCircle(float, float, float, android.graphics.Paint) + */ + @Override + public void drawCircle(float cx, float cy, float radius, Paint paint) { + // get current graphisc + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + Style style = paint.getStyle(); + + int size = (int)(radius * 2); + + // draw + if (style == Style.FILL || style == Style.FILL_AND_STROKE) { + g.fillOval((int)(cx - radius), (int)(cy - radius), size, size); + } + + if (style == Style.STROKE || style == Style.FILL_AND_STROKE) { + g.drawOval((int)(cx - radius), (int)(cy - radius), size, size); + } + + // dispose Graphics2D object + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawOval(android.graphics.RectF, android.graphics.Paint) + */ + @Override + public void drawOval(RectF oval, Paint paint) { + // get current graphics + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + Style style = paint.getStyle(); + + // draw + if (style == Style.FILL || style == Style.FILL_AND_STROKE) { + g.fillOval((int)oval.left, (int)oval.top, (int)oval.width(), (int)oval.height()); + } + + if (style == Style.STROKE || style == Style.FILL_AND_STROKE) { + g.drawOval((int)oval.left, (int)oval.top, (int)oval.width(), (int)oval.height()); + } + + // dispose Graphics2D object + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPath(android.graphics.Path, android.graphics.Paint) + */ + @Override + public void drawPath(Path path, Paint paint) { + // get current graphics + Graphics2D g = getGraphics2d(); + + g = getNewGraphics(paint, g); + + Style style = paint.getStyle(); + + // draw + if (style == Style.FILL || style == Style.FILL_AND_STROKE) { + g.fill(path.getAwtShape()); + } + + if (style == Style.STROKE || style == Style.FILL_AND_STROKE) { + g.draw(path.getAwtShape()); + } + + // dispose Graphics2D object + g.dispose(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#setMatrix(android.graphics.Matrix) + */ + @Override + public void setMatrix(Matrix matrix) { + // since SetMatrix *replaces* all the other transformation, we have to restore/save + restore(); + save(); + + // get the new current graphics + Graphics2D g = getGraphics2d(); + + // and apply the matrix + g.setTransform(matrix.getTransform()); + + if (mLogger != null && matrix.hasPerspective()) { + mLogger.warning("android.graphics.Canvas#setMatrix(android.graphics.Matrix) only supports affine transformations in the Layout Editor."); + } + } + + // -------------------- + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipPath(android.graphics.Path, android.graphics.Region.Op) + */ + @Override + public boolean clipPath(Path path, Op op) { + // TODO Auto-generated method stub + return super.clipPath(path, op); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipPath(android.graphics.Path) + */ + @Override + public boolean clipPath(Path path) { + // TODO Auto-generated method stub + return super.clipPath(path); + } + + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRegion(android.graphics.Region, android.graphics.Region.Op) + */ + @Override + public boolean clipRegion(Region region, Op op) { + // TODO Auto-generated method stub + return super.clipRegion(region, op); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#clipRegion(android.graphics.Region) + */ + @Override + public boolean clipRegion(Region region) { + // TODO Auto-generated method stub + return super.clipRegion(region); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#concat(android.graphics.Matrix) + */ + @Override + public void concat(Matrix matrix) { + // TODO Auto-generated method stub + super.concat(matrix); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawArc(android.graphics.RectF, float, float, boolean, android.graphics.Paint) + */ + @Override + public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, + Paint paint) { + // TODO Auto-generated method stub + super.drawArc(oval, startAngle, sweepAngle, useCenter, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawBitmapMesh(android.graphics.Bitmap, int, int, float[], int, int[], int, android.graphics.Paint) + */ + @Override + public void drawBitmapMesh(Bitmap bitmap, int meshWidth, int meshHeight, float[] verts, + int vertOffset, int[] colors, int colorOffset, Paint paint) { + // TODO Auto-generated method stub + super.drawBitmapMesh(bitmap, meshWidth, meshHeight, verts, vertOffset, colors, colorOffset, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPicture(android.graphics.Picture, android.graphics.Rect) + */ + @Override + public void drawPicture(Picture picture, Rect dst) { + // TODO Auto-generated method stub + super.drawPicture(picture, dst); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPicture(android.graphics.Picture, android.graphics.RectF) + */ + @Override + public void drawPicture(Picture picture, RectF dst) { + // TODO Auto-generated method stub + super.drawPicture(picture, dst); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPicture(android.graphics.Picture) + */ + @Override + public void drawPicture(Picture picture) { + // TODO Auto-generated method stub + super.drawPicture(picture); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPoint(float, float, android.graphics.Paint) + */ + @Override + public void drawPoint(float x, float y, Paint paint) { + // TODO Auto-generated method stub + super.drawPoint(x, y, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPoints(float[], int, int, android.graphics.Paint) + */ + @Override + public void drawPoints(float[] pts, int offset, int count, Paint paint) { + // TODO Auto-generated method stub + super.drawPoints(pts, offset, count, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPoints(float[], android.graphics.Paint) + */ + @Override + public void drawPoints(float[] pts, Paint paint) { + // TODO Auto-generated method stub + super.drawPoints(pts, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPosText(char[], int, int, float[], android.graphics.Paint) + */ + @Override + public void drawPosText(char[] text, int index, int count, float[] pos, Paint paint) { + // TODO Auto-generated method stub + super.drawPosText(text, index, count, pos, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawPosText(java.lang.String, float[], android.graphics.Paint) + */ + @Override + public void drawPosText(String text, float[] pos, Paint paint) { + // TODO Auto-generated method stub + super.drawPosText(text, pos, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawTextOnPath(char[], int, int, android.graphics.Path, float, float, android.graphics.Paint) + */ + @Override + public void drawTextOnPath(char[] text, int index, int count, Path path, float offset, + float offset2, Paint paint) { + // TODO Auto-generated method stub + super.drawTextOnPath(text, index, count, path, offset, offset2, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawTextOnPath(java.lang.String, android.graphics.Path, float, float, android.graphics.Paint) + */ + @Override + public void drawTextOnPath(String text, Path path, float offset, float offset2, Paint paint) { + // TODO Auto-generated method stub + super.drawTextOnPath(text, path, offset, offset2, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#drawVertices(android.graphics.Canvas.VertexMode, int, float[], int, float[], int, int[], int, short[], int, int, android.graphics.Paint) + */ + @Override + public void drawVertices(VertexMode mode, int vertexCount, float[] verts, int vertOffset, + float[] texs, int texOffset, int[] colors, int colorOffset, short[] indices, + int indexOffset, int indexCount, Paint paint) { + // TODO Auto-generated method stub + super.drawVertices(mode, vertexCount, verts, vertOffset, texs, texOffset, colors, colorOffset, + indices, indexOffset, indexCount, paint); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#getDrawFilter() + */ + @Override + public DrawFilter getDrawFilter() { + // TODO Auto-generated method stub + return super.getDrawFilter(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#getGL() + */ + @Override + public GL getGL() { + // TODO Auto-generated method stub + return super.getGL(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#getMatrix() + */ + @Override + public Matrix getMatrix() { + // TODO Auto-generated method stub + return super.getMatrix(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#getMatrix(android.graphics.Matrix) + */ + @Override + public void getMatrix(Matrix ctm) { + // TODO Auto-generated method stub + super.getMatrix(ctm); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#isOpaque() + */ + @Override + public boolean isOpaque() { + // TODO Auto-generated method stub + return super.isOpaque(); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#saveLayer(float, float, float, float, android.graphics.Paint, int) + */ + @Override + public int saveLayer(float left, float top, float right, float bottom, Paint paint, + int saveFlags) { + // TODO Auto-generated method stub + return super.saveLayer(left, top, right, bottom, paint, saveFlags); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#saveLayer(android.graphics.RectF, android.graphics.Paint, int) + */ + @Override + public int saveLayer(RectF bounds, Paint paint, int saveFlags) { + // TODO Auto-generated method stub + return super.saveLayer(bounds, paint, saveFlags); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#saveLayerAlpha(float, float, float, float, int, int) + */ + @Override + public int saveLayerAlpha(float left, float top, float right, float bottom, int alpha, + int saveFlags) { + // TODO Auto-generated method stub + return super.saveLayerAlpha(left, top, right, bottom, alpha, saveFlags); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#saveLayerAlpha(android.graphics.RectF, int, int) + */ + @Override + public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags) { + // TODO Auto-generated method stub + return super.saveLayerAlpha(bounds, alpha, saveFlags); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#setBitmap(android.graphics.Bitmap) + */ + @Override + public void setBitmap(Bitmap bitmap) { + // TODO Auto-generated method stub + super.setBitmap(bitmap); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#setDrawFilter(android.graphics.DrawFilter) + */ + @Override + public void setDrawFilter(DrawFilter filter) { + // TODO Auto-generated method stub + super.setDrawFilter(filter); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#setViewport(int, int) + */ + @Override + public void setViewport(int width, int height) { + // TODO Auto-generated method stub + super.setViewport(width, height); + } + + /* (non-Javadoc) + * @see android.graphics.Canvas#skew(float, float) + */ + @Override + public void skew(float sx, float sy) { + // TODO Auto-generated method stub + super.skew(sx, sy); + } + + + +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeConstants.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeConstants.java new file mode 100644 index 0000000..b426247 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeConstants.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +/** + * Constant definition class.<br> + * <br> + * Most constants have a prefix defining the content. + * <ul> + * <li><code>WS_</code> Workspace path constant. Those are absolute paths, + * from the project root.</li> + * <li><code>OS_</code> OS path constant. These paths are different depending on the platform.</li> + * <li><code>FN_</code> File name constant.</li> + * <li><code>FD_</code> Folder name constant.</li> + * <li><code>EXT_</code> File extension constant. This does NOT include a dot.</li> + * <li><code>DOT_</code> File extension constant. This start with a dot.</li> + * <li><code>RE_</code> Regexp constant.</li> + * <li><code>NS_</code> Namespace constant.</li> + * <li><code>CLASS_</code> Fully qualified class name.</li> + * </ul> + * + */ +public class BridgeConstants { + + /** Namespace for the resource XML */ + public final static String NS_RESOURCES = "http://schemas.android.com/apk/res/android"; + + public final static String R = "com.android.internal.R"; + + public final static String PREFIX_ANDROID_RESOURCE_REF = "@android:"; + public final static String PREFIX_RESOURCE_REF = "@"; + public final static String PREFIX_ANDROID_THEME_REF = "?android:"; + public final static String PREFIX_THEME_REF = "?"; + + public final static String PREFIX_ANDROID = "android:"; + + public final static String RES_STYLE = "style"; + public final static String RES_ATTR = "attr"; + public final static String RES_DRAWABLE = "drawable"; + public final static String RES_COLOR = "color"; + public final static String RES_LAYOUT = "layout"; + public final static String RES_STRING = "string"; + public final static String RES_ID = "id"; + + public final static String REFERENCE_STYLE = RES_STYLE + "/"; + public final static String REFERENCE_NULL = "@null"; + + public final static String FILL_PARENT = "fill_parent"; + public final static String WRAP_CONTENT = "wrap_content"; +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContentResolver.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContentResolver.java new file mode 100644 index 0000000..727d6f2 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContentResolver.java @@ -0,0 +1,107 @@ +/* + * 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. + */ + +package com.android.layoutlib.bridge; + +import android.content.ContentResolver; +import android.content.ContentServiceNative; +import android.content.Context; +import android.content.IContentProvider; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Bundle; + +/** + * A mock content resolver for the LayoutLib Bridge. + * <p/> + * It won't serve any actual data but it's good enough for all + * the widgets which expect to have a content resolver available via + * {@link BridgeContext#getContentResolver()}. + */ +public class BridgeContentResolver extends ContentResolver { + + public BridgeContentResolver(Context context) { + super(context); + } + + @Override + public IContentProvider acquireProvider(Context c, String name) { + // ignore + return null; + } + + @Override + public boolean releaseProvider(IContentProvider icp) { + // ignore + return false; + } + + /** + * Stub for the layoutlib bridge content resolver. + * <p/> + * The super implementation accesses the {@link ContentServiceNative#getDefault()} + * which returns null and would make the call crash. Instead we do nothing. + */ + @Override + public void registerContentObserver(Uri uri, boolean notifyForDescendents, + ContentObserver observer) { + // pass + } + + /** + * Stub for the layoutlib bridge content resolver. + * <p/> + * The super implementation accesses the {@link ContentServiceNative#getDefault()} + * which returns null and would make the call crash. Instead we do nothing. + */ + @Override + public void unregisterContentObserver(ContentObserver observer) { + // pass + } + + /** + * Stub for the layoutlib bridge content resolver. + * <p/> + * The super implementation accesses the {@link ContentServiceNative#getDefault()} + * which returns null and would make the call crash. Instead we do nothing. + */ + @Override + public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { + // pass + } + + /** + * Stub for the layoutlib bridge content resolver. + * <p/> + * The super implementation accesses the {@link ContentServiceNative#getDefault()} + * which returns null and would make the call crash. Instead we do nothing. + */ + @Override + public void startSync(Uri uri, Bundle extras) { + // pass + } + + /** + * Stub for the layoutlib bridge content resolver. + * <p/> + * The super implementation accesses the {@link ContentServiceNative#getDefault()} + * which returns null and would make the call crash. Instead we do nothing. + */ + @Override + public void cancelSync(Uri uri) { + // pass + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContext.java new file mode 100644 index 0000000..baa3d53 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeContext.java @@ -0,0 +1,1148 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.layoutlib.api.ILayoutLog; +import com.android.layoutlib.api.IProjectCallback; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.api.IStyleResourceValue; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.Resources.Theme; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.BridgeInflater; +import android.view.View; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.Map.Entry; + +/** + * Custom implementation of Context to handle non compiled resources. + */ +public final class BridgeContext extends Context { + + private Resources mResources; + private Theme mTheme; + private HashMap<View, Object> mViewKeyMap = new HashMap<View, Object>(); + private IStyleResourceValue mThemeValues; + private final Object mProjectKey; + private Map<String, Map<String, IResourceValue>> mProjectResources; + private Map<String, Map<String, IResourceValue>> mFrameworkResources; + private Map<IStyleResourceValue, IStyleResourceValue> mStyleInheritanceMap; + + // maps for dynamically generated id representing style objects (IStyleResourceValue) + private Map<Integer, IStyleResourceValue> mDynamicIdToStyleMap; + private Map<IStyleResourceValue, Integer> mStyleToDynamicIdMap; + private int mDynamicIdGenerator = 0x01030000; // Base id for framework R.style + + // cache for TypedArray generated from IStyleResourceValue object + private Map<int[], Map<Integer, TypedArray>> mTypedArrayCache; + private BridgeInflater mInflater; + + private final IProjectCallback mProjectCallback; + private final ILayoutLog mLogger; + private BridgeContentResolver mContentResolver; + + /** + * @param projectKey An Object identifying the project. This is used for the cache mechanism. + * @param metrics the {@link DisplayMetrics}. + * @param themeName The name of the theme to use. + * @param projectResources the resources of the project. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the + * map contains (String, {@link IResourceValue}) pairs where the key is the resource name, + * and the value is the resource value. + * @param frameworkResources the framework resources. The map contains (String, map) pairs + * where the string is the type of the resource reference used in the layout file, and the map + * contains (String, {@link IResourceValue}) pairs where the key is the resource name, and the + * value is the resource value. + * @param styleInheritanceMap + * @param customViewLoader + */ + public BridgeContext(Object projectKey, DisplayMetrics metrics, + IStyleResourceValue currentTheme, + Map<String, Map<String, IResourceValue>> projectResources, + Map<String, Map<String, IResourceValue>> frameworkResources, + Map<IStyleResourceValue, IStyleResourceValue> styleInheritanceMap, + IProjectCallback customViewLoader, ILayoutLog logger) { + mProjectKey = projectKey; + mProjectCallback = customViewLoader; + mLogger = logger; + Configuration config = new Configuration(); + + AssetManager assetManager = BridgeAssetManager.initSystem(); + mResources = BridgeResources.initSystem( + this, + assetManager, + metrics, + config, + customViewLoader); + + mTheme = mResources.newTheme(); + + mThemeValues = currentTheme; + mProjectResources = projectResources; + mFrameworkResources = frameworkResources; + mStyleInheritanceMap = styleInheritanceMap; + } + + public void setBridgeInflater(BridgeInflater inflater) { + mInflater = inflater; + } + + public void addViewKey(View view, Object viewKey) { + mViewKeyMap.put(view, viewKey); + } + + public Object getViewKey(View view) { + return mViewKeyMap.get(view); + } + + public Object getProjectKey() { + return mProjectKey; + } + + public IProjectCallback getProjectCallback() { + return mProjectCallback; + } + + public ILayoutLog getLogger() { + return mLogger; + } + + // ------------ Context methods + + @Override + public Resources getResources() { + return mResources; + } + + @Override + public Theme getTheme() { + return mTheme; + } + + @Override + public ClassLoader getClassLoader() { + return this.getClass().getClassLoader(); + } + + @Override + public Object getSystemService(String service) { + if (LAYOUT_INFLATER_SERVICE.equals(service)) { + return mInflater; + } + + // AutoCompleteTextView and MultiAutoCompleteTextView want a window + // service. We don't have any but it's not worth an exception. + if (WINDOW_SERVICE.equals(service)) { + return null; + } + + throw new UnsupportedOperationException("Unsupported Service: " + service); + } + + + @Override + public final TypedArray obtainStyledAttributes(int[] attrs) { + return createStyleBasedTypedArray(mThemeValues, attrs); + } + + @Override + public final TypedArray obtainStyledAttributes(int resid, int[] attrs) + throws Resources.NotFoundException { + // get the IStyleResourceValue based on the resId; + IStyleResourceValue style = getStyleByDynamicId(resid); + + if (style == null) { + throw new Resources.NotFoundException(); + } + + if (mTypedArrayCache == null) { + mTypedArrayCache = new HashMap<int[], Map<Integer,TypedArray>>(); + + Map<Integer, TypedArray> map = new HashMap<Integer, TypedArray>(); + mTypedArrayCache.put(attrs, map); + + BridgeTypedArray ta = createStyleBasedTypedArray(style, attrs); + map.put(resid, ta); + + return ta; + } + + // get the 2nd map + Map<Integer, TypedArray> map = mTypedArrayCache.get(attrs); + if (map == null) { + map = new HashMap<Integer, TypedArray>(); + mTypedArrayCache.put(attrs, map); + } + + // get the array from the 2nd map + TypedArray ta = map.get(resid); + + if (ta == null) { + ta = createStyleBasedTypedArray(style, attrs); + map.put(resid, ta); + } + + return ta; + } + + @Override + public final TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs) { + return obtainStyledAttributes(set, attrs, 0, 0); + } + + @Override + public TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs, + int defStyleAttr, int defStyleRes) { + + // Hint: for XmlPullParser, attach source //DEVICE_SRC/dalvik/libcore/xml/src/java + BridgeXmlBlockParser parser = null; + if (set instanceof BridgeXmlBlockParser) { + parser = (BridgeXmlBlockParser)set; + } else { + // reall this should not be happening since its instantiated in Bridge + mLogger.error("Parser is not a BridgeXmlBlockParser!"); + return null; + } + + boolean[] frameworkAttributes = new boolean[1]; + TreeMap<Integer, String> styleNameMap = searchAttrs(attrs, frameworkAttributes); + + BridgeTypedArray ta = ((BridgeResources) mResources).newTypeArray(attrs.length, + parser.isPlatformFile()); + + // resolve the defStyleAttr value into a IStyleResourceValue + IStyleResourceValue defStyleValues = null; + if (defStyleAttr != 0) { + // get the name from the int. + String defStyleName = searchAttr(defStyleAttr); + + // look for the style in the current theme, and its parent: + if (mThemeValues != null) { + IResourceValue item = findItemInStyle(mThemeValues, defStyleName); + + if (item != null) { + // item is a reference to a style entry. Search for it. + item = findResValue(item.getValue()); + + if (item instanceof IStyleResourceValue) { + defStyleValues = (IStyleResourceValue)item; + } + } else { + // TODO: log the error properly + System.out.println("Failed to find defStyle: " + defStyleName); + } + } + } + + if (defStyleRes != 0) { + // FIXME: See what we need to do with this. + throw new UnsupportedOperationException(); + } + + String namespace = BridgeConstants.NS_RESOURCES; + if (frameworkAttributes[0] == false) { + // need to use the application namespace + namespace = mProjectCallback.getNamespace(); + } + + if (styleNameMap != null) { + for (Entry<Integer, String> styleAttribute : styleNameMap.entrySet()) { + int index = styleAttribute.getKey().intValue(); + + String name = styleAttribute.getValue(); + String value = parser.getAttributeValue(namespace, name); + + // if there's no direct value for this attribute in the XML, we look for default + // values in the widget defStyle, and then in the theme. + if (value == null) { + IResourceValue resValue = null; + + // look for the value in the defStyle first (and its parent if needed) + if (defStyleValues != null) { + resValue = findItemInStyle(defStyleValues, name); + } + + // if the item is not present in the defStyle, we look in the main theme (and + // its parent themes) + if (resValue == null && mThemeValues != null) { + resValue = findItemInStyle(mThemeValues, name); + } + + // if we found a value, we make sure this doesn't reference another value. + // So we resolve it. + if (resValue != null) { + resValue = resolveResValue(resValue); + } + + ta.bridgeSetValue(index, name, resValue); + } else { + // there is a value in the XML, but we need to resolve it in case it's + // referencing another resource or a theme value. + ta.bridgeSetValue(index, name, resolveValue(null, name, value)); + } + } + } + + ta.sealArray(); + + return ta; + } + + + // ------------- private new methods + + /** + * Creates a {@link BridgeTypedArray} by filling the values defined by the int[] with the + * values found in the given style. + * @see #obtainStyledAttributes(int, int[]) + */ + private BridgeTypedArray createStyleBasedTypedArray(IStyleResourceValue style, int[] attrs) + throws Resources.NotFoundException { + TreeMap<Integer, String> styleNameMap = searchAttrs(attrs, null); + + BridgeTypedArray ta = ((BridgeResources) mResources).newTypeArray(attrs.length, + false /* platformResourceFlag */); + + // loop through all the values in the style map, and init the TypedArray with + // the style we got from the dynamic id + for (Entry<Integer, String> styleAttribute : styleNameMap.entrySet()) { + int index = styleAttribute.getKey().intValue(); + + String name = styleAttribute.getValue(); + + // get the value from the style, or its parent styles. + IResourceValue resValue = findItemInStyle(style, name); + + // resolve it to make sure there are no references left. + ta.bridgeSetValue(index, name, resolveResValue(resValue)); + } + + ta.sealArray(); + + return ta; + } + + + /** + * Resolves the value of a resource, if the value references a theme or resource value. + * <p/> + * This method ensures that it returns a {@link IResourceValue} object that does not + * reference another resource. + * If the resource cannot be resolved, it returns <code>null</code>. + * <p/> + * If a value that does not need to be resolved is given, the method will return a new + * instance of IResourceValue that contains the input value. + * + * @param type the type of the resource + * @param name the name of the attribute containing this value. + * @param value the resource value, or reference to resolve + * @return the resolved resource value or <code>null</code> if it failed to resolve it. + */ + private IResourceValue resolveValue(String type, String name, String value) { + if (value == null) { + return null; + } + + // get the IResourceValue referenced by this value + IResourceValue resValue = findResValue(value); + + // if resValue is null, but value is not null, this means it was not a reference. + // we return the name/value wrapper in a IResourceValue + if (resValue == null) { + return new ResourceValue(type, name, value); + } + + // we resolved a first reference, but we need to make sure this isn't a reference also. + return resolveResValue(resValue); + } + + /** + * Returns the {@link IResourceValue} referenced by the value of <var>value</var>. + * <p/> + * This method ensures that it returns a {@link IResourceValue} object that does not + * reference another resource. + * If the resource cannot be resolved, it returns <code>null</code>. + * <p/> + * If a value that does not need to be resolved is given, the method will return the input + * value. + * + * @param value the value containing the reference to resolve. + * @return a {@link IResourceValue} object or <code>null</code> + */ + IResourceValue resolveResValue(IResourceValue value) { + if (value == null) { + return null; + } + + // if the resource value is a style, we simply return it. + if (value instanceof IStyleResourceValue) { + return value; + } + + // else attempt to find another IResourceValue referenced by this one. + IResourceValue resolvedValue = findResValue(value.getValue()); + + // if the value did not reference anything, then we simply return the input value + if (resolvedValue == null) { + return value; + } + + // otherwise, we attempt to resolve this new value as well + return resolveResValue(resolvedValue); + } + + /** + * Searches for, and returns a {@link IResourceValue} by its reference. + * <p/> + * The reference format can be: + * <pre>@resType/resName</pre> + * <pre>@android:resType/resName</pre> + * <pre>@resType/android:resName</pre> + * <pre>?resType/resName</pre> + * <pre>?android:resType/resName</pre> + * <pre>?resType/android:resName</pre> + * Any other string format will return <code>null</code>. + * <p/> + * The actual format of a reference is <pre>@[namespace:]resType/resName</pre> but this method + * only support the android namespace. + * + * @param reference the resource reference to search for. + * @return a {@link IResourceValue} or <code>null</code>. + */ + IResourceValue findResValue(String reference) { + if (reference == null) { + return null; + } + if (reference.startsWith(BridgeConstants.PREFIX_THEME_REF)) { + // no theme? no need to go further! + if (mThemeValues == null) { + return null; + } + + boolean frameworkOnly = false; + + // eleminate the prefix from the string + if (reference.startsWith(BridgeConstants.PREFIX_ANDROID_THEME_REF)) { + frameworkOnly = true; + reference = reference.substring(BridgeConstants.PREFIX_ANDROID_THEME_REF.length()); + } else { + reference = reference.substring(BridgeConstants.PREFIX_THEME_REF.length()); + } + + // at this point, value can contain type/name (drawable/foo for instance). + // split it to make sure. + String[] segments = reference.split("\\/"); + + // we look for the referenced item name. + String referenceName = null; + + if (segments.length == 2) { + // there was a resType in the reference. If it's attr, we ignore it + // else, we assert for now. + if (BridgeConstants.RES_ATTR.equals(segments[0])) { + referenceName = segments[1]; + } else { + // At this time, no support for ?type/name where type is not "attr" + return null; + } + } else { + // it's just an item name. + referenceName = segments[0]; + } + + // now we look for android: in the referenceName in order to support format + // such as: ?attr/android:name + if (referenceName.startsWith(BridgeConstants.PREFIX_ANDROID)) { + frameworkOnly = true; + referenceName = referenceName.substring(BridgeConstants.PREFIX_ANDROID.length()); + } + + // Now look for the item in the theme, starting with the current one. + if (frameworkOnly) { + // FIXME for now we do the same as if it didn't specify android: + return findItemInStyle(mThemeValues, referenceName); + } + + return findItemInStyle(mThemeValues, referenceName); + } else if (reference.startsWith(BridgeConstants.PREFIX_RESOURCE_REF)) { + boolean frameworkOnly = false; + + // check for the specific null reference value. + if (BridgeConstants.REFERENCE_NULL.equals(reference)) { + return null; + } + + // Eliminate the prefix from the string. + if (reference.startsWith(BridgeConstants.PREFIX_ANDROID_RESOURCE_REF)) { + frameworkOnly = true; + reference = reference.substring( + BridgeConstants.PREFIX_ANDROID_RESOURCE_REF.length()); + } else { + reference = reference.substring(BridgeConstants.PREFIX_RESOURCE_REF.length()); + } + + // at this point, value contains type/[android:]name (drawable/foo for instance) + String[] segments = reference.split("\\/"); + + // now we look for android: in the resource name in order to support format + // such as: @drawable/android:name + if (segments[1].startsWith(BridgeConstants.PREFIX_ANDROID)) { + frameworkOnly = true; + segments[1] = segments[1].substring(BridgeConstants.PREFIX_ANDROID.length()); + } + + return findResValue(segments[0], segments[1], frameworkOnly); + } + + // Looks like the value didn't reference anything. Return null. + return null; + } + + /** + * Searches for, and returns a {@link IResourceValue} by its name, and type. + * @param resType the type of the resource + * @param resName the name of the resource + * @param frameworkOnly if <code>true</code>, the method does not search in the + * project resources + */ + private IResourceValue findResValue(String resType, String resName, boolean frameworkOnly) { + // map of IResouceValue for the given type + Map<String, IResourceValue> typeMap; + + // if allowed, search in the project resources first. + if (frameworkOnly == false) { + typeMap = mProjectResources.get(resType); + if (typeMap != null) { + IResourceValue item = typeMap.get(resName); + if (item != null) { + return item; + } + } + } + + // now search in the framework resources. + typeMap = mFrameworkResources.get(resType); + if (typeMap != null) { + IResourceValue item = typeMap.get(resName); + if (item != null) { + return item; + } + } + + // didn't find the resource anywhere. + return null; + } + + /** + * Returns a framework resource by type and name. The returned resource is resolved. + * @param resourceType the type of the resource + * @param resourceName the name of the resource + */ + public IResourceValue getFrameworkResource(String resourceType, String resourceName) { + return getResource(resourceType, resourceName, mFrameworkResources); + } + + /** + * Returns a project resource by type and name. The returned resource is resolved. + * @param resourceType the type of the resource + * @param resourceName the name of the resource + */ + public IResourceValue getProjectResource(String resourceType, String resourceName) { + return getResource(resourceType, resourceName, mProjectResources); + } + + IResourceValue getResource(String resourceType, String resourceName, + Map<String, Map<String, IResourceValue>> resourceRepository) { + Map<String, IResourceValue> typeMap = resourceRepository.get(resourceType); + if (typeMap != null) { + IResourceValue item = typeMap.get(resourceName); + if (item != null) { + item = resolveResValue(item); + return item; + } + } + + // didn't find the resource anywhere. + return null; + + } + + /** + * Returns the {@link IResourceValue} matching a given name in a given style. If the + * item is not directly available in the style, the method looks in its parent style. + * @param style the style to search in + * @param itemName the name of the item to search for. + * @return the {@link IResourceValue} object or <code>null</code> + */ + IResourceValue findItemInStyle(IStyleResourceValue style, String itemName) { + IResourceValue item = style.findItem(itemName); + + // if we didn't find it, we look in the parent style (if applicable) + if (item == null && mStyleInheritanceMap != null) { + IStyleResourceValue parentStyle = mStyleInheritanceMap.get(style); + if (parentStyle != null) { + return findItemInStyle(parentStyle, itemName); + } + } + + return item; + } + + /** + * The input int[] attrs is one of com.android.internal.R.styleable fields where the name + * of the field is the style being referenced and the array contains one index per attribute. + * <p/> + * searchAttrs() finds all the names of the attributes referenced so for example if + * attrs == com.android.internal.R.styleable.View, this returns the list of the "xyz" where + * there's a field com.android.internal.R.styleable.View_xyz and the field value is the index + * that is used to reference the attribute later in the TypedArray. + * + * @param attrs An attribute array reference given to obtainStyledAttributes. + * @return A sorted map Attribute-Value to Attribute-Name for all attributes declared by the + * attribute array. Returns null if nothing is found. + */ + private TreeMap<Integer,String> searchAttrs(int[] attrs, boolean[] outFrameworkFlag) { + // get the name of the array from the framework resources + String arrayName = Bridge.resolveResourceValue(attrs); + if (arrayName != null) { + // if we found it, get the name of each of the int in the array. + TreeMap<Integer,String> attributes = new TreeMap<Integer, String>(); + for (int i = 0 ; i < attrs.length ; i++) { + String[] info = Bridge.resolveResourceValue(attrs[i]); + if (info != null) { + attributes.put(i, info[0]); + } else { + // FIXME Not sure what we should be doing here... + attributes.put(i, null); + } + } + + if (outFrameworkFlag != null) { + outFrameworkFlag[0] = true; + } + + return attributes; + } + + // if the name was not found in the framework resources, look in the project + // resources + arrayName = mProjectCallback.resolveResourceValue(attrs); + if (arrayName != null) { + TreeMap<Integer,String> attributes = new TreeMap<Integer, String>(); + for (int i = 0 ; i < attrs.length ; i++) { + String[] info = mProjectCallback.resolveResourceValue(attrs[i]); + if (info != null) { + attributes.put(i, info[0]); + } else { + // FIXME Not sure what we should be doing here... + attributes.put(i, null); + } + } + + if (outFrameworkFlag != null) { + outFrameworkFlag[0] = false; + } + + return attributes; + } + + return null; + } + + /** + * Searches for the attribute referenced by its internal id. + * + * @param attr An attribute reference given to obtainStyledAttributes such as defStyle. + * @return The unique name of the attribute, if found, e.g. "buttonStyle". Returns null + * if nothing is found. + */ + public String searchAttr(int attr) { + String[] info = Bridge.resolveResourceValue(attr); + if (info != null) { + return info[0]; + } + + info = mProjectCallback.resolveResourceValue(attr); + if (info != null) { + return info[0]; + } + + return null; + } + + int getDynamicIdByStyle(IStyleResourceValue resValue) { + if (mDynamicIdToStyleMap == null) { + // create the maps. + mDynamicIdToStyleMap = new HashMap<Integer, IStyleResourceValue>(); + mStyleToDynamicIdMap = new HashMap<IStyleResourceValue, Integer>(); + } + + // look for an existing id + Integer id = mStyleToDynamicIdMap.get(resValue); + + if (id == null) { + // generate a new id + id = Integer.valueOf(++mDynamicIdGenerator); + + // and add it to the maps. + mDynamicIdToStyleMap.put(id, resValue); + mStyleToDynamicIdMap.put(resValue, id); + } + + return id; + } + + private IStyleResourceValue getStyleByDynamicId(int i) { + if (mDynamicIdToStyleMap != null) { + return mDynamicIdToStyleMap.get(i); + } + + return null; + } + + int getFrameworkIdValue(String idName, int defValue) { + Integer value = Bridge.getResourceValue(BridgeConstants.RES_ID, idName); + if (value != null) { + return value.intValue(); + } + + return defValue; + } + + int getProjectIdValue(String idName, int defValue) { + if (mProjectCallback != null) { + Integer value = mProjectCallback.getResourceValue(BridgeConstants.RES_ID, idName); + if (value != null) { + return value.intValue(); + } + } + + return defValue; + } + + //------------ NOT OVERRIDEN -------------------- + + @Override + public boolean bindService(Intent arg0, ServiceConnection arg1, int arg2) { + // TODO Auto-generated method stub + return false; + } + + @Override + public int checkCallingOrSelfPermission(String arg0) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int checkCallingOrSelfUriPermission(Uri arg0, int arg1) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int checkCallingPermission(String arg0) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int checkCallingUriPermission(Uri arg0, int arg1) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int checkPermission(String arg0, int arg1, int arg2) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int checkUriPermission(Uri arg0, int arg1, int arg2, int arg3) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int checkUriPermission(Uri arg0, String arg1, String arg2, int arg3, + int arg4, int arg5) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public void clearWallpaper() { + // TODO Auto-generated method stub + + } + + @Override + public Context createPackageContext(String arg0, int arg1) { + // TODO Auto-generated method stub + return null; + } + + @Override + public String[] databaseList() { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean deleteDatabase(String arg0) { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean deleteFile(String arg0) { + // TODO Auto-generated method stub + return false; + } + + @Override + public void enforceCallingOrSelfPermission(String arg0, String arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void enforceCallingOrSelfUriPermission(Uri arg0, int arg1, + String arg2) { + // TODO Auto-generated method stub + + } + + @Override + public void enforceCallingPermission(String arg0, String arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void enforceCallingUriPermission(Uri arg0, int arg1, String arg2) { + // TODO Auto-generated method stub + + } + + @Override + public void enforcePermission(String arg0, int arg1, int arg2, String arg3) { + // TODO Auto-generated method stub + + } + + @Override + public void enforceUriPermission(Uri arg0, int arg1, int arg2, int arg3, + String arg4) { + // TODO Auto-generated method stub + + } + + @Override + public void enforceUriPermission(Uri arg0, String arg1, String arg2, + int arg3, int arg4, int arg5, String arg6) { + // TODO Auto-generated method stub + + } + + @Override + public String[] fileList() { + // TODO Auto-generated method stub + return null; + } + + @Override + public AssetManager getAssets() { + // TODO Auto-generated method stub + return null; + } + + @Override + public File getCacheDir() { + // TODO Auto-generated method stub + return null; + } + + @Override + public ContentResolver getContentResolver() { + if (mContentResolver == null) { + mContentResolver = new BridgeContentResolver(this); + } + return mContentResolver; + } + + @Override + public File getDatabasePath(String arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public File getDir(String arg0, int arg1) { + // TODO Auto-generated method stub + return null; + } + + @Override + public File getFileStreamPath(String arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public File getFilesDir() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getPackageCodePath() { + // TODO Auto-generated method stub + return null; + } + + @Override + public PackageManager getPackageManager() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getPackageName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getPackageResourcePath() { + // TODO Auto-generated method stub + return null; + } + + @Override + public SharedPreferences getSharedPreferences(String arg0, int arg1) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Drawable getWallpaper() { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getWallpaperDesiredMinimumWidth() { + return -1; + } + + @Override + public int getWallpaperDesiredMinimumHeight() { + return -1; + } + + @Override + public void grantUriPermission(String arg0, Uri arg1, int arg2) { + // TODO Auto-generated method stub + + } + + @SuppressWarnings("unused") + @Override + public FileInputStream openFileInput(String arg0) + throws FileNotFoundException { + // TODO Auto-generated method stub + return null; + } + + @SuppressWarnings("unused") + @Override + public FileOutputStream openFileOutput(String arg0, int arg1) + throws FileNotFoundException { + // TODO Auto-generated method stub + return null; + } + + @Override + public SQLiteDatabase openOrCreateDatabase(String arg0, int arg1, + CursorFactory arg2) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Drawable peekWallpaper() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Intent registerReceiver(BroadcastReceiver arg0, IntentFilter arg1) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Intent registerReceiver(BroadcastReceiver arg0, IntentFilter arg1, + String arg2, Handler arg3) { + // TODO Auto-generated method stub + return null; + } + + @Override + public void removeStickyBroadcast(Intent arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void revokeUriPermission(Uri arg0, int arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void sendBroadcast(Intent arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void sendBroadcast(Intent arg0, String arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void sendOrderedBroadcast(Intent arg0, String arg1) { + // TODO Auto-generated method stub + + } + + @Override + public void sendOrderedBroadcast(Intent arg0, String arg1, + BroadcastReceiver arg2, Handler arg3, int arg4, String arg5, + Bundle arg6) { + // TODO Auto-generated method stub + + } + + @Override + public void sendStickyBroadcast(Intent arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void setTheme(int arg0) { + // TODO Auto-generated method stub + + } + + @SuppressWarnings("unused") + @Override + public void setWallpaper(Bitmap arg0) throws IOException { + // TODO Auto-generated method stub + + } + + @SuppressWarnings("unused") + @Override + public void setWallpaper(InputStream arg0) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void startActivity(Intent arg0) { + // TODO Auto-generated method stub + + } + + @Override + public boolean startInstrumentation(ComponentName arg0, String arg1, + Bundle arg2) { + // TODO Auto-generated method stub + return false; + } + + @Override + public ComponentName startService(Intent arg0) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean stopService(Intent arg0) { + // TODO Auto-generated method stub + return false; + } + + @Override + public void unbindService(ServiceConnection arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void unregisterReceiver(BroadcastReceiver arg0) { + // TODO Auto-generated method stub + + } + + @Override + public Looper getMainLooper() { + throw new UnsupportedOperationException(); + } + + @Override + public Context getApplicationContext() { + throw new UnsupportedOperationException(); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeResources.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeResources.java new file mode 100644 index 0000000..0bcc7fd --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeResources.java @@ -0,0 +1,498 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.layoutlib.api.IProjectCallback; +import com.android.layoutlib.api.IResourceValue; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; +import android.content.res.ColorStateList; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.ViewGroup.LayoutParams; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.InputStream; + +/** + * + */ +public final class BridgeResources extends Resources { + + private BridgeContext mContext; + private IProjectCallback mProjectCallback; + private boolean[] mPlatformResourceFlag = new boolean[1]; + + /** + * This initializes the static field {@link Resources#mSystem} which is used + * by methods who get global resources using {@link Resources#getSystem()}. + * <p/> + * They will end up using our bridge resources. + * <p/> + * {@link Bridge} calls this method after setting up a new bridge. + */ + /*package*/ static Resources initSystem(BridgeContext context, + AssetManager assets, + DisplayMetrics metrics, + Configuration config, + IProjectCallback projectCallback) { + if (!(Resources.mSystem instanceof BridgeResources)) { + Resources.mSystem = new BridgeResources(context, + assets, + metrics, + config, + projectCallback); + } + return Resources.mSystem; + } + + /** + * Clears the static {@link Resources#mSystem} to make sure we don't leave objects + * around that would prevent us from unloading the library. + */ + /*package*/ static void clearSystem() { + if (Resources.mSystem instanceof BridgeResources) { + ((BridgeResources)(Resources.mSystem)).mContext = null; + ((BridgeResources)(Resources.mSystem)).mProjectCallback = null; + } + Resources.mSystem = null; + } + + private BridgeResources(BridgeContext context, AssetManager assets, DisplayMetrics metrics, + Configuration config, IProjectCallback projectCallback) { + super(assets, metrics, config); + mContext = context; + mProjectCallback = projectCallback; + } + + public BridgeTypedArray newTypeArray(int numEntries, boolean platformFile) { + return new BridgeTypedArray(this, mContext, numEntries, platformFile); + } + + private IResourceValue getResourceValue(int id, boolean[] platformResFlag_out) { + // first get the String related to this id in the framework + String[] resourceInfo = Bridge.resolveResourceValue(id); + + if (resourceInfo != null) { + platformResFlag_out[0] = true; + return mContext.getFrameworkResource(resourceInfo[1], resourceInfo[0]); + } + + // didn't find a match in the framework? look in the project. + if (mProjectCallback != null) { + resourceInfo = mProjectCallback.resolveResourceValue(id); + + if (resourceInfo != null) { + platformResFlag_out[0] = false; + return mContext.getProjectResource(resourceInfo[1], resourceInfo[0]); + } + } + + return null; + } + + @Override + public Drawable getDrawable(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + return ResourceHelper.getDrawable(value.getValue(), mContext, value.isFramework()); + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public int getColor(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + try { + return ResourceHelper.getColor(value.getValue()); + } catch (NumberFormatException e) { + return 0; + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return 0; + } + + @Override + public ColorStateList getColorStateList(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + try { + int color = ResourceHelper.getColor(value.getValue()); + return ColorStateList.valueOf(color); + } catch (NumberFormatException e) { + return null; + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public CharSequence getText(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + return value.getValue(); + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public XmlResourceParser getLayout(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + File xml = new File(value.getValue()); + if (xml.isFile()) { + // we need to create a pull parser around the layout XML file, and then + // give that to our XmlBlockParser + try { + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(xml)); + + return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]); + } catch (XmlPullParserException e) { + mContext.getLogger().error(e); + + // we'll return null below. + } catch (FileNotFoundException e) { + // this shouldn't happen since we check above. + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public TypedArray obtainAttributes(AttributeSet set, int[] attrs) { + return mContext.obtainStyledAttributes(set, attrs); + } + + @Override + public TypedArray obtainTypedArray(int id) throws NotFoundException { + throw new UnsupportedOperationException(); + } + + + @Override + public float getDimension(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getValue(); + + if (v != null) { + if (v.equals(BridgeConstants.FILL_PARENT)) { + return LayoutParams.FILL_PARENT; + } else if (v.equals(BridgeConstants.WRAP_CONTENT)) { + return LayoutParams.WRAP_CONTENT; + } + + if (ResourceHelper.stringToFloat(v, mTmpValue) && + mTmpValue.type == TypedValue.TYPE_DIMENSION) { + return mTmpValue.getDimension(mMetrics); + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return 0; + } + + @Override + public int getDimensionPixelOffset(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getValue(); + + if (v != null) { + if (ResourceHelper.stringToFloat(v, mTmpValue) && + mTmpValue.type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelOffset(mTmpValue.data, mMetrics); + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return 0; + } + + @Override + public int getDimensionPixelSize(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getValue(); + + if (v != null) { + if (ResourceHelper.stringToFloat(v, mTmpValue) && + mTmpValue.type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelSize(mTmpValue.data, mMetrics); + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return 0; + } + + @Override + public int getInteger(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null && value.getValue() != null) { + String v = value.getValue(); + int radix = 10; + if (v.startsWith("0x")) { + v = v.substring(2); + radix = 16; + } + try { + return Integer.parseInt(v, radix); + } catch (NumberFormatException e) { + // return exception below + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return 0; + } + + @Override + public String getResourceEntryName(int resid) throws NotFoundException { + throw new UnsupportedOperationException(); + } + + @Override + public String getResourceName(int resid) throws NotFoundException { + throw new UnsupportedOperationException(); + } + + @Override + public String getResourceTypeName(int resid) throws NotFoundException { + throw new UnsupportedOperationException(); + } + + @Override + public String getString(int id, Object... formatArgs) throws NotFoundException { + String s = getString(id); + if (s != null) { + return String.format(s, formatArgs); + + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public String getString(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null && value.getValue() != null) { + return value.getValue(); + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public void getValue(int id, TypedValue outValue, boolean resolveRefs) + throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getValue(); + + if (v != null) { + if (ResourceHelper.stringToFloat(v, outValue)) { + return; + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + } + + @Override + public void getValue(String name, TypedValue outValue, boolean resolveRefs) + throws NotFoundException { + throw new UnsupportedOperationException(); + } + + @Override + public XmlResourceParser getXml(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getValue(); + + if (v != null) { + // check this is a file + File f = new File(value.getValue()); + if (f.isFile()) { + try { + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(f)); + + return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]); + } catch (XmlPullParserException e) { + NotFoundException newE = new NotFoundException(); + newE.initCause(e); + throw newE; + } catch (FileNotFoundException e) { + NotFoundException newE = new NotFoundException(); + newE.initCause(e); + throw newE; + } + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public InputStream openRawResource(int id) throws NotFoundException { + IResourceValue value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getValue(); + + if (v != null) { + // check this is a file + File f = new File(value.getValue()); + if (f.isFile()) { + try { + return new FileInputStream(f); + } catch (FileNotFoundException e) { + NotFoundException newE = new NotFoundException(); + newE.initCause(e); + throw newE; + } + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return null; + } + + @Override + public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException { + throw new UnsupportedOperationException(); + } + + /** + * Builds and throws a {@link Resources.NotFoundException} based on a resource id and a resource type. + * @param id the id of the resource + * @throws NotFoundException + */ + private void throwException(int id) throws NotFoundException { + // first get the String related to this id in the framework + String[] resourceInfo = Bridge.resolveResourceValue(id); + + // if the name is unknown in the framework, get it from the custom view loader. + if (resourceInfo == null && mProjectCallback != null) { + resourceInfo = mProjectCallback.resolveResourceValue(id); + } + + String message = null; + if (resourceInfo != null) { + message = String.format( + "Could not find %1$s resource matching value 0x%2$X (resolved name: %3$s) in current configuration.", + resourceInfo[1], id, resourceInfo[0]); + } else { + message = String.format( + "Could not resolve resource value: 0x%1$X.", id); + } + + throw new NotFoundException(message); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeTypedArray.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeTypedArray.java new file mode 100644 index 0000000..f5da91d --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeTypedArray.java @@ -0,0 +1,781 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.internal.util.XmlUtils; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.api.IStyleResourceValue; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; + +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.ViewGroup.LayoutParams; + +import java.io.File; +import java.io.FileReader; +import java.util.Map; + +/** + * TODO: describe. + */ +public final class BridgeTypedArray extends TypedArray { + + @SuppressWarnings("hiding") + private BridgeResources mResources; + private BridgeContext mContext; + @SuppressWarnings("hiding") + private IResourceValue[] mData; + private String[] mNames; + private final boolean mPlatformFile; + + public BridgeTypedArray(BridgeResources resources, BridgeContext context, int len, + boolean platformFile) { + super(null, null, null, 0); + mResources = resources; + mContext = context; + mPlatformFile = platformFile; + mData = new IResourceValue[len]; + mNames = new String[len]; + } + + /** A bridge-specific method that sets a value in the type array */ + public void bridgeSetValue(int index, String name, IResourceValue value) { + mData[index] = value; + mNames[index] = name; + } + + /** + * Seals the array after all calls to {@link #bridgeSetValue(int, String, IResourceValue)} have + * been done. + * <p/>This allows to compute the list of non default values, permitting + * {@link #getIndexCount()} to return the proper value. + */ + public void sealArray() { + // fills TypedArray.mIndices which is used to implement getIndexCount/getIndexAt + // first count the array size + int count = 0; + for (IResourceValue data : mData) { + if (data != null) { + count++; + } + } + + // allocate the table with an extra to store the size + mIndices = new int[count+1]; + mIndices[0] = count; + + // fill the array with the indices. + int index = 1; + for (int i = 0 ; i < mData.length ; i++) { + if (mData[i] != null) { + mIndices[index++] = i; + } + } + } + + /** + * Return the number of values in this array. + */ + @Override + public int length() { + return mData.length; + } + + /** + * Return the Resources object this array was loaded from. + */ + @Override + public Resources getResources() { + return mResources; + } + + /** + * Retrieve the styled string value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * + * @return CharSequence holding string data. May be styled. Returns + * null if the attribute is not defined. + */ + @Override + public CharSequence getText(int index) { + if (mData[index] != null) { + // FIXME: handle styled strings! + return mData[index].getValue(); + } + + return null; + } + + /** + * Retrieve the string value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * + * @return String holding string data. Any styling information is + * removed. Returns null if the attribute is not defined. + */ + @Override + public String getString(int index) { + if (mData[index] != null) { + return mData[index].getValue(); + } + + return null; + } + + /** + * Retrieve the boolean value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined. + * + * @return Attribute boolean value, or defValue if not defined. + */ + @Override + public boolean getBoolean(int index, boolean defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + if (s != null) { + return XmlUtils.convertValueToBoolean(s, defValue); + } + + return defValue; + } + + /** + * Retrieve the integer value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined. + * + * @return Attribute int value, or defValue if not defined. + */ + @Override + public int getInt(int index, int defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + + try { + return (s == null) ? defValue : XmlUtils.convertValueToInt(s, defValue); + } catch (NumberFormatException e) { + // pass + } + + // Field is not null and is not an integer. + // Check for possible constants and try to find them. + // Get the map of attribute-constant -> IntegerValue + Map<String, Integer> map = Bridge.getEnumValues(mNames[index]); + + if (map != null) { + // accumulator to store the value of the 1+ constants. + int result = 0; + + // split the value in case this is a mix of several flags. + String[] keywords = s.split("\\|"); + for (String keyword : keywords) { + Integer i = map.get(keyword.trim()); + if (i != null) { + result |= i.intValue(); + } else { + mContext.getLogger().warning(String.format( + "Unknown constant \"%s\" in attribute \"%2$s\"", + keyword, mNames[index])); + } + } + return result; + } + + return defValue; + } + + /** + * Retrieve the float value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * + * @return Attribute float value, or defValue if not defined.. + */ + @Override + public float getFloat(int index, float defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + + if (s != null) { + try { + return Float.parseFloat(s); + } catch (NumberFormatException e) { + mContext.getLogger().warning(String.format( + "Unable to convert \"%s\" into a float in attribute \"%2$s\"", + s, mNames[index])); + + // we'll return the default value below. + } + } + return defValue; + } + + /** + * Retrieve the color value for the attribute at <var>index</var>. If + * the attribute references a color resource holding a complex + * {@link android.content.res.ColorStateList}, then the default color from + * the set is returned. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute color value, or defValue if not defined. + */ + @Override + public int getColor(int index, int defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + try { + return ResourceHelper.getColor(s); + } catch (NumberFormatException e) { + mContext.getLogger().warning(String.format( + "Unable to convert \"%s\" into a color in attribute \"%2$s\"", + s, mNames[index])); + + // we'll return the default value below. + } + + return defValue; + } + + /** + * Retrieve the ColorStateList for the attribute at <var>index</var>. + * The value may be either a single solid color or a reference to + * a color or complex {@link android.content.res.ColorStateList} description. + * + * @param index Index of attribute to retrieve. + * + * @return ColorStateList for the attribute, or null if not defined. + */ + @Override + public ColorStateList getColorStateList(int index) { + if (mData[index] == null) { + return null; + } + + String value = mData[index].getValue(); + + if (value == null) { + return null; + } + + try { + int color = ResourceHelper.getColor(value); + return ColorStateList.valueOf(color); + } catch (NumberFormatException e) { + // if it's not a color value, we'll attempt to read the xml based color below. + } + + // let the framework inflate the ColorStateList from the XML file. + try { + File f = new File(value); + if (f.isFile()) { + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(f)); + + ColorStateList colorStateList = ColorStateList.createFromXml( + mContext.getResources(), + // FIXME: we need to know if this resource is platform or not + new BridgeXmlBlockParser(parser, mContext, false)); + return colorStateList; + } + } catch (Exception e) { + // this is an error and not warning since the file existence is checked before + // attempting to parse it. + mContext.getLogger().error(e); + + // return null below. + } + + // looks like were unable to resolve the color value. + mContext.getLogger().warning(String.format( + "Unable to resolve color value \"%1$s\" in attribute \"%2$s\"", + value, mNames[index])); + + return null; + } + + /** + * Retrieve the integer value for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute integer value, or defValue if not defined. + */ + @Override + public int getInteger(int index, int defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + + if (s != null) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + mContext.getLogger().warning(String.format( + "Unable to convert \"%s\" into a integer in attribute \"%2$s\"", + s, mNames[index])); + + // The default value is returned below. + } + } + + return defValue; + } + + /** + * Retrieve a dimensional unit attribute at <var>index</var>. Unit + * conversions are based on the current {@link DisplayMetrics} + * associated with the resources this {@link TypedArray} object + * came from. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute dimension value multiplied by the appropriate + * metric, or defValue if not defined. + * + * @see #getDimensionPixelOffset + * @see #getDimensionPixelSize + */ + @Override + public float getDimension(int index, float defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + + if (s == null) { + return defValue; + } else if (s.equals(BridgeConstants.FILL_PARENT)) { + return LayoutParams.FILL_PARENT; + } else if (s.equals(BridgeConstants.WRAP_CONTENT)) { + return LayoutParams.WRAP_CONTENT; + } + + if (ResourceHelper.stringToFloat(s, mValue)) { + return mValue.getDimension(mResources.mMetrics); + } + + // looks like we were unable to resolve the dimension value + mContext.getLogger().warning(String.format( + "Unable to resolve dimension value \"%1$s\" in attribute \"%2$s\"", + s, mNames[index])); + + return defValue; + } + + /** + * Retrieve a dimensional unit attribute at <var>index</var> for use + * as an offset in raw pixels. This is the same as + * {@link #getDimension}, except the returned value is converted to + * integer pixels for you. An offset conversion involves simply + * truncating the base value to an integer. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute dimension value multiplied by the appropriate + * metric and truncated to integer pixels, or defValue if not defined. + * + * @see #getDimension + * @see #getDimensionPixelSize + */ + @Override + public int getDimensionPixelOffset(int index, int defValue) { + return (int) getDimension(index, defValue); + } + + /** + * Retrieve a dimensional unit attribute at <var>index</var> for use + * as a size in raw pixels. This is the same as + * {@link #getDimension}, except the returned value is converted to + * integer pixels for use as a size. A size conversion involves + * rounding the base value, and ensuring that a non-zero base value + * is at least one pixel in size. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute dimension value multiplied by the appropriate + * metric and truncated to integer pixels, or defValue if not defined. + * + * @see #getDimension + * @see #getDimensionPixelOffset + */ + @Override + public int getDimensionPixelSize(int index, int defValue) { + if (mData[index] == null) { + return defValue; + } + + String s = mData[index].getValue(); + + if (s == null) { + return defValue; + } else if (s.equals(BridgeConstants.FILL_PARENT)) { + return LayoutParams.FILL_PARENT; + } else if (s.equals(BridgeConstants.WRAP_CONTENT)) { + return LayoutParams.WRAP_CONTENT; + } + + // FIXME huh? + + float f = getDimension(index, defValue); + final int res = (int)(f+0.5f); + if (res != 0) return res; + if (f == 0) return 0; + if (f > 0) return 1; + + throw new UnsupportedOperationException("Can't convert to dimension: " + + Integer.toString(index)); + } + + /** + * Special version of {@link #getDimensionPixelSize} for retrieving + * {@link android.view.ViewGroup}'s layout_width and layout_height + * attributes. This is only here for performance reasons; applications + * should use {@link #getDimensionPixelSize}. + * + * @param index Index of the attribute to retrieve. + * @param name Textual name of attribute for error reporting. + * + * @return Attribute dimension value multiplied by the appropriate + * metric and truncated to integer pixels. + */ + @Override + public int getLayoutDimension(int index, String name) { + return getDimensionPixelSize(index, 0); + } + + /** + * Retrieve a fractional unit attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param base The base value of this fraction. In other words, a + * standard fraction is multiplied by this value. + * @param pbase The parent base value of this fraction. In other + * words, a parent fraction (nn%p) is multiplied by this + * value. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute fractional value multiplied by the appropriate + * base value, or defValue if not defined. + */ + @Override + public float getFraction(int index, int base, int pbase, float defValue) { + if (mData[index] == null) { + return defValue; + } + + String value = mData[index].getValue(); + if (value == null) { + return defValue; + } + + if (ResourceHelper.stringToFloat(value, mValue)) { + return mValue.getFraction(base, pbase); + } + + // looks like we were unable to resolve the fraction value + mContext.getLogger().warning(String.format( + "Unable to resolve fraction value \"%1$s\" in attribute \"%2$s\"", + value, mNames[index])); + + return defValue; + } + + /** + * Retrieve the resource identifier for the attribute at + * <var>index</var>. Note that attribute resource as resolved when + * the overall {@link TypedArray} object is retrieved. As a + * result, this function will return the resource identifier of the + * final resource value that was found, <em>not</em> necessarily the + * original resource that was specified by the attribute. + * + * @param index Index of attribute to retrieve. + * @param defValue Value to return if the attribute is not defined or + * not a resource. + * + * @return Attribute resource identifier, or defValue if not defined. + */ + @Override + public int getResourceId(int index, int defValue) { + // get the IResource for this index + IResourceValue resValue = mData[index]; + + // no data, return the default value. + if (resValue == null) { + return defValue; + } + + // check if this is a style resource + if (resValue instanceof IStyleResourceValue) { + // get the id that will represent this style. + return mContext.getDynamicIdByStyle((IStyleResourceValue)resValue); + } + + // if the attribute was a reference to an id, and not a declaration of an id (@+id), then + // the xml attribute value was "resolved" which leads us to a IResourceValue with + // getType() returning "id" and getName() returning the id name + // (and getValue() returning null!). We need to handle this! + if (resValue.getType() != null && resValue.getType().equals(BridgeConstants.RES_ID)) { + // if this is a framework id + if (mPlatformFile || resValue.isFramework()) { + // look for idName in the android R classes + return mContext.getFrameworkIdValue(resValue.getName(), defValue); + } + + // look for idName in the project R class. + return mContext.getProjectIdValue(resValue.getName(), defValue); + } + + // else, try to get the value, and resolve it somehow. + String value = resValue.getValue(); + if (value == null) { + return defValue; + } + + // if the value is just an integer, return it. + try { + int i = Integer.parseInt(value); + if (Integer.toString(i).equals(value)) { + return i; + } + } catch (NumberFormatException e) { + // pass + } + + // Handle the @id/<name>, @+id/<name> and @android:id/<name> + // We need to return the exact value that was compiled (from the various R classes), + // as these values can be reused internally with calls to findViewById(). + // There's a trick with platform layouts that not use "android:" but their IDs are in + // fact in the android.R and com.android.internal.R classes. + // The field mPlatformFile will indicate that all IDs are to be looked up in the android R + // classes exclusively. + + // if this is a reference to an id, find it. + if (value.startsWith("@id/") || value.startsWith("@+") || + value.startsWith("@android:id/")) { + + int pos = value.indexOf('/'); + String idName = value.substring(pos + 1); + + // if this is a framework id + if (mPlatformFile || value.startsWith("@android") || value.startsWith("@+android")) { + // look for idName in the android R classes + return mContext.getFrameworkIdValue(idName, defValue); + } + + // look for idName in the project R class. + return mContext.getProjectIdValue(idName, defValue); + } + + // not a direct id valid reference? resolve it + Integer idValue = null; + + if (resValue.isFramework()) { + idValue = Bridge.getResourceValue(resValue.getType(), resValue.getName()); + } else { + idValue = mContext.getProjectCallback().getResourceValue( + resValue.getType(), resValue.getName()); + } + + if (idValue != null) { + return idValue.intValue(); + } + + mContext.getLogger().warning(String.format( + "Unable to resolve id \"%1$s\" for attribute \"%2$s\"", value, mNames[index])); + return defValue; + } + + /** + * Retrieve the Drawable for the attribute at <var>index</var>. This + * gets the resource ID of the selected attribute, and uses + * {@link Resources#getDrawable Resources.getDrawable} of the owning + * Resources object to retrieve its Drawable. + * + * @param index Index of attribute to retrieve. + * + * @return Drawable for the attribute, or null if not defined. + */ + @Override + public Drawable getDrawable(int index) { + if (mData[index] == null) { + return null; + } + + String value = mData[index].getValue(); + if (value == null || BridgeConstants.REFERENCE_NULL.equals(value)) { + return null; + } + + Drawable d = ResourceHelper.getDrawable(value, mContext, mData[index].isFramework()); + + if (d != null) { + return d; + } + + // looks like we were unable to resolve the drawable + mContext.getLogger().warning(String.format( + "Unable to resolve drawable \"%1$s\" in attribute \"%2$s\"", value, mNames[index])); + + return null; + } + + + /** + * Retrieve the CharSequence[] for the attribute at <var>index</var>. + * This gets the resource ID of the selected attribute, and uses + * {@link Resources#getTextArray Resources.getTextArray} of the owning + * Resources object to retrieve its String[]. + * + * @param index Index of attribute to retrieve. + * + * @return CharSequence[] for the attribute, or null if not defined. + */ + @Override + public CharSequence[] getTextArray(int index) { + if (mData[index] == null) { + return null; + } + + String value = mData[index].getValue(); + if (value == null) { + return null; + } + + throw new UnsupportedOperationException( + String.format("BridgeTypedArray: UNKNOWN VALUE FOR getTextArray(%d) => %s", //DEBUG + index, value)); + + } + + /** + * Retrieve the raw TypedValue for the attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * @param outValue TypedValue object in which to place the attribute's + * data. + * + * @return Returns true if the value was retrieved, else false. + */ + @Override + public boolean getValue(int index, TypedValue outValue) { + if (mData[index] == null) { + return false; + } + + String s = mData[index].getValue(); + + return ResourceHelper.stringToFloat(s, outValue); + } + + /** + * Determines whether there is an attribute at <var>index</var>. + * + * @param index Index of attribute to retrieve. + * + * @return True if the attribute has a value, false otherwise. + */ + @Override + public boolean hasValue(int index) { + return mData[index] != null; + } + + /** + * Retrieve the raw TypedValue for the attribute at <var>index</var> + * and return a temporary object holding its data. This object is only + * valid until the next call on to {@link TypedArray}. + * + * @param index Index of attribute to retrieve. + * + * @return Returns a TypedValue object if the attribute is defined, + * containing its data; otherwise returns null. (You will not + * receive a TypedValue whose type is TYPE_NULL.) + */ + @Override + public TypedValue peekValue(int index) { + if (getValue(index, mValue)) { + return mValue; + } + + return null; + } + + /** + * Returns a message about the parser state suitable for printing error messages. + */ + @Override + public String getPositionDescription() { + return "<internal -- stub if needed>"; + } + + /** + * Give back a previously retrieved StyledAttributes, for later re-use. + */ + @Override + public void recycle() { + // pass + } + + @Override + public boolean getValueAt(int index, TypedValue outValue) { + // pass + return false; + } + + @Override + public String toString() { + return mData.toString(); + } + } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeXmlBlockParser.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeXmlBlockParser.java new file mode 100644 index 0000000..d842a66 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeXmlBlockParser.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.layoutlib.api.IXmlPullParser; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.res.XmlResourceParser; +import android.util.AttributeSet; +import android.util.XmlPullAttributes; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +/** + * {@link BridgeXmlBlockParser} reimplements most of android.xml.XmlBlock.Parser. + * It delegates to both an instance of {@link XmlPullParser} and an instance of + * {@link XmlPullAttributes} (for the {@link AttributeSet} part). + */ +public class BridgeXmlBlockParser implements XmlResourceParser { + + private XmlPullParser mParser; + private XmlPullAttributes mAttrib; + + private boolean mStarted = false; + private boolean mDecNextDepth = false; + private int mDepth = 0; + private int mEventType = START_DOCUMENT; + private final boolean mPlatformFile; + + /** + * Builds a {@link BridgeXmlBlockParser}. + * @param parser The XmlPullParser to get the content from. + * @param context the Context. + * @param platformFile Indicates whether the the file is a platform file or not. + */ + public BridgeXmlBlockParser(XmlPullParser parser, BridgeContext context, boolean platformFile) { + mParser = parser; + mPlatformFile = platformFile; + mAttrib = new BridgeXmlPullAttributes(parser, context, mPlatformFile); + } + + public boolean isPlatformFile() { + return mPlatformFile; + } + + public Object getViewKey() { + if (mParser instanceof IXmlPullParser) { + return ((IXmlPullParser)mParser).getViewKey(); + } + + return null; + } + + + // ------- XmlResourceParser implementation + + public void setFeature(String name, boolean state) + throws XmlPullParserException { + if (FEATURE_PROCESS_NAMESPACES.equals(name) && state) { + return; + } + if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name) && state) { + return; + } + throw new XmlPullParserException("Unsupported feature: " + name); + } + + public boolean getFeature(String name) { + if (FEATURE_PROCESS_NAMESPACES.equals(name)) { + return true; + } + if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name)) { + return true; + } + return false; + } + + public void setProperty(String name, Object value) throws XmlPullParserException { + throw new XmlPullParserException("setProperty() not supported"); + } + + public Object getProperty(String name) { + return null; + } + + public void setInput(Reader in) throws XmlPullParserException { + mParser.setInput(in); + } + + public void setInput(InputStream inputStream, String inputEncoding) + throws XmlPullParserException { + mParser.setInput(inputStream, inputEncoding); + } + + public void defineEntityReplacementText(String entityName, + String replacementText) throws XmlPullParserException { + throw new XmlPullParserException( + "defineEntityReplacementText() not supported"); + } + + public String getNamespacePrefix(int pos) throws XmlPullParserException { + throw new XmlPullParserException("getNamespacePrefix() not supported"); + } + + public String getInputEncoding() { + return null; + } + + public String getNamespace(String prefix) { + throw new RuntimeException("getNamespace() not supported"); + } + + public int getNamespaceCount(int depth) throws XmlPullParserException { + throw new XmlPullParserException("getNamespaceCount() not supported"); + } + + public String getPositionDescription() { + return "Binary XML file line #" + getLineNumber(); + } + + public String getNamespaceUri(int pos) throws XmlPullParserException { + throw new XmlPullParserException("getNamespaceUri() not supported"); + } + + public int getColumnNumber() { + return -1; + } + + public int getDepth() { + return mDepth; + } + + public String getText() { + return mParser.getText(); + } + + public int getLineNumber() { + return mParser.getLineNumber(); + } + + public int getEventType() { + return mEventType; + } + + public boolean isWhitespace() throws XmlPullParserException { + // Original comment: whitespace was stripped by aapt. + return mParser.isWhitespace(); + } + + public String getPrefix() { + throw new RuntimeException("getPrefix not supported"); + } + + public char[] getTextCharacters(int[] holderForStartAndLength) { + String txt = getText(); + char[] chars = null; + if (txt != null) { + holderForStartAndLength[0] = 0; + holderForStartAndLength[1] = txt.length(); + chars = new char[txt.length()]; + txt.getChars(0, txt.length(), chars, 0); + } + return chars; + } + + public String getNamespace() { + return mParser.getNamespace(); + } + + public String getName() { + return mParser.getName(); + } + + public String getAttributeNamespace(int index) { + return mParser.getAttributeNamespace(index); + } + + public String getAttributeName(int index) { + return mParser.getAttributeName(index); + } + + public String getAttributePrefix(int index) { + throw new RuntimeException("getAttributePrefix not supported"); + } + + public boolean isEmptyElementTag() { + // XXX Need to detect this. + return false; + } + + public int getAttributeCount() { + return mParser.getAttributeCount(); + } + + public String getAttributeValue(int index) { + return mParser.getAttributeValue(index); + } + + public String getAttributeType(int index) { + return "CDATA"; + } + + public boolean isAttributeDefault(int index) { + return false; + } + + public int nextToken() throws XmlPullParserException, IOException { + return next(); + } + + public String getAttributeValue(String namespace, String name) { + return mParser.getAttributeValue(namespace, name); + } + + public int next() throws XmlPullParserException, IOException { + if (!mStarted) { + mStarted = true; + return START_DOCUMENT; + } + int ev = mParser.next(); + if (mDecNextDepth) { + mDepth--; + mDecNextDepth = false; + } + switch (ev) { + case START_TAG: + mDepth++; + break; + case END_TAG: + mDecNextDepth = true; + break; + } + mEventType = ev; + return ev; + } + + public void require(int type, String namespace, String name) + throws XmlPullParserException { + if (type != getEventType() + || (namespace != null && !namespace.equals(getNamespace())) + || (name != null && !name.equals(getName()))) + throw new XmlPullParserException("expected " + TYPES[type] + + getPositionDescription()); + } + + public String nextText() throws XmlPullParserException, IOException { + if (getEventType() != START_TAG) { + throw new XmlPullParserException(getPositionDescription() + + ": parser must be on START_TAG to read next text", this, + null); + } + int eventType = next(); + if (eventType == TEXT) { + String result = getText(); + eventType = next(); + if (eventType != END_TAG) { + throw new XmlPullParserException( + getPositionDescription() + + ": event TEXT it must be immediately followed by END_TAG", + this, null); + } + return result; + } else if (eventType == END_TAG) { + return ""; + } else { + throw new XmlPullParserException(getPositionDescription() + + ": parser must be on START_TAG or TEXT to read text", + this, null); + } + } + + public int nextTag() throws XmlPullParserException, IOException { + int eventType = next(); + if (eventType == TEXT && isWhitespace()) { // skip whitespace + eventType = next(); + } + if (eventType != START_TAG && eventType != END_TAG) { + throw new XmlPullParserException(getPositionDescription() + + ": expected start or end tag", this, null); + } + return eventType; + } + + // AttributeSet implementation + + + public void close() { + // pass + } + + public boolean getAttributeBooleanValue(int index, boolean defaultValue) { + return mAttrib.getAttributeBooleanValue(index, defaultValue); + } + + public boolean getAttributeBooleanValue(String namespace, String attribute, + boolean defaultValue) { + return mAttrib.getAttributeBooleanValue(namespace, attribute, defaultValue); + } + + public float getAttributeFloatValue(int index, float defaultValue) { + return mAttrib.getAttributeFloatValue(index, defaultValue); + } + + public float getAttributeFloatValue(String namespace, String attribute, float defaultValue) { + return mAttrib.getAttributeFloatValue(namespace, attribute, defaultValue); + } + + public int getAttributeIntValue(int index, int defaultValue) { + return mAttrib.getAttributeIntValue(index, defaultValue); + } + + public int getAttributeIntValue(String namespace, String attribute, int defaultValue) { + return mAttrib.getAttributeIntValue(namespace, attribute, defaultValue); + } + + public int getAttributeListValue(int index, String[] options, int defaultValue) { + return mAttrib.getAttributeListValue(index, options, defaultValue); + } + + public int getAttributeListValue(String namespace, String attribute, + String[] options, int defaultValue) { + return mAttrib.getAttributeListValue(namespace, attribute, options, defaultValue); + } + + public int getAttributeNameResource(int index) { + return mAttrib.getAttributeNameResource(index); + } + + public int getAttributeResourceValue(int index, int defaultValue) { + return mAttrib.getAttributeResourceValue(index, defaultValue); + } + + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { + return mAttrib.getAttributeResourceValue(namespace, attribute, defaultValue); + } + + public int getAttributeUnsignedIntValue(int index, int defaultValue) { + return mAttrib.getAttributeUnsignedIntValue(index, defaultValue); + } + + public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) { + return mAttrib.getAttributeUnsignedIntValue(namespace, attribute, defaultValue); + } + + public String getClassAttribute() { + return mAttrib.getClassAttribute(); + } + + public String getIdAttribute() { + return mAttrib.getIdAttribute(); + } + + public int getIdAttributeResourceValue(int defaultValue) { + return mAttrib.getIdAttributeResourceValue(defaultValue); + } + + public int getStyleAttribute() { + return mAttrib.getStyleAttribute(); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeXmlPullAttributes.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeXmlPullAttributes.java new file mode 100644 index 0000000..4be6eab --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeXmlPullAttributes.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.layoutlib.api.IResourceValue; + +import org.xmlpull.v1.XmlPullParser; + +import android.util.AttributeSet; +import android.util.XmlPullAttributes; + +/** + * A correct implementation of the {@link AttributeSet} interface on top of a XmlPullParser + */ +public class BridgeXmlPullAttributes extends XmlPullAttributes { + + private final BridgeContext mContext; + private final boolean mPlatformFile; + + public BridgeXmlPullAttributes(XmlPullParser parser, BridgeContext context, + boolean platformFile) { + super(parser); + mContext = context; + mPlatformFile = platformFile; + } + + /* + * (non-Javadoc) + * @see android.util.XmlPullAttributes#getAttributeNameResource(int) + * + * This methods must return com.android.internal.R.attr.<name> matching + * the name of the attribute. + * It returns 0 if it doesn't find anything. + */ + @Override + public int getAttributeNameResource(int index) { + // get the attribute name. + String name = getAttributeName(index); + + // get the attribute namespace + String ns = mParser.getAttributeNamespace(index); + + if (BridgeConstants.NS_RESOURCES.equals(ns)) { + Integer v = Bridge.getResourceValue(BridgeConstants.RES_ATTR, name); + if (v != null) { + return v.intValue(); + } + + return 0; + } + + // this is not an attribute in the android namespace, we query the customviewloader, if + // the namespaces match. + if (mContext.getProjectCallback().getNamespace().equals(ns)) { + Integer v = mContext.getProjectCallback().getResourceValue(BridgeConstants.RES_ATTR, + name); + if (v != null) { + return v.intValue(); + } + } + + return 0; + } + + /* + * (non-Javadoc) + * @see android.util.XmlPullAttributes#getAttributeResourceValue(int, int) + */ + @Override + public int getAttributeResourceValue(int index, int defaultValue) { + String value = getAttributeValue(index); + + return resolveResourceValue(value, defaultValue); + } + + /* + * (non-Javadoc) + * @see android.util.XmlPullAttributes#getAttributeResourceValue(java.lang.String, java.lang.String, int) + */ + @Override + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { + String value = getAttributeValue(namespace, attribute); + + return resolveResourceValue(value, defaultValue); + } + + private int resolveResourceValue(String value, int defaultValue) { + // now look for this particular value + IResourceValue resource = mContext.resolveResValue(mContext.findResValue(value)); + + if (resource != null) { + Integer id = null; + if (mPlatformFile || resource.isFramework()) { + id = Bridge.getResourceValue(resource.getType(), resource.getName()); + } else { + id = mContext.getProjectCallback().getResourceValue( + resource.getType(), resource.getName()); + } + + if (id != null) { + return id; + } + } + + return defaultValue; + } + +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/FontLoader.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/FontLoader.java new file mode 100644 index 0000000..1bdd1cc --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/FontLoader.java @@ -0,0 +1,349 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import android.graphics.Typeface; + +import java.awt.Font; +import java.awt.FontFormatException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +/** + * Provides {@link Font} object to the layout lib. + * <p/> + * The fonts are loaded from the SDK directory. Family/style mapping is done by parsing the + * fonts.xml file located alongside the ttf files. + */ +public final class FontLoader { + private static final String FONTS_DEFINITIONS = "fonts.xml"; + + private static final String NODE_FONTS = "fonts"; + private static final String NODE_FONT = "font"; + private static final String NODE_NAME = "name"; + + private static final String ATTR_TTF = "ttf"; + + private static final String[] NODE_LEVEL = { NODE_FONTS, NODE_FONT, NODE_NAME }; + + private static final String FONT_EXT = ".ttf"; + + private static final String[] FONT_STYLE_DEFAULT = { "", "-Regular" }; + private static final String[] FONT_STYLE_BOLD = { "-Bold" }; + private static final String[] FONT_STYLE_ITALIC = { "-Italic" }; + private static final String[] FONT_STYLE_BOLDITALIC = { "-BoldItalic" }; + + // list of font style, in the order matching the Typeface Font style + private static final String[][] FONT_STYLES = { + FONT_STYLE_DEFAULT, + FONT_STYLE_BOLD, + FONT_STYLE_ITALIC, + FONT_STYLE_BOLDITALIC + }; + + private final Map<String, String> mFamilyToTtf = new HashMap<String, String>(); + private final Map<String, Map<Integer, Font>> mTtfToFontMap = + new HashMap<String, Map<Integer, Font>>(); + + public static FontLoader create(String fontOsLocation) { + try { + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + + SAXParser parser = parserFactory.newSAXParser(); + File f = new File(fontOsLocation + File.separator + FONTS_DEFINITIONS); + + FontDefinitionParser definitionParser = new FontDefinitionParser( + fontOsLocation + File.separator); + parser.parse(new FileInputStream(f), definitionParser); + + return definitionParser.getFontLoader(); + } catch (ParserConfigurationException e) { + // return null below + } catch (SAXException e) { + // return null below + } catch (FileNotFoundException e) { + // return null below + } catch (IOException e) { + // return null below + } + + return null; + } + + private FontLoader(List<FontInfo> fontList) { + for (FontInfo info : fontList) { + for (String family : info.families) { + mFamilyToTtf.put(family, info.ttf); + } + } + } + + public synchronized Font getFont(String family, int[] style) { + if (family == null) { + return null; + } + + // get the ttf name from the family + String ttf = mFamilyToTtf.get(family); + + if (ttf == null) { + return null; + } + + // get the font from the ttf + Map<Integer, Font> styleMap = mTtfToFontMap.get(ttf); + + if (styleMap == null) { + styleMap = new HashMap<Integer, Font>(); + mTtfToFontMap.put(ttf, styleMap); + } + + Font f = styleMap.get(style); + + if (f != null) { + return f; + } + + // if it doesn't exist, we create it, and we can't, we try with a simpler style + switch (style[0]) { + case Typeface.NORMAL: + f = getFont(ttf, FONT_STYLES[Typeface.NORMAL]); + break; + case Typeface.BOLD: + case Typeface.ITALIC: + f = getFont(ttf, FONT_STYLES[style[0]]); + if (f == null) { + f = getFont(ttf, FONT_STYLES[Typeface.NORMAL]); + style[0] = Typeface.NORMAL; + } + break; + case Typeface.BOLD_ITALIC: + f = getFont(ttf, FONT_STYLES[style[0]]); + if (f == null) { + f = getFont(ttf, FONT_STYLES[Typeface.BOLD]); + if (f != null) { + style[0] = Typeface.BOLD; + } else { + f = getFont(ttf, FONT_STYLES[Typeface.ITALIC]); + if (f != null) { + style[0] = Typeface.ITALIC; + } else { + f = getFont(ttf, FONT_STYLES[Typeface.NORMAL]); + style[0] = Typeface.NORMAL; + } + } + } + break; + } + + if (f != null) { + styleMap.put(style[0], f); + return f; + } + + return null; + } + + private Font getFont(String ttf, String[] fontFileSuffix) { + for (String suffix : fontFileSuffix) { + String name = ttf + suffix + FONT_EXT; + + File f = new File(name); + if (f.isFile()) { + try { + Font font = Font.createFont(Font.TRUETYPE_FONT, f); + if (font != null) { + return font; + } + } catch (FontFormatException e) { + // skip this font name + } catch (IOException e) { + // skip this font name + } + } + } + + return null; + } + + private final static class FontInfo { + String ttf; + final Set<String> families; + + FontInfo() { + families = new HashSet<String>(); + } + } + + private final static class FontDefinitionParser extends DefaultHandler { + private final String mOsFontsLocation; + + private int mDepth = 0; + private FontInfo mFontInfo = null; + private final StringBuilder mBuilder = new StringBuilder(); + private final List<FontInfo> mFontList = new ArrayList<FontInfo>(); + + private FontDefinitionParser(String osFontsLocation) { + super(); + mOsFontsLocation = osFontsLocation; + } + + FontLoader getFontLoader() { + return new FontLoader(mFontList); + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes) + */ + @Override + public void startElement(String uri, String localName, String name, Attributes attributes) + throws SAXException { + if (localName.equals(NODE_LEVEL[mDepth])) { + mDepth++; + + if (mDepth == 2) { // font level. + String ttf = attributes.getValue(ATTR_TTF); + if (ttf != null) { + mFontInfo = new FontInfo(); + mFontInfo.ttf = mOsFontsLocation + ttf; + mFontList.add(mFontInfo); + } + } + } + + super.startElement(uri, localName, name, attributes); + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int) + */ + @SuppressWarnings("unused") + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (mFontInfo != null) { + mBuilder.append(ch, start, length); + } + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String) + */ + @SuppressWarnings("unused") + @Override + public void endElement(String uri, String localName, String name) throws SAXException { + if (localName.equals(NODE_LEVEL[mDepth-1])) { + mDepth--; + if (mDepth == 2) { // end of a <name> node + if (mFontInfo != null) { + String family = trimXmlWhitespaces(mBuilder.toString()); + mFontInfo.families.add(family); + mBuilder.setLength(0); + } + } else if (mDepth == 1) { // end of a <font> node + mFontInfo = null; + } + } + } + + private String trimXmlWhitespaces(String value) { + if (value == null) { + return null; + } + + // look for carriage return and replace all whitespace around it by just 1 space. + int index; + + while ((index = value.indexOf('\n')) != -1) { + // look for whitespace on each side + int left = index - 1; + while (left >= 0) { + if (Character.isWhitespace(value.charAt(left))) { + left--; + } else { + break; + } + } + + int right = index + 1; + int count = value.length(); + while (right < count) { + if (Character.isWhitespace(value.charAt(right))) { + right++; + } else { + break; + } + } + + // remove all between left and right (non inclusive) and replace by a single space. + String leftString = null; + if (left >= 0) { + leftString = value.substring(0, left + 1); + } + String rightString = null; + if (right < count) { + rightString = value.substring(right); + } + + if (leftString != null) { + value = leftString; + if (rightString != null) { + value += " " + rightString; + } + } else { + value = rightString != null ? rightString : ""; + } + } + + // now we un-escape the string + int length = value.length(); + char[] buffer = value.toCharArray(); + + for (int i = 0 ; i < length ; i++) { + if (buffer[i] == '\\') { + if (buffer[i+1] == 'n') { + // replace the char with \n + buffer[i+1] = '\n'; + } + + // offset the rest of the buffer since we go from 2 to 1 char + System.arraycopy(buffer, i+1, buffer, i, length - i - 1); + length--; + } + } + + return new String(buffer, 0, length); + } + + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/LayoutResult.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/LayoutResult.java new file mode 100644 index 0000000..c4c5225 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/LayoutResult.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.layoutlib.api.ILayoutResult; + +import java.awt.image.BufferedImage; + +/** + * Implementation of {@link ILayoutResult} + */ +public final class LayoutResult implements ILayoutResult { + + private final ILayoutViewInfo mRootView; + private final BufferedImage mImage; + private final int mSuccess; + private final String mErrorMessage; + + /** + * Creates a {@link #SUCCESS} {@link ILayoutResult} with the specified params + * @param rootView + * @param image + */ + public LayoutResult(ILayoutViewInfo rootView, BufferedImage image) { + mSuccess = SUCCESS; + mErrorMessage = null; + mRootView = rootView; + mImage = image; + } + + /** + * Creates a LayoutResult with a specific success code and associated message + * @param code + * @param message + */ + public LayoutResult(int code, String message) { + mSuccess = code; + mErrorMessage = message; + mRootView = null; + mImage = null; + } + + public int getSuccess() { + return mSuccess; + } + + public String getErrorMessage() { + return mErrorMessage; + } + + public BufferedImage getImage() { + return mImage; + } + + public ILayoutViewInfo getRootView() { + return mRootView; + } + + /** + * Implementation of {@link ILayoutResult.ILayoutViewInfo} + */ + public static final class LayoutViewInfo implements ILayoutViewInfo { + private final Object mKey; + private final String mName; + private final int mLeft; + private final int mRight; + private final int mTop; + private final int mBottom; + private ILayoutViewInfo[] mChildren; + + public LayoutViewInfo(String name, Object key, int left, int top, int right, int bottom) { + mName = name; + mKey = key; + mLeft = left; + mRight = right; + mTop = top; + mBottom = bottom; + } + + public void setChildren(ILayoutViewInfo[] children) { + mChildren = children; + } + + public ILayoutViewInfo[] getChildren() { + return mChildren; + } + + public Object getViewKey() { + return mKey; + } + + public String getName() { + return mName; + } + + public int getLeft() { + return mLeft; + } + + public int getTop() { + return mTop; + } + + public int getRight() { + return mRight; + } + + public int getBottom() { + return mBottom; + } + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java new file mode 100644 index 0000000..1ca3182 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.widget.TextView; + +/** + * Base class for mocked views. + * + * TODO: implement onDraw and draw a rectangle in a random color with the name of the class + * (or better the id of the view). + */ +public class MockView extends TextView { + + public MockView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + setText(this.getClass().getSimpleName()); + setTextColor(0xFF000000); + } + + @Override + public void onDraw(Canvas canvas) { + canvas.drawARGB(0xFF, 0x7F, 0x7F, 0x7F); + + super.onDraw(canvas); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/NinePatchDrawable.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/NinePatchDrawable.java new file mode 100644 index 0000000..5f0852e --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/NinePatchDrawable.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.ninepatch.NinePatch; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +public class NinePatchDrawable extends Drawable { + + private NinePatch m9Patch; + + NinePatchDrawable(NinePatch ninePatch) { + m9Patch = ninePatch; + } + + @Override + public int getMinimumWidth() { + return m9Patch.getWidth(); + } + + @Override + public int getMinimumHeight() { + return m9Patch.getHeight(); + } + + /** + * Return the intrinsic width of the underlying drawable object. Returns + * -1 if it has no intrinsic width, such as with a solid color. + */ + @Override + public int getIntrinsicWidth() { + return m9Patch.getWidth(); + } + + /** + * Return the intrinsic height of the underlying drawable object. Returns + * -1 if it has no intrinsic height, such as with a solid color. + */ + @Override + public int getIntrinsicHeight() { + return m9Patch.getHeight(); + } + + /** + * Return in padding the insets suggested by this Drawable for placing + * content inside the drawable's bounds. Positive values move toward the + * center of the Drawable (set Rect.inset). Returns true if this drawable + * actually has a padding, else false. When false is returned, the padding + * is always set to 0. + */ + @Override + public boolean getPadding(Rect padding) { + int[] padd = new int[4]; + m9Patch.getPadding(padd); + padding.left = padd[0]; + padding.top = padd[1]; + padding.right = padd[2]; + padding.bottom = padd[3]; + return true; + } + + @Override + public void draw(Canvas canvas) { + if (canvas instanceof BridgeCanvas) { + BridgeCanvas bridgeCanvas = (BridgeCanvas)canvas; + + Rect r = getBounds(); + m9Patch.draw(bridgeCanvas.getGraphics2d(), r.left, r.top, r.width(), r.height()); + + return; + } + + throw new UnsupportedOperationException(); + } + + + // ----------- Not implemented methods --------------- + + + @Override + public int getOpacity() { + // FIXME + return 0xFF; + } + + @Override + public void setAlpha(int arg0) { + // FIXME ! + } + + @Override + public void setColorFilter(ColorFilter arg0) { + // FIXME + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/ResourceHelper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/ResourceHelper.java new file mode 100644 index 0000000..fbdf8dc --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/ResourceHelper.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.ninepatch.NinePatch; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper class to provide various convertion method used in handling android resources. + */ +public final class ResourceHelper { + + private final static Pattern sFloatPattern = Pattern.compile("(-?[0-9]+(?:\\.[0-9]+)?)(.*)"); + private final static float[] sFloatOut = new float[1]; + + private final static TypedValue mValue = new TypedValue(); + + /** + * Returns the color value represented by the given string value + * @param value the color value + * @return the color as an int + * @throw NumberFormatException if the conversion failed. + */ + static int getColor(String value) { + if (value != null) { + if (value.startsWith("#") == false) { + throw new NumberFormatException(); + } + + value = value.substring(1); + + // make sure it's not longer than 32bit + if (value.length() > 8) { + throw new NumberFormatException(); + } + + if (value.length() == 3) { // RGB format + char[] color = new char[8]; + color[0] = color[1] = 'F'; + color[2] = color[3] = value.charAt(0); + color[4] = color[5] = value.charAt(1); + color[6] = color[7] = value.charAt(2); + value = new String(color); + } else if (value.length() == 4) { // ARGB format + char[] color = new char[8]; + color[0] = color[1] = value.charAt(0); + color[2] = color[3] = value.charAt(1); + color[4] = color[5] = value.charAt(2); + color[6] = color[7] = value.charAt(3); + value = new String(color); + } else if (value.length() == 6) { + value = "FF" + value; + } + + // this is a RRGGBB or AARRGGBB value + + // Integer.parseInt will fail to parse strings like "ff191919", so we use + // a Long, but cast the result back into an int, since we know that we're only + // dealing with 32 bit values. + return (int)Long.parseLong(value, 16); + } + + throw new NumberFormatException(); + } + + /** + * Returns a drawable from the given value. + * @param value The value. A path to a 9 patch, a bitmap or a xml based drawable, + * or an hexadecimal color + * @param context + * @param isFramework indicates whether the resource is a framework resources. + * Framework resources are cached, and loaded only once. + */ + public static Drawable getDrawable(String value, BridgeContext context, boolean isFramework) { + Drawable d = null; + + String lowerCaseValue = value.toLowerCase(); + + if (lowerCaseValue.endsWith(NinePatch.EXTENSION_9PATCH)) { + File f = new File(value); + if (f.isFile()) { + NinePatch ninePatch = Bridge.getCached9Patch(value, + isFramework ? null : context.getProjectKey()); + + if (ninePatch == null) { + try { + ninePatch = NinePatch.load(new File(value).toURL(), false /* convert */); + + Bridge.setCached9Patch(value, ninePatch, + isFramework ? null : context.getProjectKey()); + } catch (MalformedURLException e) { + // URL is wrong, we'll return null below + } catch (IOException e) { + // failed to read the file, we'll return null below. + } + } + + if (ninePatch != null) { + return new NinePatchDrawable(ninePatch); + } + } + + return null; + } else if (lowerCaseValue.endsWith(".xml")) { + // create a blockparser for the file + File f = new File(value); + if (f.isFile()) { + try { + // let the framework inflate the Drawable from the XML file. + KXmlParser parser = new KXmlParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(f)); + + d = Drawable.createFromXml(context.getResources(), + // FIXME: we need to know if this resource is platform or not + new BridgeXmlBlockParser(parser, context, false)); + return d; + } catch (XmlPullParserException e) { + context.getLogger().error(e); + } catch (FileNotFoundException e) { + // will not happen, since we pre-check + } catch (IOException e) { + context.getLogger().error(e); + } + } + + return null; + } else { + File bmpFile = new File(value); + if (bmpFile.isFile()) { + try { + Bitmap bitmap = Bridge.getCachedBitmap(value, + isFramework ? null : context.getProjectKey()); + + if (bitmap == null) { + bitmap = new Bitmap(bmpFile); + Bridge.setCachedBitmap(value, bitmap, + isFramework ? null : context.getProjectKey()); + } + + return new BitmapDrawable(bitmap); + } catch (IOException e) { + // we'll return null below + // TODO: log the error. + } + } else { + // attempt to get a color from the value + try { + int color = getColor(value); + return new ColorDrawable(color); + } catch (NumberFormatException e) { + // we'll return null below. + // TODO: log the error + } + } + } + + return null; + } + + + // ------- TypedValue stuff + // This is taken from //device/libs/utils/ResourceTypes.cpp + + private static final class UnitEntry { + String name; + int type; + int unit; + float scale; + + UnitEntry(String name, int type, int unit, float scale) { + this.name = name; + this.type = type; + this.unit = unit; + this.scale = scale; + } + } + + private final static UnitEntry[] sUnitNames = new UnitEntry[] { + new UnitEntry("px", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PX, 1.0f), + new UnitEntry("dip", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), + new UnitEntry("dp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_DIP, 1.0f), + new UnitEntry("sp", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_SP, 1.0f), + new UnitEntry("pt", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_PT, 1.0f), + new UnitEntry("in", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_IN, 1.0f), + new UnitEntry("mm", TypedValue.TYPE_DIMENSION, TypedValue.COMPLEX_UNIT_MM, 1.0f), + new UnitEntry("%", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION, 1.0f/100), + new UnitEntry("%p", TypedValue.TYPE_FRACTION, TypedValue.COMPLEX_UNIT_FRACTION_PARENT, 1.0f/100), + }; + + /** + * Returns the raw value from the given string. + * This object is only valid until the next call on to {@link ResourceHelper}. + */ + public static TypedValue getValue(String s) { + if (stringToFloat(s, mValue)) { + return mValue; + } + + return null; + } + + /** + * Convert the string into a {@link TypedValue}. + * @param s + * @param outValue + * @return true if success. + */ + public static boolean stringToFloat(String s, TypedValue outValue) { + // remove the space before and after + s.trim(); + int len = s.length(); + + if (len <= 0) { + return false; + } + + // check that there's no non ascii characters. + char[] buf = s.toCharArray(); + for (int i = 0 ; i < len ; i++) { + if (buf[i] > 255) { + return false; + } + } + + // check the first character + if (buf[0] < '0' && buf[0] > '9' && buf[0] != '.') { + return false; + } + + // now look for the string that is after the float... + Matcher m = sFloatPattern.matcher(s); + if (m.matches()) { + String f_str = m.group(1); + String end = m.group(2); + + float f; + try { + f = Float.parseFloat(f_str); + } catch (NumberFormatException e) { + // this shouldn't happen with the regexp above. + return false; + } + + if (end.length() > 0 && end.charAt(0) != ' ') { + // Might be a unit... + if (parseUnit(end, outValue, sFloatOut)) { + + f *= sFloatOut[0]; + boolean neg = f < 0; + if (neg) { + f = -f; + } + long bits = (long)(f*(1<<23)+.5f); + int radix; + int shift; + if ((bits&0x7fffff) == 0) { + // Always use 23p0 if there is no fraction, just to make + // things easier to read. + radix = TypedValue.COMPLEX_RADIX_23p0; + shift = 23; + } else if ((bits&0xffffffffff800000L) == 0) { + // Magnitude is zero -- can fit in 0 bits of precision. + radix = TypedValue.COMPLEX_RADIX_0p23; + shift = 0; + } else if ((bits&0xffffffff80000000L) == 0) { + // Magnitude can fit in 8 bits of precision. + radix = TypedValue.COMPLEX_RADIX_8p15; + shift = 8; + } else if ((bits&0xffffff8000000000L) == 0) { + // Magnitude can fit in 16 bits of precision. + radix = TypedValue.COMPLEX_RADIX_16p7; + shift = 16; + } else { + // Magnitude needs entire range, so no fractional part. + radix = TypedValue.COMPLEX_RADIX_23p0; + shift = 23; + } + int mantissa = (int)( + (bits>>shift) & TypedValue.COMPLEX_MANTISSA_MASK); + if (neg) { + mantissa = (-mantissa) & TypedValue.COMPLEX_MANTISSA_MASK; + } + outValue.data |= + (radix<<TypedValue.COMPLEX_RADIX_SHIFT) + | (mantissa<<TypedValue.COMPLEX_MANTISSA_SHIFT); + return true; + } + return false; + } + + // make sure it's only spaces at the end. + end = end.trim(); + + if (end.length() == 0) { + if (outValue != null) { + outValue.type = TypedValue.TYPE_FLOAT; + outValue.data = Float.floatToIntBits(f); + return true; + } + } + } + + return false; + } + + private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) { + str = str.trim(); + + for (UnitEntry unit : sUnitNames) { + if (unit.name.equals(str)) { + outValue.type = unit.type; + outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT; + outScale[0] = unit.scale; + + return true; + } + } + + return false; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/ResourceValue.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/ResourceValue.java new file mode 100644 index 0000000..01a4871 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/ResourceValue.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.layoutlib.api.IResourceValue; + +/** + * Basic implementation of IResourceValue. + */ +class ResourceValue implements IResourceValue { + private final String mType; + private final String mName; + private String mValue = null; + + ResourceValue(String name) { + mType = null; + mName = name; + } + + public ResourceValue(String type, String name, String value) { + mType = type; + mName = name; + mValue = value; + } + + public String getType() { + return mType; + } + + public final String getName() { + return mName; + } + + public final String getValue() { + return mValue; + } + + public final void setValue(String value) { + mValue = value; + } + + public void replaceWith(ResourceValue value) { + mValue = value.mValue; + } + + public boolean isFramework() { + // ResourceValue object created directly in the framework are used to describe + // non resolvable coming from the XML. Since they will never be cached (as they can't + // be a value pointing to a bitmap, or they'd be resolvable.), the return value deoes + // not matter. + return false; + } +} diff --git a/tools/layoutlib/bridge/src/com/google/android/maps/MapView.java b/tools/layoutlib/bridge/src/com/google/android/maps/MapView.java new file mode 100644 index 0000000..6d013bb --- /dev/null +++ b/tools/layoutlib/bridge/src/com/google/android/maps/MapView.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2008 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.google.android.maps; + +import com.android.layoutlib.bridge.MockView; + +import android.content.Context; +import android.os.Bundle; +import android.util.AttributeSet; +import android.view.View; + +/** + * Mock version of the MapView. + * Only non override public methods from the real MapView have been added in there. + * Methods that take an unknown class as parameter or as return object, have been removed for now. + * + * TODO: generate automatically. + * + */ +public class MapView extends MockView { + + /** + * Construct a new WebView with a Context object. + * @param context A Context object used to access application assets. + */ + public MapView(Context context) { + this(context, null); + } + + /** + * Construct a new WebView with layout parameters. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + */ + public MapView(Context context, AttributeSet attrs) { + this(context, attrs, com.android.internal.R.attr.mapViewStyle); + } + + /** + * Construct a new WebView with layout parameters and a default style. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + * @param defStyle The default style resource ID. + */ + public MapView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + // START FAKE PUBLIC METHODS + + public void displayZoomControls(boolean takeFocus) { + } + + public boolean canCoverCenter() { + return false; + } + + public void preLoad() { + } + + public int getZoomLevel() { + return 0; + } + + public void setSatellite(boolean on) { + } + + public boolean isSatellite() { + return false; + } + + public void setTraffic(boolean on) { + } + + public boolean isTraffic() { + return false; + } + + public void setStreetView(boolean on) { + } + + public boolean isStreetView() { + return false; + } + + public int getLatitudeSpan() { + return 0; + } + + public int getLongitudeSpan() { + return 0; + } + + public int getMaxZoomLevel() { + return 0; + } + + public void onSaveInstanceState(Bundle state) { + } + + public void onRestoreInstanceState(Bundle state) { + } + + public View getZoomControls() { + return null; + } +} diff --git a/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/AndroidGraphicsTests.java b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/AndroidGraphicsTests.java new file mode 100644 index 0000000..ac144e7 --- /dev/null +++ b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/AndroidGraphicsTests.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics._Original_Paint; +import android.text.TextPaint; + +import junit.framework.TestCase; + +/** + * + */ +public class AndroidGraphicsTests extends TestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testMatrix() { + Matrix m1 = new Matrix(); + + assertFalse(m1.isIdentity()); + + m1.setValues(new float[] { 1,0,0, 0,1,0, 0,0,1 }); + assertTrue(m1.isIdentity()); + + Matrix m2 = new Matrix(m1); + + float[] v1 = new float[9]; + float[] v2 = new float[9]; + m1.getValues(v1); + m2.getValues(v2); + + for (int i = 0 ; i < 9; i++) { + assertEquals(v1[i], v2[i]); + } + } + + public void testPaint() { + _Original_Paint o = new _Original_Paint(); + assertNotNull(o); + + Paint p = new Paint(); + assertNotNull(p); + } + + public void textTextPaint() { + TextPaint p = new TextPaint(); + assertNotNull(p); + } +} diff --git a/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/BridgeTest.java b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/BridgeTest.java new file mode 100644 index 0000000..e424f1d --- /dev/null +++ b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/BridgeTest.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.layoutlib.api.ILayoutResult; +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.api.IStyleResourceValue; +import com.android.layoutlib.api.IXmlPullParser; +import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo; + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; + +import java.io.File; +import java.io.FileReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +public class BridgeTest extends TestCase { + + /** the class being tested */ + private Bridge mBridge; + /** the path to the sample layout.xml file */ + private String mLayoutXml1Path; + private String mTextOnlyXmlPath; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + mBridge = new Bridge(); + + // FIXME: need some fonts somewhere. + mBridge.init(null /* fontOsLocation */, getAttributeValues()); + + URL url = this.getClass().getClassLoader().getResource("data/layout1.xml"); + mLayoutXml1Path = url.getFile(); + + url = this.getClass().getClassLoader().getResource("data/textonly.xml"); + mTextOnlyXmlPath = url.getFile(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + // --------------- + + /** + * Test parser that implements {@link IXmlPullParser}. + */ + private static class TestParser extends KXmlParser implements IXmlPullParser { + public Object getViewKey() { + return null; + } + } + + public void testComputeLayout() throws Exception { + + TestParser parser = new TestParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(new File(mLayoutXml1Path))); + + Map<String, Map<String, IResourceValue>> projectResources = getProjectResources(); + + Map<String, Map<String, IResourceValue>> frameworkResources = getFrameworkResources(); + + int screenWidth = 320; + int screenHeight = 480; + + // FIXME need a dummy font for the tests! + ILayoutResult result = mBridge.computeLayout(parser, new Integer(1) /* projectKey */, + screenWidth, screenHeight, + "Theme", projectResources, frameworkResources, null, null); + + display(result.getRootView(), ""); + } + + private Map<String, Map<String, Integer>> getAttributeValues() { + Map<String, Map<String, Integer>> attributeValues = + new HashMap<String, Map<String,Integer>>(); + + // lets create a map for the orientation attribute + Map<String, Integer> attributeMap = new HashMap<String, Integer>(); + + attributeMap.put("horizontal", Integer.valueOf(0)); + attributeMap.put("vertical", Integer.valueOf(1)); + + attributeValues.put("orientation", attributeMap); + + return attributeValues; + } + + private Map<String, Map<String, IResourceValue>> getFrameworkResources() { + Map<String, Map<String, IResourceValue>> frameworkResources = + new HashMap<String, Map<String, IResourceValue>>(); + + // create the style map + Map<String, IResourceValue> styleMap = new HashMap<String, IResourceValue>(); + frameworkResources.put("style", styleMap); + + // create a button style. + IStyleResourceValue style = createStyle("Widget.Button", + "background", "@android:drawable/something", + "focusable", "true", + "clickable", "true", + "textAppearance", "?android:attr/textAppearanceSmallInverse", + "textColor", "?android:attr/textColorBrightInverseNoDisable", + "gravity", "center_vertical|center_horizontal" + ); + styleMap.put(style.getName(), style); + + // create the parent style of button style + style = createStyle("Widget", + "textAppearance", "?textAppearance"); + styleMap.put(style.getName(), style); + + // link the buttonStyle info in the default theme. + style = createStyle("Theme", + BridgeConstants.RES_STYLE, "buttonStyle", "@android:style/Widget.Button", + BridgeConstants.RES_STYLE, "textAppearance", "@android:style/TextAppearance", + BridgeConstants.RES_STYLE, "textAppearanceSmallInverse", "@android:style/TextAppearance.Small.Inverse", + BridgeConstants.RES_COLOR, "textColorBrightInverseNoDisable", "@android:color/bright_text_light_nodisable" + ); + styleMap.put(style.getName(), style); + + // create a dummy drawable to go with it + Map<String, IResourceValue> drawableMap = new HashMap<String, IResourceValue>(); + frameworkResources.put("drawable", drawableMap); + + // get the 9 patch test location + URL url = this.getClass().getClassLoader().getResource("data/button.9.png"); + + IResourceValue drawable = new ResourceValue(BridgeConstants.RES_DRAWABLE, "something", + url.getPath()); + drawableMap.put(drawable.getName(), drawable); + return frameworkResources; + } + + private Map<String, Map<String, IResourceValue>> getProjectResources() { + Map<String, Map<String, IResourceValue>> projectResources = + new HashMap<String, Map<String, IResourceValue>>(); + + // create the style map (even empty there should be one) + Map<String, IResourceValue> styleMap = new HashMap<String, IResourceValue>(); + projectResources.put("style", styleMap); + + return projectResources; + } + + + private void display(ILayoutViewInfo result, String offset) { + + String msg = String.format("%s%s L:%d T:%d R:%d B:%d", + offset, + result.getName(), + result.getLeft(), result.getTop(), result.getRight(), result.getBottom()); + + System.out.println(msg); + ILayoutViewInfo[] children = result.getChildren(); + if (children != null) { + offset += "+-"; + for (ILayoutViewInfo child : children) { + display(child, offset); + } + } + } + + /** + * Creates a {@link IStyleResourceValue} based on the given values. + * @param styleName the name of the style. + * @param items An array of Strings. Even indices contain a style item name, and odd indices + * a style item value. If the number of string in the array is not even, an exception is thrown. + */ + private IStyleResourceValue createStyle(String styleName, String... items) { + StyleResourceValue value = new StyleResourceValue(styleName); + + if (items.length % 3 == 0) { + for (int i = 0 ; i < items.length;) { + value.addItem(new ResourceValue(items[i++], items[i++], items[i++])); + } + } else { + throw new IllegalArgumentException("Need a multiple of 3 for the number of strings"); + } + + return value; + } + + // --------------- + + public void testTextLayout() throws Exception { + + TestParser parser = new TestParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + parser.setInput(new FileReader(new File(mTextOnlyXmlPath))); + + Map<String, Map<String, IResourceValue>> projectResources = getProjectResources(); + Map<String, Map<String, IResourceValue>> frameworkResources = getFrameworkResources(); + + int screenWidth = 320; + int screenHeight = 480; + + // FIXME need a dummy font for the tests! + ILayoutResult result = mBridge.computeLayout(parser, new Integer(1) /* projectKey */, + screenWidth, screenHeight, + "Theme", projectResources, frameworkResources, null, null); + + display(result.getRootView(), ""); + } + +} diff --git a/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/BridgeXmlBlockParserTest.java b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/BridgeXmlBlockParserTest.java new file mode 100644 index 0000000..cac1f95 --- /dev/null +++ b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/BridgeXmlBlockParserTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import org.kxml2.io.KXmlParser; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.SAXException; +import org.xmlpull.v1.XmlPullParser; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.URL; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import junit.framework.TestCase; + +public class BridgeXmlBlockParserTest extends TestCase { + + private String mXmlPath; + private Document mDoc; + + @Override + protected void setUp() throws Exception { + super.setUp(); + URL url = this.getClass().getClassLoader().getResource("data/layout1.xml"); + mXmlPath = url.getFile(); + mDoc = getXmlDocument(mXmlPath); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testXmlBlockParser() throws Exception { + XmlPullParser parser = new KXmlParser(); + parser = new BridgeXmlBlockParser(parser, null, false /* platformResourceFlag */); + parser.setInput(new FileReader(new File(mXmlPath))); + + assertEquals(XmlPullParser.START_DOCUMENT, parser.next()); + + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("LinearLayout", parser.getName()); + + assertEquals(XmlPullParser.TEXT, parser.next()); + + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("Button", parser.getName()); + assertEquals(XmlPullParser.TEXT, parser.next()); + assertEquals(XmlPullParser.END_TAG, parser.next()); + + assertEquals(XmlPullParser.TEXT, parser.next()); + + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("View", parser.getName()); + assertEquals(XmlPullParser.END_TAG, parser.next()); + + assertEquals(XmlPullParser.TEXT, parser.next()); + + assertEquals(XmlPullParser.START_TAG, parser.next()); + assertEquals("TextView", parser.getName()); + assertEquals(XmlPullParser.END_TAG, parser.next()); + + assertEquals(XmlPullParser.TEXT, parser.next()); + + assertEquals(XmlPullParser.END_TAG, parser.next()); + assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); + } + + //------------ + + private Document getXmlDocument(String xmlFilePath) + throws ParserConfigurationException, SAXException, IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + + // keep comments + factory.setIgnoringComments(false); + // don't validate our bogus DTD + factory.setValidating(false); + // we want namespaces + factory.setNamespaceAware(true); + + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(new File(xmlFilePath)); + } + + + /** + * Quick'n'dirty debug helper that dumps an XML structure to stdout. + */ + @SuppressWarnings("unused") + private void dump(Node node, String prefix) { + Node n; + + String[] types = { + "unknown", + "ELEMENT_NODE", + "ATTRIBUTE_NODE", + "TEXT_NODE", + "CDATA_SECTION_NODE", + "ENTITY_REFERENCE_NODE", + "ENTITY_NODE", + "PROCESSING_INSTRUCTION_NODE", + "COMMENT_NODE", + "DOCUMENT_NODE", + "DOCUMENT_TYPE_NODE", + "DOCUMENT_FRAGMENT_NODE", + "NOTATION_NODE" + }; + + String s = String.format("%s<%s> %s %s", + prefix, + types[node.getNodeType()], + node.getNodeName(), + node.getNodeValue() == null ? "" : node.getNodeValue().trim()); + + System.out.println(s); + + n = node.getFirstChild(); + if (n != null) { + dump(n, prefix + "- "); + } + + n = node.getNextSibling(); + if (n != null) { + dump(n, prefix); + } + + } + +} diff --git a/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/NinePatchTest.java b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/NinePatchTest.java new file mode 100644 index 0000000..67ec5e1 --- /dev/null +++ b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/NinePatchTest.java @@ -0,0 +1,35 @@ +package com.android.layoutlib.bridge; + +import com.android.ninepatch.NinePatch; + +import java.net.URL; + +import junit.framework.TestCase; + +public class NinePatchTest extends TestCase { + + private NinePatch mPatch; + + @Override + protected void setUp() throws Exception { + URL url = this.getClass().getClassLoader().getResource("data/button.9.png"); + + mPatch = NinePatch.load(url, false /* convert */); + } + + public void test9PatchLoad() throws Exception { + assertNotNull(mPatch); + } + + public void test9PatchMinSize() { + int[] padding = new int[4]; + mPatch.getPadding(padding); + assertEquals(13, padding[0]); + assertEquals(3, padding[1]); + assertEquals(13, padding[2]); + assertEquals(4, padding[3]); + assertEquals(38, mPatch.getWidth()); + assertEquals(27, mPatch.getHeight()); + } + +} diff --git a/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/StyleResourceValue.java b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/StyleResourceValue.java new file mode 100644 index 0000000..84bdc2f --- /dev/null +++ b/tools/layoutlib/bridge/tests/com/android/layoutlib/bridge/StyleResourceValue.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2008 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.layoutlib.bridge; + +import com.android.layoutlib.api.IResourceValue; +import com.android.layoutlib.api.IStyleResourceValue; + +import java.util.HashMap; + +class StyleResourceValue extends ResourceValue implements IStyleResourceValue { + + private String mParentStyle = null; + private HashMap<String, IResourceValue> mItems = new HashMap<String, IResourceValue>(); + + StyleResourceValue(String name) { + super(name); + } + + StyleResourceValue(String name, String parentStyle) { + super(name); + mParentStyle = parentStyle; + } + + public String getParentStyle() { + return mParentStyle; + } + + public IResourceValue findItem(String name) { + return mItems.get(name); + } + + public void addItem(IResourceValue value) { + mItems.put(value.getName(), value); + } + + @Override + public void replaceWith(ResourceValue value) { + super.replaceWith(value); + + if (value instanceof StyleResourceValue) { + mItems.clear(); + mItems.putAll(((StyleResourceValue)value).mItems); + } + } + +} diff --git a/tools/layoutlib/bridge/tests/data/button.9.png b/tools/layoutlib/bridge/tests/data/button.9.png Binary files differnew file mode 100644 index 0000000..9d52f40 --- /dev/null +++ b/tools/layoutlib/bridge/tests/data/button.9.png diff --git a/tools/layoutlib/bridge/tests/data/layout1.xml b/tools/layoutlib/bridge/tests/data/layout1.xml new file mode 100644 index 0000000..554f541 --- /dev/null +++ b/tools/layoutlib/bridge/tests/data/layout1.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + Copyright (C) 2008 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical" +> + <Button + android:id="@+id/bouton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="My Button Text" + > + </Button> + <View + android:id="@+id/surface" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:layout_weight="2" + /> + <TextView + android:id="@+id/status" + android:paddingLeft="2dip" + android:layout_weight="0" + android:background="@drawable/black" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:lines="1" + android:gravity="center_vertical|center_horizontal" + android:text="My TextView Text" + /> +</LinearLayout> diff --git a/tools/layoutlib/create/.classpath b/tools/layoutlib/create/.classpath new file mode 100644 index 0000000..0c60f6a --- /dev/null +++ b/tools/layoutlib/create/.classpath @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry excluding="mock_android/" kind="src" path="tests"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/> + <classpathentry kind="var" path="ANDROID_SRC/prebuilt/common/asm/asm-3.1.jar"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/tools/layoutlib/create/.project b/tools/layoutlib/create/.project new file mode 100644 index 0000000..e100d17 --- /dev/null +++ b/tools/layoutlib/create/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>layoutlib_create</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/tools/layoutlib/create/Android.mk b/tools/layoutlib/create/Android.mk new file mode 100644 index 0000000..310fae5 --- /dev/null +++ b/tools/layoutlib/create/Android.mk @@ -0,0 +1,28 @@ +# +# Copyright (C) 2008 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. +# +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-java-files-under,src) + +LOCAL_JAR_MANIFEST := manifest.txt +LOCAL_STATIC_JAVA_LIBRARIES := \ + asm-3.1 + +LOCAL_MODULE := layoutlib_create + +include $(BUILD_HOST_JAVA_LIBRARY) + diff --git a/tools/layoutlib/create/README.txt b/tools/layoutlib/create/README.txt new file mode 100644 index 0000000..09b392b --- /dev/null +++ b/tools/layoutlib/create/README.txt @@ -0,0 +1,71 @@ +# Copyright (C) 2008 The Android Open Source Project + + +- Description - +--------------- + +makeLayoutLib generates a library used by the Eclipse graphical layout editor +to perform layout. + + + +- Usage - +--------- + + ./makeLayoutLib path/to/android.jar destination.jar + + + +- Implementation Notes - +------------------------ + +The goal of makeLayoutLib is to list all the classes from the input jar and create a new +jar that only keeps certain classes and create stubs for all their dependencies. + +First the input jar is parsed to find all the classes defined. + +In the Main(), the following list of classes are hardcoded (TODO config file later): +- keep all classes that derive from android.view.View. +- keep all classes in the android.view and android.widget packages (sub-packages excluded). +- keep specific classes such as android.policy.PhoneLayoutInflater. + +For each class to keep, their dependencies are examined using BCEL. +A dependency is defined as a class needed to instantiate the given class that should be kept, +directly or indirectly. So a dependency is a class that is used by the input class, that is +defined in the input jar and that is not part of the current JRE. + +Dependencies are computed recursively. + +Once all dependencies are found, the final jar can be created. +There are three kind of classes to write: +- classes that are to be kept as-is. They are just dumped in the new jar unchanged. +- classes that are to be kept yet contain native methods or fields. +- classes that are just dependencies. We don't want to expose their implementation in the final + jar. + +The implementation of native methods and all methods of mock classes is replaced by a stub +that throws UnsupportedOperationException. + +Incidentally, the access level of native and mock classes needs to be changed in order for +native methods to be later overridden. Methods that are "final private native" must become +non-final, non-native and at most protected. Package-default access is changed to public. +Classes that are final are made non-final. Abstract methods are left untouched. + + + +---- +20080617 Replace Class + +Some classes are basically wrappers over native objects. +Subclassing doesn't work as most methods are either static or we don't +control object creation. In this scenario the idea is to be able to +replace classes in the final jar. + +Example: android.graphics.Paint would get renamed to OriginalPaint +in the generated jar. Then in the bridge we'll introduce a replacement +Paint class that derives from OriginalPaint. + +This won't rename/replace the inner static methods of a given class. + + + diff --git a/tools/layoutlib/create/manifest.txt b/tools/layoutlib/create/manifest.txt new file mode 100644 index 0000000..238e7f9 --- /dev/null +++ b/tools/layoutlib/create/manifest.txt @@ -0,0 +1 @@ +Main-Class: com.android.tools.layoutlib.create.Main diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java new file mode 100644 index 0000000..b197ea7 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java @@ -0,0 +1,751 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.Map.Entry; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Analyzes the input JAR using the ASM java bytecode manipulation library + * to list the desired classes and their dependencies. + */ +public class AsmAnalyzer { + + // Note: a bunch of stuff has package-level access for unit tests. Consider it private. + + /** Output logger. */ + private final Log mLog; + /** The input source JAR to parse. */ + private final List<String> mOsSourceJar; + /** The generator to fill with the class list and dependency list. */ + private final AsmGenerator mGen; + /** Keep all classes that derive from these one (these included). */ + private final String[] mDeriveFrom; + /** Glob patterns of classes to keep, e.g. "com.foo.*" */ + private final String[] mIncludeGlobs; + + /** + * Creates a new analyzer. + * + * @param log The log output. + * @param osJarPath The input source JARs to parse. + * @param gen The generator to fill with the class list and dependency list. + * @param deriveFrom Keep all classes that derive from these one (these included). + * @param includeGlobs Glob patterns of classes to keep, e.g. "com.foo.*" + * ("*" does not matches dots whilst "**" does, "." and "$" are interpreted as-is) + */ + public AsmAnalyzer(Log log, List<String> osJarPath, AsmGenerator gen, + String[] deriveFrom, String[] includeGlobs) { + mLog = log; + mGen = gen; + mOsSourceJar = osJarPath != null ? osJarPath : new ArrayList<String>(); + mDeriveFrom = deriveFrom != null ? deriveFrom : new String[0]; + mIncludeGlobs = includeGlobs != null ? includeGlobs : new String[0]; + } + + /** + * Starts the analysis using parameters from the constructor. + * Fills the generator with classes & dependencies found. + */ + public void analyze() throws IOException, LogAbortException { + + AsmAnalyzer visitor = this; + + Map<String, ClassReader> zipClasses = parseZip(mOsSourceJar); + mLog.info("Found %d classes in input JAR%s.", zipClasses.size(), + mOsSourceJar.size() > 1 ? "s" : ""); + + Map<String, ClassReader> found = findIncludes(zipClasses); + Map<String, ClassReader> deps = findDeps(zipClasses, found); + + if (mGen != null) { + mGen.setKeep(found); + mGen.setDeps(deps); + } + } + + /** + * Parses a JAR file and returns a list of all classes founds using a map + * class name => ASM ClassReader. Class names are in the form "android.view.View". + */ + Map<String,ClassReader> parseZip(List<String> jarPathList) throws IOException { + TreeMap<String, ClassReader> classes = new TreeMap<String, ClassReader>(); + + for (String jarPath : jarPathList) { + ZipFile zip = new ZipFile(jarPath); + Enumeration<? extends ZipEntry> entries = zip.entries(); + ZipEntry entry; + while (entries.hasMoreElements()) { + entry = entries.nextElement(); + if (entry.getName().endsWith(".class")) { + ClassReader cr = new ClassReader(zip.getInputStream(entry)); + String className = classReaderToClassName(cr); + classes.put(className, cr); + } + } + } + + return classes; + } + + /** + * Utility that returns the fully qualified binary class name for a ClassReader. + * E.g. it returns something like android.view.View. + */ + static String classReaderToClassName(ClassReader classReader) { + if (classReader == null) { + return null; + } else { + return classReader.getClassName().replace('/', '.'); + } + } + + /** + * Utility that returns the fully qualified binary class name from a path-like FQCN. + * E.g. it returns android.view.View from android/view/View. + */ + static String internalToBinaryClassName(String className) { + if (className == null) { + return null; + } else { + return className.replace('/', '.'); + } + } + + /** + * Process the "includes" arrays. + * <p/> + * This updates the in_out_found map. + */ + Map<String, ClassReader> findIncludes(Map<String, ClassReader> zipClasses) + throws LogAbortException { + TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + + mLog.debug("Find classes to include."); + + for (String s : mIncludeGlobs) { + findGlobs(s, zipClasses, found); + } + for (String s : mDeriveFrom) { + findClassesDerivingFrom(s, zipClasses, found); + } + + return found; + } + + + /** + * Uses ASM to find the class reader for the given FQCN class name. + * If found, insert it in the in_out_found map. + * Returns the class reader object. + */ + ClassReader findClass(String className, Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inOutFound) throws LogAbortException { + ClassReader classReader = zipClasses.get(className); + if (classReader == null) { + throw new LogAbortException("Class %s not found by ASM in %s", + className, mOsSourceJar); + } + + inOutFound.put(className, classReader); + return classReader; + } + + /** + * Insert in the inOutFound map all classes found in zipClasses that match the + * given glob pattern. + * <p/> + * The glob pattern is not a regexp. It only accepts the "*" keyword to mean + * "anything but a period". The "." and "$" characters match themselves. + * The "**" keyword means everything including ".". + * <p/> + * Examples: + * <ul> + * <li>com.foo.* matches all classes in the package com.foo but NOT sub-packages. + * <li>com.foo*.*$Event matches all internal Event classes in a com.foo*.* class. + * </ul> + */ + void findGlobs(String globPattern, Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inOutFound) throws LogAbortException { + // transforms the glob pattern in a regexp: + // - escape "." with "\." + // - replace "*" by "[^.]*" + // - escape "$" with "\$" + // - add end-of-line match $ + globPattern = globPattern.replaceAll("\\$", "\\\\\\$"); + globPattern = globPattern.replaceAll("\\.", "\\\\."); + // prevent ** from being altered by the next rule, then process the * rule and finally + // the real ** rule (which is now @) + globPattern = globPattern.replaceAll("\\*\\*", "@"); + globPattern = globPattern.replaceAll("\\*", "[^.]*"); + globPattern = globPattern.replaceAll("@", ".*"); + globPattern += "$"; + + Pattern regexp = Pattern.compile(globPattern); + + for (Entry<String, ClassReader> entry : zipClasses.entrySet()) { + String class_name = entry.getKey(); + if (regexp.matcher(class_name).matches()) { + findClass(class_name, zipClasses, inOutFound); + } + } + } + + /** + * Checks all the classes defined in the JarClassName instance and uses BCEL to + * determine if they are derived from the given FQCN super class name. + * Inserts the super class and all the class objects found in the map. + */ + void findClassesDerivingFrom(String super_name, Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inOutFound) throws LogAbortException { + ClassReader super_clazz = findClass(super_name, zipClasses, inOutFound); + + for (Entry<String, ClassReader> entry : zipClasses.entrySet()) { + String className = entry.getKey(); + if (super_name.equals(className)) { + continue; + } + ClassReader classReader = entry.getValue(); + ClassReader parent_cr = classReader; + while (parent_cr != null) { + String parent_name = internalToBinaryClassName(parent_cr.getSuperName()); + if (parent_name == null) { + // not found + break; + } else if (super_name.equals(parent_name)) { + inOutFound.put(className, classReader); + break; + } + parent_cr = zipClasses.get(parent_name); + } + } + } + + /** + * Instantiates a new DependencyVisitor. Useful for unit tests. + */ + DependencyVisitor getVisitor(Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inKeep, + Map<String, ClassReader> outKeep, + Map<String, ClassReader> inDeps, + Map<String, ClassReader> outDeps) { + return new DependencyVisitor(zipClasses, inKeep, outKeep, inDeps, outDeps); + } + + /** + * Finds all dependencies for all classes in keepClasses which are also + * listed in zipClasses. Returns a map of all the dependencies found. + */ + Map<String, ClassReader> findDeps(Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inOutKeepClasses) { + + TreeMap<String, ClassReader> deps = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> new_deps = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> new_keep = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> temp = new TreeMap<String, ClassReader>(); + + DependencyVisitor visitor = getVisitor(zipClasses, + inOutKeepClasses, new_keep, + deps, new_deps); + + for (ClassReader cr : inOutKeepClasses.values()) { + cr.accept(visitor, 0 /* flags */); + } + + while (new_deps.size() > 0 || new_keep.size() > 0) { + deps.putAll(new_deps); + inOutKeepClasses.putAll(new_keep); + + temp.clear(); + temp.putAll(new_deps); + temp.putAll(new_keep); + new_deps.clear(); + new_keep.clear(); + mLog.debug("Found %1$d to keep, %2$d dependencies.", + inOutKeepClasses.size(), deps.size()); + + for (ClassReader cr : temp.values()) { + cr.accept(visitor, 0 /* flags */); + } + } + + mLog.info("Found %1$d classes to keep, %2$d class dependencies.", + inOutKeepClasses.size(), deps.size()); + + return deps; + } + + + + // ---------------------------------- + + /** + * Visitor to collect all the type dependencies from a class. + */ + public class DependencyVisitor + implements ClassVisitor, FieldVisitor, MethodVisitor, SignatureVisitor, AnnotationVisitor { + + /** All classes found in the source JAR. */ + private final Map<String, ClassReader> mZipClasses; + /** Classes from which dependencies are to be found. */ + private final Map<String, ClassReader> mInKeep; + /** Dependencies already known. */ + private final Map<String, ClassReader> mInDeps; + /** New dependencies found by this visitor. */ + private final Map<String, ClassReader> mOutDeps; + /** New classes to keep as-is found by this visitor. */ + private final Map<String, ClassReader> mOutKeep; + + /** + * Creates a new visitor that will find all the dependencies for the visited class. + * Types which are already in the zipClasses, keepClasses or inDeps are not marked. + * New dependencies are marked in outDeps. + * + * @param zipClasses All classes found in the source JAR. + * @param inKeep Classes from which dependencies are to be found. + * @param inDeps Dependencies already known. + * @param outDeps New dependencies found by this visitor. + */ + public DependencyVisitor(Map<String, ClassReader> zipClasses, + Map<String, ClassReader> inKeep, + Map<String, ClassReader> outKeep, + Map<String,ClassReader> inDeps, + Map<String,ClassReader> outDeps) { + mZipClasses = zipClasses; + mInKeep = inKeep; + mOutKeep = outKeep; + mInDeps = inDeps; + mOutDeps = outDeps; + } + + /** + * Considers the given class name as a dependency. + * If it does, add to the mOutDeps map. + */ + public void considerName(String className) { + if (className == null) { + return; + } + + className = internalToBinaryClassName(className); + + // exclude classes that have already been found + if (mInKeep.containsKey(className) || + mOutKeep.containsKey(className) || + mInDeps.containsKey(className) || + mOutDeps.containsKey(className)) { + return; + } + + // exclude classes that are not part of the JAR file being examined + ClassReader cr = mZipClasses.get(className); + if (cr == null) { + return; + } + + try { + // exclude classes that are part of the default JRE (the one executing this program) + if (getClass().getClassLoader().loadClass(className) != null) { + return; + } + } catch (ClassNotFoundException e) { + // ignore + } + + // accept this class: + // - android classes are added to dependencies + // - non-android classes are added to the list of classes to keep as-is (they don't need + // to be stubbed). + if (className.indexOf("android") >= 0) { // TODO make configurable + mOutDeps.put(className, cr); + } else { + mOutKeep.put(className, cr); + } + } + + /** + * Considers this array of names using considerName(). + */ + public void considerNames(String[] classNames) { + if (classNames != null) { + for (String className : classNames) { + considerName(className); + } + } + } + + /** + * Considers this signature or type signature by invoking the {@link SignatureVisitor} + * on it. + */ + public void considerSignature(String signature) { + if (signature != null) { + SignatureReader sr = new SignatureReader(signature); + // SignatureReader.accept will call accessType so we don't really have + // to differentiate where the signature comes from. + sr.accept(this); + } + } + + /** + * Considers this {@link Type}. For arrays, the element type is considered. + * If the type is an object, it's internal name is considered. + */ + public void considerType(Type t) { + if (t != null) { + if (t.getSort() == Type.ARRAY) { + t = t.getElementType(); + } + if (t.getSort() == Type.OBJECT) { + considerName(t.getInternalName()); + } + } + } + + /** + * Considers a descriptor string. The descriptor is converted to a {@link Type} + * and then considerType() is invoked. + */ + public void considerDesc(String desc) { + if (desc != null) { + try { + Type t = Type.getType(desc); + considerType(t); + } catch (ArrayIndexOutOfBoundsException e) { + // ignore, not a valid type. + } + } + } + + + // --------------------------------------------------- + // --- ClassVisitor, FieldVisitor + // --------------------------------------------------- + + // Visits a class header + public void visit(int version, int access, String name, + String signature, String superName, String[] interfaces) { + // signature is the signature of this class. May be null if the class is not a generic + // one, and does not extend or implement generic classes or interfaces. + + if (signature != null) { + considerSignature(signature); + } + + // superName is the internal of name of the super class (see getInternalName). + // For interfaces, the super class is Object. May be null but only for the Object class. + considerName(superName); + + // interfaces is the internal names of the class's interfaces (see getInternalName). + // May be null. + considerNames(interfaces); + } + + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return this; // return this to visit annotion values + } + + public void visitAttribute(Attribute attr) { + // pass + } + + // Visits the end of a class + public void visitEnd() { + // pass + } + + public FieldVisitor visitField(int access, String name, String desc, + String signature, Object value) { + // desc is the field's descriptor (see Type). + considerDesc(desc); + + // signature is the field's signature. May be null if the field's type does not use + // generic types. + considerSignature(signature); + + return this; // a visitor to visit field annotations and attributes + } + + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // name is the internal name of an inner class (see getInternalName). + considerName(name); + } + + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + // desc is the method's descriptor (see Type). + considerDesc(desc); + // signature is the method's signature. May be null if the method parameters, return + // type and exceptions do not use generic types. + considerSignature(signature); + + return this; // returns this to visit the method + } + + public void visitOuterClass(String owner, String name, String desc) { + // pass + } + + public void visitSource(String source, String debug) { + // pass + } + + + // --------------------------------------------------- + // --- MethodVisitor + // --------------------------------------------------- + + public AnnotationVisitor visitAnnotationDefault() { + return this; // returns this to visit the default value + } + + + public void visitCode() { + // pass + } + + // field instruction + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + // name is the field's name. + considerName(name); + // desc is the field's descriptor (see Type). + considerDesc(desc); + } + + public void visitFrame(int type, int local, Object[] local2, int stack, Object[] stack2) { + // pass + } + + public void visitIincInsn(int var, int increment) { + // pass -- an IINC instruction + } + + public void visitInsn(int opcode) { + // pass -- a zero operand instruction + } + + public void visitIntInsn(int opcode, int operand) { + // pass -- a single int operand instruction + } + + public void visitJumpInsn(int opcode, Label label) { + // pass -- a jump instruction + } + + public void visitLabel(Label label) { + // pass -- a label target + } + + // instruction to load a constant from the stack + public void visitLdcInsn(Object cst) { + if (cst instanceof Type) { + considerType((Type) cst); + } + } + + public void visitLineNumber(int line, Label start) { + // pass + } + + public void visitLocalVariable(String name, String desc, + String signature, Label start, Label end, int index) { + // desc is the type descriptor of this local variable. + considerDesc(desc); + // signature is the type signature of this local variable. May be null if the local + // variable type does not use generic types. + considerSignature(signature); + } + + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + // pass -- a lookup switch instruction + } + + public void visitMaxs(int maxStack, int maxLocals) { + // pass + } + + // instruction that invokes a method + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + + // owner is the internal name of the method's owner class + considerName(owner); + // desc is the method's descriptor (see Type). + considerDesc(desc); + } + + // instruction multianewarray, whatever that is + public void visitMultiANewArrayInsn(String desc, int dims) { + + // desc an array type descriptor. + considerDesc(desc); + } + + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, + boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return this; // return this to visit annotation values + } + + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + // pass -- table switch instruction + + } + + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + // type is the internal name of the type of exceptions handled by the handler, + // or null to catch any exceptions (for "finally" blocks). + considerName(type); + } + + // type instruction + public void visitTypeInsn(int opcode, String type) { + // type is the operand of the instruction to be visited. This operand must be the + // internal name of an object or array class. + considerName(type); + } + + public void visitVarInsn(int opcode, int var) { + // pass -- local variable instruction + } + + + // --------------------------------------------------- + // --- SignatureVisitor + // --------------------------------------------------- + + private String mCurrentSignatureClass = null; + + // Starts the visit of a signature corresponding to a class or interface type + public void visitClassType(String name) { + mCurrentSignatureClass = name; + considerName(name); + } + + // Visits an inner class + public void visitInnerClassType(String name) { + if (mCurrentSignatureClass != null) { + mCurrentSignatureClass += "$" + name; + considerName(mCurrentSignatureClass); + } + } + + public SignatureVisitor visitArrayType() { + return this; // returns this to visit the signature of the array element type + } + + public void visitBaseType(char descriptor) { + // pass -- a primitive type, ignored + } + + public SignatureVisitor visitClassBound() { + return this; // returns this to visit the signature of the class bound + } + + public SignatureVisitor visitExceptionType() { + return this; // return this to visit the signature of the exception type. + } + + public void visitFormalTypeParameter(String name) { + // pass + } + + public SignatureVisitor visitInterface() { + return this; // returns this to visit the signature of the interface type + } + + public SignatureVisitor visitInterfaceBound() { + return this; // returns this to visit the signature of the interface bound + } + + public SignatureVisitor visitParameterType() { + return this; // returns this to visit the signature of the parameter type + } + + public SignatureVisitor visitReturnType() { + return this; // returns this to visit the signature of the return type + } + + public SignatureVisitor visitSuperclass() { + return this; // returns this to visit the signature of the super class type + } + + public SignatureVisitor visitTypeArgument(char wildcard) { + return this; // returns this to visit the signature of the type argument + } + + public void visitTypeVariable(String name) { + // pass + } + + public void visitTypeArgument() { + // pass + } + + + // --------------------------------------------------- + // --- AnnotationVisitor + // --------------------------------------------------- + + + // Visits a primitive value of an annotation + public void visit(String name, Object value) { + // value is the actual value, whose type must be Byte, Boolean, Character, Short, + // Integer, Long, Float, Double, String or Type + if (value instanceof Type) { + considerType((Type) value); + } + } + + public AnnotationVisitor visitAnnotation(String name, String desc) { + // desc is the class descriptor of the nested annotation class. + considerDesc(desc); + return this; // returns this to visit the actual nested annotation value + } + + public AnnotationVisitor visitArray(String name) { + return this; // returns this to visit the actual array value elements + } + + public void visitEnum(String name, String desc, String value) { + // desc is the class descriptor of the enumeration class. + considerDesc(desc); + } + + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java new file mode 100644 index 0000000..1adcc17 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.Map.Entry; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +/** + * Class that generates a new JAR from a list of classes, some of which are to be kept as-is + * and some of which are to be stubbed partially or totally. + */ +public class AsmGenerator { + + /** Output logger. */ + private final Log mLog; + /** The path of the destination JAR to create. */ + private final String mOsDestJar; + /** List of classes to inject in the final JAR from _this_ archive. */ + private final Class<?>[] mInjectClasses; + /** The set of methods to stub out. */ + private final Set<String> mStubMethods; + /** All classes to output as-is, except if they have native methods. */ + private Map<String, ClassReader> mKeep; + /** All dependencies that must be completely stubbed. */ + private Map<String, ClassReader> mDeps; + /** Counter of number of classes renamed during transform. */ + private int mRenameCount; + /** FQCN Names of the classes to rename: map old-FQCN => new-FQCN */ + private final HashMap<String, String> mRenameClasses; + /** FQCN Names of "old" classes that were NOT renamed. This starts with the full list of + * old-FQCN to rename and they get erased as they get renamed. At the end, classes still + * left here are not in the code base anymore and thus were not renamed. */ + private HashSet<String> mClassesNotRenamed; + /** A map { FQCN => map { list of return types to delete from the FQCN } }. */ + private HashMap<String, Set<String>> mDeleteReturns; + + /** + * Creates a new generator that can generate the output JAR with the stubbed classes. + * + * @param log Output logger. + * @param osDestJar The path of the destination JAR to create. + * @param stubMethods The list of methods to stub out. Each entry must be in the form + * "package.package.OuterClass$InnerClass#MethodName". + * @param renameClasses The list of classes to rename, must be an even list: the binary FQCN + * of class to replace followed by the new FQCN. + * @param deleteReturns List of classes for which the methods returning them should be deleted. + * The array contains a list of null terminated section starting with the name of the class + * to rename in which the methods are deleted, followed by a list of return types identifying + * the methods to delete. + */ + public AsmGenerator(Log log, String osDestJar, + Class<?>[] injectClasses, + String[] stubMethods, + String[] renameClasses, String[] deleteReturns) { + mLog = log; + mOsDestJar = osDestJar; + mInjectClasses = injectClasses != null ? injectClasses : new Class<?>[0]; + mStubMethods = stubMethods != null ? new HashSet<String>(Arrays.asList(stubMethods)) : + new HashSet<String>(); + + // Create the map of classes to rename. + mRenameClasses = new HashMap<String, String>(); + mClassesNotRenamed = new HashSet<String>(); + int n = renameClasses == null ? 0 : renameClasses.length; + for (int i = 0; i < n; i += 2) { + assert i + 1 < n; + // The ASM class names uses "/" separators, whereas regular FQCN use "." + String oldFqcn = binaryToInternalClassName(renameClasses[i]); + String newFqcn = binaryToInternalClassName(renameClasses[i + 1]); + mRenameClasses.put(oldFqcn, newFqcn); + mClassesNotRenamed.add(oldFqcn); + } + + // create the map of renamed class -> return type of method to delete. + mDeleteReturns = new HashMap<String, Set<String>>(); + if (deleteReturns != null) { + Set<String> returnTypes = null; + String renamedClass = null; + for (String className : deleteReturns) { + // if we reach the end of a section, add it to the main map + if (className == null) { + if (returnTypes != null) { + mDeleteReturns.put(renamedClass, returnTypes); + } + + renamedClass = null; + continue; + } + + // if the renamed class is null, this is the beginning of a section + if (renamedClass == null) { + renamedClass = binaryToInternalClassName(className); + continue; + } + + // just a standard return type, we add it to the list. + if (returnTypes == null) { + returnTypes = new HashSet<String>(); + } + returnTypes.add(binaryToInternalClassName(className)); + } + } + } + + /** + * Returns the list of classes that have not been renamed yet. + * <p/> + * The names are "internal class names" rather than FQCN, i.e. they use "/" instead "." + * as package separators. + */ + public Set<String> getClassesNotRenamed() { + return mClassesNotRenamed; + } + + /** + * Utility that returns the internal ASM class name from a fully qualified binary class + * name. E.g. it returns android/view/View from android.view.View. + */ + String binaryToInternalClassName(String className) { + if (className == null) { + return null; + } else { + return className.replace('.', '/'); + } + } + + /** Sets the map of classes to output as-is, except if they have native methods */ + public void setKeep(Map<String, ClassReader> keep) { + mKeep = keep; + } + + /** Sets the map of dependencies that must be completely stubbed */ + public void setDeps(Map<String, ClassReader> deps) { + mDeps = deps; + } + + /** Gets the map of classes to output as-is, except if they have native methods */ + public Map<String, ClassReader> getKeep() { + return mKeep; + } + + /** Gets the map of dependencies that must be completely stubbed */ + public Map<String, ClassReader> getDeps() { + return mDeps; + } + + /** Generates the final JAR */ + public void generate() throws FileNotFoundException, IOException { + TreeMap<String, byte[]> all = new TreeMap<String, byte[]>(); + + for (Class<?> clazz : mInjectClasses) { + String name = classToEntryPath(clazz); + InputStream is = ClassLoader.getSystemResourceAsStream(name); + ClassReader cr = new ClassReader(is); + byte[] b = transform(cr, true /* stubNativesOnly */); + name = classNameToEntryPath(transformName(cr.getClassName())); + all.put(name, b); + } + + for (Entry<String, ClassReader> entry : mDeps.entrySet()) { + ClassReader cr = entry.getValue(); + byte[] b = transform(cr, true /* stubNativesOnly */); + String name = classNameToEntryPath(transformName(cr.getClassName())); + all.put(name, b); + } + + for (Entry<String, ClassReader> entry : mKeep.entrySet()) { + ClassReader cr = entry.getValue(); + byte[] b = transform(cr, true /* stubNativesOnly */); + String name = classNameToEntryPath(transformName(cr.getClassName())); + all.put(name, b); + } + + mLog.info("# deps classes: %d", mDeps.size()); + mLog.info("# keep classes: %d", mKeep.size()); + mLog.info("# renamed : %d", mRenameCount); + + createJar(new FileOutputStream(mOsDestJar), all); + mLog.info("Created JAR file %s", mOsDestJar); + } + + /** + * Writes the JAR file. + * + * @param outStream The file output stream were to write the JAR. + * @param all The map of all classes to output. + * @throws IOException if an I/O error has occurred + */ + void createJar(FileOutputStream outStream, Map<String,byte[]> all) throws IOException { + JarOutputStream jar = new JarOutputStream(outStream); + for (Entry<String, byte[]> entry : all.entrySet()) { + String name = entry.getKey(); + JarEntry jar_entry = new JarEntry(name); + jar.putNextEntry(jar_entry); + jar.write(entry.getValue()); + jar.closeEntry(); + } + jar.flush(); + jar.close(); + } + + /** + * Utility method that converts a fully qualified java name into a JAR entry path + * e.g. for the input "android.view.View" it returns "android/view/View.class" + */ + String classNameToEntryPath(String className) { + return className.replaceAll("\\.", "/").concat(".class"); + } + + /** + * Utility method to get the JAR entry path from a Class name. + * e.g. it returns someting like "com/foo/OuterClass$InnerClass1$InnerClass2.class" + */ + private String classToEntryPath(Class<?> clazz) { + String name = ""; + Class<?> parent; + while ((parent = clazz.getEnclosingClass()) != null) { + name = "$" + clazz.getSimpleName() + name; + clazz = parent; + } + return classNameToEntryPath(clazz.getCanonicalName() + name); + } + + /** + * Transforms a class. + * <p/> + * There are 3 kind of transformations: + * + * 1- For "mock" dependencies classes, we want to remove all code from methods and replace + * by a stub. Native methods must be implemented with this stub too. Abstract methods are + * left intact. Modified classes must be overridable (non-private, non-final). + * Native methods must be made non-final, non-private. + * + * 2- For "keep" classes, we want to rewrite all native methods as indicated above. + * If a class has native methods, it must also be made non-private, non-final. + * + * Note that unfortunately static methods cannot be changed to non-static (since static and + * non-static are invoked differently.) + */ + byte[] transform(ClassReader cr, boolean stubNativesOnly) { + + boolean hasNativeMethods = hasNativeMethods(cr); + String className = cr.getClassName(); + + String newName = transformName(className); + // transformName returns its input argument if there's no need to rename the class + if (newName != className) { + mRenameCount++; + // This class is being renamed, so remove it from the list of classes not renamed. + mClassesNotRenamed.remove(className); + } + + mLog.debug("Transform %s%s%s%s", className, + newName == className ? "" : " (renamed to " + newName + ")", + hasNativeMethods ? " -- has natives" : "", + stubNativesOnly ? " -- stub natives only" : ""); + + // Rewrite the new class from scratch, without reusing the constant pool from the + // original class reader. + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); + + ClassVisitor rv = cw; + if (newName != className) { + rv = new RenameClassAdapter(cw, className, newName); + } + + TransformClassAdapter cv = new TransformClassAdapter(mLog, mStubMethods, + mDeleteReturns.get(className), + newName, rv, + stubNativesOnly, stubNativesOnly || hasNativeMethods); + cr.accept(cv, 0 /* flags */); + return cw.toByteArray(); + } + + /** + * Should this class be renamed, this returns the new name. Otherwise it returns the + * original name. + * + * @param className The internal ASM name of the class that may have to be renamed + * @return A new transformed name or the original input argument. + */ + String transformName(String className) { + String newName = mRenameClasses.get(className); + if (newName != null) { + return newName; + } + int pos = className.indexOf('$'); + if (pos > 0) { + // Is this an inner class of a renamed class? + String base = className.substring(0, pos); + newName = mRenameClasses.get(base); + if (newName != null) { + return newName + className.substring(pos); + } + } + + return className; + } + + /** + * Returns true if a class has any native methods. + */ + boolean hasNativeMethods(ClassReader cr) { + ClassHasNativeVisitor cv = new ClassHasNativeVisitor(); + cr.accept(cv, 0 /* flags */); + return cv.hasNativeMethods(); + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java new file mode 100644 index 0000000..5424efa --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +/** + * Indicates if a class contains any native methods. + */ +public class ClassHasNativeVisitor implements ClassVisitor { + + private boolean mHasNativeMethods = false; + + public boolean hasNativeMethods() { + return mHasNativeMethods; + } + + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + // pass + } + + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // pass + return null; + } + + public void visitAttribute(Attribute attr) { + // pass + } + + public void visitEnd() { + // pass + } + + public FieldVisitor visitField(int access, String name, String desc, + String signature, Object value) { + // pass + return null; + } + + public void visitInnerClass(String name, String outerName, + String innerName, int access) { + // pass + } + + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + mHasNativeMethods |= ((access & Opcodes.ACC_NATIVE) != 0); + return null; + } + + public void visitOuterClass(String owner, String name, String desc) { + // pass + } + + public void visitSource(String source, String debug) { + // pass + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Log.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Log.java new file mode 100644 index 0000000..8efd871 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Log.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import java.io.PrintWriter; +import java.io.StringWriter; + +public class Log { + + private boolean mVerbose = false; + + public void setVerbose(boolean verbose) { + mVerbose = verbose; + } + + public void debug(String format, Object... args) { + if (mVerbose) { + info(format, args); + } + } + + public void info(String format, Object... args) { + String s = String.format(format, args); + outPrintln(s); + } + + public void error(String format, Object... args) { + String s = String.format(format, args); + errPrintln(s); + } + + public void exception(Throwable t, String format, Object... args) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + pw.flush(); + error(format + "\n" + sw.toString(), args); + } + + /** for unit testing */ + protected void errPrintln(String msg) { + System.err.println(msg); + } + + /** for unit testing */ + protected void outPrintln(String msg) { + System.out.println(msg); + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/LogAbortException.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/LogAbortException.java new file mode 100644 index 0000000..dc4b4a7 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/LogAbortException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +public class LogAbortException extends Exception { + + private final String mFormat; + private final Object[] mArgs; + + public LogAbortException(String format, Object... args) { + mFormat = format; + mArgs = args; + } + + public void error(Log log) { + log.error(mFormat, mArgs); + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java new file mode 100644 index 0000000..76bd8d4 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Set; + + + +public class Main { + + public static void main(String[] args) { + + Log log = new Log(); + + ArrayList<String> osJarPath = new ArrayList<String>(); + String[] osDestJar = { null }; + + if (!processArgs(log, args, osJarPath, osDestJar)) { + log.error("Usage: layoutlib_create [-v] output.jar input.jar ..."); + System.exit(1); + } + + log.info("Output: %1$s", osDestJar[0]); + for (String path : osJarPath) { + log.info("Input : %1$s", path); + } + + try { + AsmGenerator agen = new AsmGenerator(log, osDestJar[0], + new Class<?>[] { // classes to inject in the final JAR + OverrideMethod.class, + MethodListener.class, + MethodAdapter.class + }, + new String[] { // methods to force override + "android.view.View#isInEditMode", + "android.content.res.Resources$Theme#obtainStyledAttributes", + }, + new String[] { // classes to rename (so that we can replace them in layoutlib) + // original-platform-class-name ======> renamed-class-name + "android.graphics.Matrix", "android.graphics._Original_Matrix", + "android.graphics.Paint", "android.graphics._Original_Paint", + "android.graphics.Typeface", "android.graphics._Original_Typeface", + "android.graphics.Bitmap", "android.graphics._Original_Bitmap", + "android.graphics.Path", "android.graphics._Original_Path", + "android.graphics.PorterDuffXfermode", "android.graphics._Original_PorterDuffXfermode", + "android.graphics.Shader", "android.graphics._Original_Shader", + "android.graphics.LinearGradient", "android.graphics._Original_LinearGradient", + "android.graphics.BitmapShader", "android.graphics._Original_BitmapShader", + "android.graphics.ComposeShader", "android.graphics._Original_ComposeShader", + "android.graphics.RadialGradient", "android.graphics._Original_RadialGradient", + "android.graphics.SweepGradient", "android.graphics._Original_SweepGradient", + "android.util.FloatMath", "android.util._Original_FloatMath", + "android.view.SurfaceView", "android.view._Original_SurfaceView", + }, + new String[] { // methods deleted from their return type. + "android.graphics.Paint", // class to delete method from + "android.graphics.Paint$Align", // list of type identifying methods to delete + "android.graphics.Paint$Style", + "android.graphics.Paint$Join", + "android.graphics.Paint$Cap", + "android.graphics.Paint$FontMetrics", + "android.graphics.Paint$FontMetricsInt", + null } + ); + + AsmAnalyzer aa = new AsmAnalyzer(log, osJarPath, agen, + new String[] { "android.view.View" }, // derived from + new String[] { // include classes + "android.*", // for android.R + "android.util.*", + "com.android.internal.util.*", + "android.view.*", + "android.widget.*", + "com.android.internal.widget.*", + "android.text.**", + "android.graphics.*", + "android.graphics.drawable.*", + "android.content.*", + "android.content.res.*", + "org.apache.harmony.xml.*", + "com.android.internal.R**", + "android.pim.*", // for datepicker + "android.os.*", // for android.os.Handler + }); + aa.analyze(); + agen.generate(); + + // Throw an error if any class failed to get renamed by the generator + // + // IMPORTANT: if you're building the platform and you get this error message, + // it means the renameClasses[] array in AsmGenerator needs to be updated: some + // class should have been renamed but it was not found in the input JAR files. + Set<String> notRenamed = agen.getClassesNotRenamed(); + if (notRenamed.size() > 0) { + // (80-column guide below for error formatting) + // 01234567890123456789012345678901234567890123456789012345678901234567890123456789 + log.error( + "ERROR when running layoutlib_create: the following classes are referenced\n" + + "by tools/layoutlib/create but were not actually found in the input JAR files.\n" + + "This may be due to some platform classes having been renamed."); + for (String fqcn : notRenamed) { + log.error("- Class not found: %s", fqcn.replace('/', '.')); + } + for (String path : osJarPath) { + log.info("- Input JAR : %1$s", path); + } + System.exit(1); + } + + System.exit(0); + } catch (IOException e) { + log.exception(e, "Failed to load jar"); + } catch (LogAbortException e) { + e.error(log); + } + + System.exit(1); + } + + /** + * Returns true if args where properly parsed. + * Returns false if program should exit with command-line usage. + * <p/> + * Note: the String[0] is an output parameter wrapped in an array, since there is no + * "out" parameter support. + */ + private static boolean processArgs(Log log, String[] args, + ArrayList<String> osJarPath, String[] osDestJar) { + for (int i = 0; i < args.length; i++) { + String s = args[i]; + if (s.equals("-v")) { + log.setVerbose(true); + } else if (!s.startsWith("-")) { + if (osDestJar[0] == null) { + osDestJar[0] = s; + } else { + osJarPath.add(s); + } + } else { + log.error("Unknow argument: %s", s); + return false; + } + } + + if (osJarPath.isEmpty()) { + log.error("Missing parameter: path to input jar"); + return false; + } + if (osDestJar[0] == null) { + log.error("Missing parameter: path to output jar"); + return false; + } + + return true; + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodAdapter.java new file mode 100644 index 0000000..627ea17 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodAdapter.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + + +/** + * An adapter to make it easier to use {@link MethodListener}. + * <p/> + * The adapter calls the void {@link #onInvokeV(String, boolean, Object)} listener + * for all types (I, L, F, D and A), returning 0 or null as appropriate. + */ +public class MethodAdapter implements MethodListener { + /** + * A stub method is being invoked. + * <p/> + * Known limitation: caller arguments are not available. + * + * @param signature The signature of the method being invoked, composed of the + * binary class name followed by the method descriptor (aka argument + * types). Example: "com/foo/MyClass/InnerClass/printInt(I)V". + * @param isNative True if the method was a native method. + * @param caller The calling object. Null for static methods, "this" for instance methods. + */ + public void onInvokeV(String signature, boolean isNative, Object caller) { + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns an integer or similar. + * @see #onInvokeV(String, boolean, Object) + * @return an integer, or a boolean, or a short or a byte. + */ + public int onInvokeI(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return 0; + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a long. + * @see #onInvokeV(String, boolean, Object) + * @return a long. + */ + public long onInvokeL(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return 0; + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a float. + * @see #onInvokeV(String, boolean, Object) + * @return a float. + */ + public float onInvokeF(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return 0; + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a double. + * @see #onInvokeV(String, boolean, Object) + * @return a double. + */ + public double onInvokeD(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return 0; + } + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns an object. + * @see #onInvokeV(String, boolean, Object) + * @return an object. + */ + public Object onInvokeA(String signature, boolean isNative, Object caller) { + onInvokeV(signature, isNative, caller); + return null; + } +} + diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java new file mode 100644 index 0000000..6fc2b24 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + + +/** + * Interface to allow a method invocation to be listened upon. + * <p/> + * This is used by {@link OverrideMethod} to register a listener for methods that + * have been stubbed by the {@link AsmGenerator}. At runtime the stub will call either a + * default global listener or a specific listener based on the method signature. + */ +public interface MethodListener { + /** + * A stub method is being invoked. + * <p/> + * Known limitation: caller arguments are not available. + * + * @param signature The signature of the method being invoked, composed of the + * binary class name followed by the method descriptor (aka argument + * types). Example: "com/foo/MyClass/InnerClass/printInt(I)V". + * @param isNative True if the method was a native method. + * @param caller The calling object. Null for static methods, "this" for instance methods. + */ + public void onInvokeV(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns an integer or similar. + * @see #onInvokeV(String, boolean, Object) + * @return an integer, or a boolean, or a short or a byte. + */ + public int onInvokeI(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a long. + * @see #onInvokeV(String, boolean, Object) + * @return a long. + */ + public long onInvokeL(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a float. + * @see #onInvokeV(String, boolean, Object) + * @return a float. + */ + public float onInvokeF(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns a double. + * @see #onInvokeV(String, boolean, Object) + * @return a double. + */ + public double onInvokeD(String signature, boolean isNative, Object caller); + + /** + * Same as {@link #onInvokeV(String, boolean, Object)} but returns an object. + * @see #onInvokeV(String, boolean, Object) + * @return an object. + */ + public Object onInvokeA(String signature, boolean isNative, Object caller); +} + diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java new file mode 100644 index 0000000..a6aff99 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import java.util.HashMap; + +/** + * Allows stub methods from LayoutLib to be overriden at runtime. + * <p/> + * Implementation note: all types required by this class(inner/outer classes & interfaces) + * must be referenced by the injectClass argument to {@link AsmGenerator} in Main.java; + * Otherwise they won't be accessible in layoutlib.jar at runtime. + */ +public final class OverrideMethod { + + /** Map of method overridden. */ + private static HashMap<String, MethodListener> sMethods = new HashMap<String, MethodListener>(); + /** Default listener for all method not listed in sMethods. Nothing if null. */ + private static MethodListener sDefaultListener = null; + + /** + * Sets the default listener for all methods not specifically handled. + * Null means to do nothing. + */ + public static void setDefaultListener(MethodListener listener) { + sDefaultListener = listener; + } + + /** + * Defines or reset a listener for the given method signature. + * + * @param signature The signature of the method being invoked, composed of the + * binary class name followed by the method descriptor (aka argument + * types). Example: "com/foo/MyClass/InnerClass/printInt(I)V" + * @param listener The new listener. Removes it if null. + */ + public static void setMethodListener(String signature, MethodListener listener) { + if (listener == null) { + sMethods.remove(signature); + } else { + sMethods.put(signature, listener); + } + } + + /** + * Invokes the specific listener for the given signature or the default one if defined. + * <p/> + * This version invokes the method listener for the void return type. + * <p/> + * Note: this is not intended to be used by the LayoutLib Bridge. It is intended to be called + * by the stubbed methods generated by the LayoutLib_create tool. + * + * @param signature The signature of the method being invoked, composed of the + * binary class name followed by the method descriptor (aka argument + * types). Example: "com/foo/MyClass/InnerClass/printInt(I)V". + * @param isNative True if the method was a native method. + * @param caller The calling object. Null for static methods, "this" for instance methods. + */ + public static void invokeV(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + i.onInvokeV(signature, isNative, caller); + } else if (sDefaultListener != null) { + sDefaultListener.onInvokeV(signature, isNative, caller); + } + } + + /** + * Invokes the specific listener for the int return type. + * @see #invokeV(String, boolean, Object) + */ + public static int invokeI(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeI(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeI(signature, isNative, caller); + } + return 0; + } + + /** + * Invokes the specific listener for the long return type. + * @see #invokeV(String, boolean, Object) + */ + public static long invokeL(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeL(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeL(signature, isNative, caller); + } + return 0; + } + + /** + * Invokes the specific listener for the float return type. + * @see #invokeV(String, boolean, Object) + */ + public static float invokeF(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeF(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeF(signature, isNative, caller); + } + return 0; + } + + /** + * Invokes the specific listener for the double return type. + * @see #invokeV(String, boolean, Object) + */ + public static double invokeD(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeD(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeD(signature, isNative, caller); + } + return 0; + } + + /** + * Invokes the specific listener for the object return type. + * @see #invokeV(String, boolean, Object) + */ + public static Object invokeA(String signature, boolean isNative, Object caller) { + MethodListener i = sMethods.get(signature); + if (i != null) { + return i.onInvokeA(signature, isNative, caller); + } else if (sDefaultListener != null) { + return sDefaultListener.onInvokeA(signature, isNative, caller); + } + return null; + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/RenameClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/RenameClassAdapter.java new file mode 100644 index 0000000..0956b92 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/RenameClassAdapter.java @@ -0,0 +1,446 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassAdapter; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodAdapter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; +import org.objectweb.asm.signature.SignatureWriter; + +/** + * This class visitor renames a class from a given old name to a given new name. + * The class visitor will also rename all inner classes and references in the methods. + * <p/> + * + * For inner classes, this handles only the case where the outer class name changes. + * The inner class name should remain the same. + */ +public class RenameClassAdapter extends ClassAdapter { + + + private final String mOldName; + private final String mNewName; + private String mOldBase; + private String mNewBase; + + /** + * Creates a class visitor that renames a class from a given old name to a given new name. + * The class visitor will also rename all inner classes and references in the methods. + * The names must be full qualified internal ASM names (e.g. com/blah/MyClass$InnerClass). + */ + public RenameClassAdapter(ClassWriter cv, String oldName, String newName) { + super(cv); + mOldBase = mOldName = oldName; + mNewBase = mNewName = newName; + + int pos = mOldName.indexOf('$'); + if (pos > 0) { + mOldBase = mOldName.substring(0, pos); + } + pos = mNewName.indexOf('$'); + if (pos > 0) { + mNewBase = mNewName.substring(0, pos); + } + + assert (mOldBase == null && mNewBase == null) || (mOldBase != null && mNewBase != null); + } + + + /** + * Renames a type descriptor, e.g. "Lcom.package.MyClass;" + * If the type doesn't need to be renamed, returns the input string as-is. + */ + String renameTypeDesc(String desc) { + if (desc == null) { + return null; + } + + return renameType(Type.getType(desc)); + } + + /** + * Renames an object type, e.g. "Lcom.package.MyClass;" or an array type that has an + * object element, e.g. "[Lcom.package.MyClass;" + * If the type doesn't need to be renamed, returns the internal name of the input type. + */ + String renameType(Type type) { + if (type == null) { + return null; + } + + if (type.getSort() == Type.OBJECT) { + String in = type.getInternalName(); + return "L" + renameInternalType(in) + ";"; + } else if (type.getSort() == Type.ARRAY) { + StringBuilder sb = new StringBuilder(); + for (int n = type.getDimensions(); n > 0; n--) { + sb.append('['); + } + sb.append(renameType(type.getElementType())); + return sb.toString(); + } + return type.getDescriptor(); + } + + /** + * Renames an object type, e.g. "Lcom.package.MyClass;" or an array type that has an + * object element, e.g. "[Lcom.package.MyClass;". + * This is like renameType() except that it returns a Type object. + * If the type doesn't need to be renamed, returns the input type object. + */ + Type renameTypeAsType(Type type) { + if (type == null) { + return null; + } + + if (type.getSort() == Type.OBJECT) { + String in = type.getInternalName(); + String newIn = renameInternalType(in); + if (newIn != in) { + return Type.getType("L" + newIn + ";"); + } + } else if (type.getSort() == Type.ARRAY) { + StringBuilder sb = new StringBuilder(); + for (int n = type.getDimensions(); n > 0; n--) { + sb.append('['); + } + sb.append(renameType(type.getElementType())); + return Type.getType(sb.toString()); + } + return type; + } + + /** + * Renames an internal type name, e.g. "com.package.MyClass". + * If the type doesn't need to be renamed, returns the input string as-is. + * <p/> + * The internal type of some of the MethodVisitor turns out to be a type + descriptor sometimes so descriptors are renamed too. + */ + String renameInternalType(String type) { + if (type == null) { + return null; + } + + if (type.equals(mOldName)) { + return mNewName; + } + + if (mOldBase != mOldName && type.equals(mOldBase)) { + return mNewBase; + } + + int pos = type.indexOf('$'); + if (pos == mOldBase.length() && type.startsWith(mOldBase)) { + return mNewBase + type.substring(pos); + } + + // The internal type of some of the MethodVisitor turns out to be a type + // descriptor sometimes. This is the case with visitTypeInsn(type) and + // visitMethodInsn(owner). We try to detect it and adjust it here. + if (type.indexOf(';') > 0) { + type = renameTypeDesc(type); + } + + return type; + } + + /** + * Renames a method descriptor, i.e. applies renameType to all arguments and to the + * return value. + */ + String renameMethodDesc(String desc) { + if (desc == null) { + return null; + } + + Type[] args = Type.getArgumentTypes(desc); + + StringBuilder sb = new StringBuilder("("); + for (Type arg : args) { + String name = renameType(arg); + sb.append(name); + } + sb.append(')'); + + Type ret = Type.getReturnType(desc); + String name = renameType(ret); + sb.append(name); + + return sb.toString(); + } + + + /** + * Renames the ClassSignature handled by ClassVisitor.visit + * or the MethodTypeSignature handled by ClassVisitor.visitMethod. + */ + String renameTypeSignature(String sig) { + if (sig == null) { + return null; + } + SignatureReader reader = new SignatureReader(sig); + SignatureWriter writer = new SignatureWriter(); + reader.accept(new RenameSignatureAdapter(writer)); + sig = writer.toString(); + return sig; + } + + + /** + * Renames the FieldTypeSignature handled by ClassVisitor.visitField + * or MethodVisitor.visitLocalVariable. + */ + String renameFieldSignature(String sig) { + if (sig == null) { + return null; + } + SignatureReader reader = new SignatureReader(sig); + SignatureWriter writer = new SignatureWriter(); + reader.acceptType(new RenameSignatureAdapter(writer)); + sig = writer.toString(); + return sig; + } + + + //---------------------------------- + // Methods from the ClassAdapter + + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + name = renameInternalType(name); + superName = renameInternalType(superName); + signature = renameTypeSignature(signature); + + super.visit(version, access, name, signature, superName, interfaces); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + assert outerName.equals(mOldName); + outerName = renameInternalType(outerName); + name = outerName + "$" + innerName; + super.visitInnerClass(name, outerName, innerName, access); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + desc = renameMethodDesc(desc); + signature = renameTypeSignature(signature); + MethodVisitor mw = super.visitMethod(access, name, desc, signature, exceptions); + return new RenameMethodAdapter(mw); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + desc = renameTypeDesc(desc); + return super.visitAnnotation(desc, visible); + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, + String signature, Object value) { + desc = renameTypeDesc(desc); + signature = renameFieldSignature(signature); + return super.visitField(access, name, desc, signature, value); + } + + + //---------------------------------- + + /** + * A method visitor that renames all references from an old class name to a new class name. + */ + public class RenameMethodAdapter extends MethodAdapter { + + /** + * Creates a method visitor that renames all references from a given old name to a given new + * name. The method visitor will also rename all inner classes. + * The names must be full qualified internal ASM names (e.g. com/blah/MyClass$InnerClass). + */ + public RenameMethodAdapter(MethodVisitor mv) { + super(mv); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + desc = renameTypeDesc(desc); + + return super.visitAnnotation(desc, visible); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) { + desc = renameTypeDesc(desc); + + return super.visitParameterAnnotation(parameter, desc, visible); + } + + @Override + public void visitTypeInsn(int opcode, String type) { + type = renameInternalType(type); + + super.visitTypeInsn(opcode, type); + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + owner = renameInternalType(owner); + desc = renameTypeDesc(desc); + + super.visitFieldInsn(opcode, owner, name, desc); + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + owner = renameInternalType(owner); + desc = renameMethodDesc(desc); + + super.visitMethodInsn(opcode, owner, name, desc); + } + + @Override + public void visitLdcInsn(Object cst) { + // If cst is a Type, this means the code is trying to pull the .class constant + // for this class, so it needs to be renamed too. + if (cst instanceof Type) { + cst = renameTypeAsType((Type) cst); + } + super.visitLdcInsn(cst); + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + desc = renameTypeDesc(desc); + + super.visitMultiANewArrayInsn(desc, dims); + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + type = renameInternalType(type); + + super.visitTryCatchBlock(start, end, handler, type); + } + + @Override + public void visitLocalVariable(String name, String desc, String signature, + Label start, Label end, int index) { + desc = renameTypeDesc(desc); + signature = renameFieldSignature(signature); + + super.visitLocalVariable(name, desc, signature, start, end, index); + } + + } + + //---------------------------------- + + public class RenameSignatureAdapter implements SignatureVisitor { + + private final SignatureVisitor mSv; + + public RenameSignatureAdapter(SignatureVisitor sv) { + mSv = sv; + } + + public void visitClassType(String name) { + name = renameInternalType(name); + mSv.visitClassType(name); + } + + public void visitInnerClassType(String name) { + name = renameInternalType(name); + mSv.visitInnerClassType(name); + } + + public SignatureVisitor visitArrayType() { + SignatureVisitor sv = mSv.visitArrayType(); + return new RenameSignatureAdapter(sv); + } + + public void visitBaseType(char descriptor) { + mSv.visitBaseType(descriptor); + } + + public SignatureVisitor visitClassBound() { + SignatureVisitor sv = mSv.visitClassBound(); + return new RenameSignatureAdapter(sv); + } + + public void visitEnd() { + mSv.visitEnd(); + } + + public SignatureVisitor visitExceptionType() { + SignatureVisitor sv = mSv.visitExceptionType(); + return new RenameSignatureAdapter(sv); + } + + public void visitFormalTypeParameter(String name) { + mSv.visitFormalTypeParameter(name); + } + + public SignatureVisitor visitInterface() { + SignatureVisitor sv = mSv.visitInterface(); + return new RenameSignatureAdapter(sv); + } + + public SignatureVisitor visitInterfaceBound() { + SignatureVisitor sv = mSv.visitInterfaceBound(); + return new RenameSignatureAdapter(sv); + } + + public SignatureVisitor visitParameterType() { + SignatureVisitor sv = mSv.visitParameterType(); + return new RenameSignatureAdapter(sv); + } + + public SignatureVisitor visitReturnType() { + SignatureVisitor sv = mSv.visitReturnType(); + return new RenameSignatureAdapter(sv); + } + + public SignatureVisitor visitSuperclass() { + SignatureVisitor sv = mSv.visitSuperclass(); + return new RenameSignatureAdapter(sv); + } + + public void visitTypeArgument() { + mSv.visitTypeArgument(); + } + + public SignatureVisitor visitTypeArgument(char wildcard) { + SignatureVisitor sv = mSv.visitTypeArgument(wildcard); + return new RenameSignatureAdapter(sv); + } + + public void visitTypeVariable(String name) { + mSv.visitTypeVariable(name); + } + + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java new file mode 100644 index 0000000..9a57a4a --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +/** + * This method adapter rewrites a method by discarding the original code and generating + * a stub depending on the return type. Original annotations are passed along unchanged. + */ +class StubMethodAdapter implements MethodVisitor { + + private static String CONSTRUCTOR = "<init>"; + private static String CLASS_INIT = "<clinit>"; + + /** The parent method writer */ + private MethodVisitor mParentVisitor; + /** The method return type. Can be null. */ + private Type mReturnType; + /** Message to be printed by stub methods. */ + private String mInvokeSignature; + /** Flag to output the first line number. */ + private boolean mOutputFirstLineNumber = true; + /** Flag that is true when implementing a constructor, to accept all original + * code calling the original super constructor. */ + private boolean mIsInitMethod = false; + + private boolean mMessageGenerated; + private final boolean mIsStatic; + private final boolean mIsNative; + + public StubMethodAdapter(MethodVisitor mv, String methodName, Type returnType, + String invokeSignature, boolean isStatic, boolean isNative) { + mParentVisitor = mv; + mReturnType = returnType; + mInvokeSignature = invokeSignature; + mIsStatic = isStatic; + mIsNative = isNative; + + if (CONSTRUCTOR.equals(methodName) || CLASS_INIT.equals(methodName)) { + mIsInitMethod = true; + } + } + + private void generateInvoke() { + /* Generates the code: + * OverrideMethod.invoke("signature", mIsNative ? true : false, null or this); + */ + mParentVisitor.visitLdcInsn(mInvokeSignature); + // push true or false + mParentVisitor.visitInsn(mIsNative ? Opcodes.ICONST_1 : Opcodes.ICONST_0); + // push null or this + if (mIsStatic) { + mParentVisitor.visitInsn(Opcodes.ACONST_NULL); + } else { + mParentVisitor.visitVarInsn(Opcodes.ALOAD, 0); + } + + int sort = mReturnType != null ? mReturnType.getSort() : Type.VOID; + switch(sort) { + case Type.VOID: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeV", + "(Ljava/lang/String;ZLjava/lang/Object;)V"); + mParentVisitor.visitInsn(Opcodes.RETURN); + break; + case Type.BOOLEAN: + case Type.CHAR: + case Type.BYTE: + case Type.SHORT: + case Type.INT: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeI", + "(Ljava/lang/String;ZLjava/lang/Object;)I"); + switch(sort) { + case Type.BOOLEAN: + Label l1 = new Label(); + mParentVisitor.visitJumpInsn(Opcodes.IFEQ, l1); + mParentVisitor.visitInsn(Opcodes.ICONST_1); + mParentVisitor.visitInsn(Opcodes.IRETURN); + mParentVisitor.visitLabel(l1); + mParentVisitor.visitInsn(Opcodes.ICONST_0); + break; + case Type.CHAR: + mParentVisitor.visitInsn(Opcodes.I2C); + break; + case Type.BYTE: + mParentVisitor.visitInsn(Opcodes.I2B); + break; + case Type.SHORT: + mParentVisitor.visitInsn(Opcodes.I2S); + break; + } + mParentVisitor.visitInsn(Opcodes.IRETURN); + break; + case Type.LONG: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeL", + "(Ljava/lang/String;ZLjava/lang/Object;)J"); + mParentVisitor.visitInsn(Opcodes.LRETURN); + break; + case Type.FLOAT: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeF", + "(Ljava/lang/String;ZLjava/lang/Object;)F"); + mParentVisitor.visitInsn(Opcodes.FRETURN); + break; + case Type.DOUBLE: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeD", + "(Ljava/lang/String;ZLjava/lang/Object;)D"); + mParentVisitor.visitInsn(Opcodes.DRETURN); + break; + case Type.ARRAY: + case Type.OBJECT: + mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, + "com/android/tools/layoutlib/create/OverrideMethod", + "invokeA", + "(Ljava/lang/String;ZLjava/lang/Object;)Ljava/lang/Object;"); + mParentVisitor.visitTypeInsn(Opcodes.CHECKCAST, mReturnType.getInternalName()); + mParentVisitor.visitInsn(Opcodes.ARETURN); + break; + } + + } + + private void generatePop() { + /* Pops the stack, depending on the return type. + */ + switch(mReturnType != null ? mReturnType.getSort() : Type.VOID) { + case Type.VOID: + break; + case Type.BOOLEAN: + case Type.CHAR: + case Type.BYTE: + case Type.SHORT: + case Type.INT: + case Type.FLOAT: + case Type.ARRAY: + case Type.OBJECT: + mParentVisitor.visitInsn(Opcodes.POP); + break; + case Type.LONG: + case Type.DOUBLE: + mParentVisitor.visitInsn(Opcodes.POP2); + break; + } + } + + /* Pass down to visitor writer. In this implementation, either do nothing. */ + public void visitCode() { + mParentVisitor.visitCode(); + } + + /* + * visitMaxs is called just before visitEnd if there was any code to rewrite. + * For non-constructor, generate the messaging code and the return statement + * if it hasn't been done before. + */ + public void visitMaxs(int maxStack, int maxLocals) { + if (!mIsInitMethod && !mMessageGenerated) { + generateInvoke(); + mMessageGenerated = true; + } + mParentVisitor.visitMaxs(maxStack, maxLocals); + } + + /** + * End of visiting. + * For non-constructor, generate the messaging code and the return statement + * if it hasn't been done before. + */ + public void visitEnd() { + if (!mIsInitMethod && !mMessageGenerated) { + generateInvoke(); + mMessageGenerated = true; + mParentVisitor.visitMaxs(1, 1); + } + mParentVisitor.visitEnd(); + } + + /* Writes all annotation from the original method. */ + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + return mParentVisitor.visitAnnotation(desc, visible); + } + + /* Writes all annotation default values from the original method. */ + public AnnotationVisitor visitAnnotationDefault() { + return mParentVisitor.visitAnnotationDefault(); + } + + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, + boolean visible) { + return mParentVisitor.visitParameterAnnotation(parameter, desc, visible); + } + + /* Writes all attributes from the original method. */ + public void visitAttribute(Attribute attr) { + mParentVisitor.visitAttribute(attr); + } + + /* + * Only writes the first line number present in the original code so that source + * viewers can direct to the correct method, even if the content doesn't match. + */ + public void visitLineNumber(int line, Label start) { + if (mIsInitMethod || mOutputFirstLineNumber) { + mParentVisitor.visitLineNumber(line, start); + mOutputFirstLineNumber = false; + } + } + + /** + * For non-constructor, rewrite existing "return" instructions to write the message. + */ + public void visitInsn(int opcode) { + if (mIsInitMethod) { + switch (opcode) { + case Opcodes.RETURN: + case Opcodes.ARETURN: + case Opcodes.DRETURN: + case Opcodes.FRETURN: + case Opcodes.IRETURN: + case Opcodes.LRETURN: + // Pop the last word from the stack since invoke will generate its own return. + generatePop(); + generateInvoke(); + mMessageGenerated = true; + default: + mParentVisitor.visitInsn(opcode); + } + } + } + + public void visitLabel(Label label) { + if (mIsInitMethod) { + mParentVisitor.visitLabel(label); + } + } + + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (mIsInitMethod) { + mParentVisitor.visitTryCatchBlock(start, end, handler, type); + } + } + + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + if (mIsInitMethod) { + mParentVisitor.visitMethodInsn(opcode, owner, name, desc); + } + } + + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + if (mIsInitMethod) { + mParentVisitor.visitFieldInsn(opcode, owner, name, desc); + } + } + + public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { + if (mIsInitMethod) { + mParentVisitor.visitFrame(type, nLocal, local, nStack, stack); + } + } + + public void visitIincInsn(int var, int increment) { + if (mIsInitMethod) { + mParentVisitor.visitIincInsn(var, increment); + } + } + + public void visitIntInsn(int opcode, int operand) { + if (mIsInitMethod) { + mParentVisitor.visitIntInsn(opcode, operand); + } + } + + public void visitJumpInsn(int opcode, Label label) { + if (mIsInitMethod) { + mParentVisitor.visitJumpInsn(opcode, label); + } + } + + public void visitLdcInsn(Object cst) { + if (mIsInitMethod) { + mParentVisitor.visitLdcInsn(cst); + } + } + + public void visitLocalVariable(String name, String desc, String signature, + Label start, Label end, int index) { + if (mIsInitMethod) { + mParentVisitor.visitLocalVariable(name, desc, signature, start, end, index); + } + } + + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + if (mIsInitMethod) { + mParentVisitor.visitLookupSwitchInsn(dflt, keys, labels); + } + } + + public void visitMultiANewArrayInsn(String desc, int dims) { + if (mIsInitMethod) { + mParentVisitor.visitMultiANewArrayInsn(desc, dims); + } + } + + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + if (mIsInitMethod) { + mParentVisitor.visitTableSwitchInsn(min, max, dflt, labels); + } + } + + public void visitTypeInsn(int opcode, String type) { + if (mIsInitMethod) { + mParentVisitor.visitTypeInsn(opcode, type); + } + } + + public void visitVarInsn(int opcode, int var) { + if (mIsInitMethod) { + mParentVisitor.visitVarInsn(opcode, var); + } + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java new file mode 100644 index 0000000..e294d56 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import org.objectweb.asm.ClassAdapter; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import java.util.Set; + +/** + * Class adapter that can stub some or all of the methods of the class. + */ +class TransformClassAdapter extends ClassAdapter { + + /** True if all methods should be stubbed, false if only native ones must be stubbed. */ + private final boolean mStubAll; + /** True if the class is an interface. */ + private boolean mIsInterface; + private final String mClassName; + private final Log mLog; + private final Set<String> mStubMethods; + private Set<String> mDeleteReturns; + + /** + * Creates a new class adapter that will stub some or all methods. + * @param logger + * @param stubMethods + * @param deleteReturns list of types that trigger the deletion of methods returning them. + * @param className The name of the class being modified + * @param cv The parent class writer visitor + * @param stubNativesOnly True if only native methods should be stubbed. False if all + * methods should be stubbed. + * @param hasNative True if the method has natives, in which case its access should be + * changed. + */ + public TransformClassAdapter(Log logger, Set<String> stubMethods, + Set<String> deleteReturns, String className, ClassVisitor cv, + boolean stubNativesOnly, boolean hasNative) { + super(cv); + mLog = logger; + mStubMethods = stubMethods; + mClassName = className; + mStubAll = !stubNativesOnly; + mIsInterface = false; + mDeleteReturns = deleteReturns; + } + + /* Visits the class header. */ + @Override + public void visit(int version, int access, String name, + String signature, String superName, String[] interfaces) { + + // This class might be being renamed. + name = mClassName; + + // remove protected or private and set as public + access = access & ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED); + access |= Opcodes.ACC_PUBLIC; + // remove final + access = access & ~Opcodes.ACC_FINAL; + // note: leave abstract classes as such + // don't try to implement stub for interfaces + + mIsInterface = ((access & Opcodes.ACC_INTERFACE) != 0); + super.visit(version, access, name, signature, superName, interfaces); + } + + /* Visits the header of an inner class. */ + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // remove protected or private and set as public + access = access & ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED); + access |= Opcodes.ACC_PUBLIC; + // remove final + access = access & ~Opcodes.ACC_FINAL; + // note: leave abstract classes as such + // don't try to implement stub for interfaces + + super.visitInnerClass(name, outerName, innerName, access); + } + + /* Visits a method. */ + @Override + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + + if (mDeleteReturns != null) { + Type t = Type.getReturnType(desc); + if (t.getSort() == Type.OBJECT) { + String returnType = t.getInternalName(); + if (returnType != null) { + if (mDeleteReturns.contains(returnType)) { + return null; + } + } + } + } + + String methodSignature = mClassName.replace('/', '.') + "#" + name; + + // change access to public + access &= ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE); + access |= Opcodes.ACC_PUBLIC; + + // remove final + access = access & ~Opcodes.ACC_FINAL; + + // stub this method if they are all to be stubbed or if it is a native method + // and don't try to stub interfaces nor abstract non-native methods. + if (!mIsInterface && + ((access & (Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE)) != Opcodes.ACC_ABSTRACT) && + (mStubAll || + (access & Opcodes.ACC_NATIVE) != 0) || + mStubMethods.contains(methodSignature)) { + + boolean isStatic = (access & Opcodes.ACC_STATIC) != 0; + boolean isNative = (access & Opcodes.ACC_NATIVE) != 0; + + // remove abstract, final and native + access = access & ~(Opcodes.ACC_ABSTRACT | Opcodes.ACC_FINAL | Opcodes.ACC_NATIVE); + + String invokeSignature = methodSignature + desc; + mLog.debug(" Stub: %s (%s)", invokeSignature, isNative ? "native" : ""); + + MethodVisitor mw = super.visitMethod(access, name, desc, signature, exceptions); + return new StubMethodAdapter(mw, name, returnType(desc), invokeSignature, + isStatic, isNative); + + } else { + mLog.debug(" Keep: %s %s", name, desc); + return super.visitMethod(access, name, desc, signature, exceptions); + } + } + + /* Visits a field. Makes it public. */ + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, + Object value) { + // change access to public + access &= ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE); + access |= Opcodes.ACC_PUBLIC; + + return super.visitField(access, name, desc, signature, value); + } + + /** + * Extracts the return {@link Type} of this descriptor. + */ + Type returnType(String desc) { + if (desc != null) { + try { + return Type.getReturnType(desc); + } catch (ArrayIndexOutOfBoundsException e) { + // ignore, not a valid type. + } + } + return null; + } +} diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java new file mode 100644 index 0000000..603284e --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.tools.layoutlib.create; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import com.android.tools.layoutlib.create.AsmAnalyzer.DependencyVisitor; +import com.android.tools.layoutlib.create.LogTest.MockLog; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; + +/** + * Unit tests for some methods of {@link AsmAnalyzer}. + */ +public class AsmAnalyzerTest { + + private MockLog mLog; + private ArrayList<String> mOsJarPath; + private AsmAnalyzer mAa; + + @Before + public void setUp() throws Exception { + mLog = new LogTest.MockLog(); + URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar"); + + mOsJarPath = new ArrayList<String>(); + mOsJarPath.add(url.getFile()); + + mAa = new AsmAnalyzer(mLog, mOsJarPath, null /* gen */, + null /* deriveFrom */, null /* includeGlobs */ ); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testParseZip() throws IOException { + Map<String, ClassReader> map = mAa.parseZip(mOsJarPath); + + assertArrayEquals(new String[] { + "mock_android.dummy.InnerTest", + "mock_android.dummy.InnerTest$DerivingClass", + "mock_android.dummy.InnerTest$MyGenerics1", + "mock_android.dummy.InnerTest$MyIntEnum", + "mock_android.dummy.InnerTest$MyStaticInnerClass", + "mock_android.dummy.InnerTest$NotStaticInner1", + "mock_android.dummy.InnerTest$NotStaticInner2", + "mock_android.view.View", + "mock_android.view.ViewGroup", + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams", + "mock_android.widget.LinearLayout", + "mock_android.widget.LinearLayout$LayoutParams", + "mock_android.widget.TableLayout", + "mock_android.widget.TableLayout$LayoutParams" + }, + map.keySet().toArray()); + } + + @Test + public void testFindClass() throws IOException, LogAbortException { + Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath); + TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + + ClassReader cr = mAa.findClass("mock_android.view.ViewGroup$LayoutParams", + zipClasses, found); + + assertNotNull(cr); + assertEquals("mock_android/view/ViewGroup$LayoutParams", cr.getClassName()); + assertArrayEquals(new String[] { "mock_android.view.ViewGroup$LayoutParams" }, + found.keySet().toArray()); + assertArrayEquals(new ClassReader[] { cr }, found.values().toArray()); + } + + @Test + public void testFindGlobs() throws IOException, LogAbortException { + Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath); + TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + + // this matches classes, a package match returns nothing + found.clear(); + mAa.findGlobs("mock_android.view", zipClasses, found); + + assertArrayEquals(new String[] { }, + found.keySet().toArray()); + + // a complex glob search. * is a search pattern that matches names, not dots + mAa.findGlobs("mock_android.*.*Group$*Layout*", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams" + }, + found.keySet().toArray()); + + // a complex glob search. ** is a search pattern that matches names including dots + mAa.findGlobs("mock_android.**Group*", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.ViewGroup", + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams" + }, + found.keySet().toArray()); + + // matches a single class + found.clear(); + mAa.findGlobs("mock_android.view.View", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.View" + }, + found.keySet().toArray()); + + // matches everyting inside the given package but not sub-packages + found.clear(); + mAa.findGlobs("mock_android.view.*", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.View", + "mock_android.view.ViewGroup", + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams" + }, + found.keySet().toArray()); + + for (String key : found.keySet()) { + ClassReader value = found.get(key); + assertNotNull("No value for " + key, value); + assertEquals(key, AsmAnalyzer.classReaderToClassName(value)); + } + } + + @Test + public void testFindClassesDerivingFrom() throws LogAbortException, IOException { + Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath); + TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + + mAa.findClassesDerivingFrom("mock_android.view.View", zipClasses, found); + + assertArrayEquals(new String[] { + "mock_android.view.View", + "mock_android.view.ViewGroup", + "mock_android.widget.LinearLayout", + "mock_android.widget.TableLayout", + }, + found.keySet().toArray()); + + for (String key : found.keySet()) { + ClassReader value = found.get(key); + assertNotNull("No value for " + key, value); + assertEquals(key, AsmAnalyzer.classReaderToClassName(value)); + } + } + + @Test + public void testDependencyVisitor() throws IOException, LogAbortException { + Map<String, ClassReader> zipClasses = mAa.parseZip(mOsJarPath); + TreeMap<String, ClassReader> keep = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> new_keep = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> in_deps = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> out_deps = new TreeMap<String, ClassReader>(); + + ClassReader cr = mAa.findClass("mock_android.widget.TableLayout", zipClasses, keep); + DependencyVisitor visitor = mAa.getVisitor(zipClasses, keep, new_keep, in_deps, out_deps); + + // get first level dependencies + cr.accept(visitor, 0 /* flags */); + + assertArrayEquals(new String[] { + "mock_android.view.ViewGroup", + "mock_android.widget.TableLayout$LayoutParams", + }, + out_deps.keySet().toArray()); + + in_deps.putAll(out_deps); + out_deps.clear(); + + // get second level dependencies + for (ClassReader cr2 : in_deps.values()) { + cr2.accept(visitor, 0 /* flags */); + } + + assertArrayEquals(new String[] { + "mock_android.view.View", + "mock_android.view.ViewGroup$LayoutParams", + "mock_android.view.ViewGroup$MarginLayoutParams", + }, + out_deps.keySet().toArray()); + + in_deps.putAll(out_deps); + out_deps.clear(); + + // get third level dependencies (there are none) + for (ClassReader cr2 : in_deps.values()) { + cr2.accept(visitor, 0 /* flags */); + } + + assertArrayEquals(new String[] { }, out_deps.keySet().toArray()); + } +} diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java new file mode 100644 index 0000000..7cdf79a --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.tools.layoutlib.create; + + +import static org.junit.Assert.assertArrayEquals; + +import com.android.tools.layoutlib.create.LogTest.MockLog; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Set; + +/** + * Unit tests for some methods of {@link AsmGenerator}. + */ +public class AsmGeneratorTest { + + private MockLog mLog; + private ArrayList<String> mOsJarPath; + private String mOsDestJar; + private File mTempFile; + + @Before + public void setUp() throws Exception { + mLog = new LogTest.MockLog(); + URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar"); + + mOsJarPath = new ArrayList<String>(); + mOsJarPath.add(url.getFile()); + + mTempFile = File.createTempFile("mock", "jar"); + mOsDestJar = mTempFile.getAbsolutePath(); + mTempFile.deleteOnExit(); + } + + @After + public void tearDown() throws Exception { + if (mTempFile != null) { + mTempFile.delete(); + mTempFile = null; + } + } + + @Test + public void testClassRenaming() throws IOException, LogAbortException { + + AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, + null, // classes to inject in the final JAR + null, // methods to force override + new String[] { // classes to rename (so that we can replace them) + "mock_android.view.View", "mock_android.view._Original_View", + "not.an.actual.ClassName", "anoter.fake.NewClassName", + }, + null // methods deleted from their return type. + ); + + AsmAnalyzer aa = new AsmAnalyzer(mLog, mOsJarPath, agen, + null, // derived from + new String[] { // include classes + "**" + }); + aa.analyze(); + agen.generate(); + + Set<String> notRenamed = agen.getClassesNotRenamed(); + assertArrayEquals(new String[] { "not/an/actual/ClassName" }, notRenamed.toArray()); + } +} diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java new file mode 100644 index 0000000..3f13158 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class LogTest { + + public static class MockLog extends Log { + StringBuilder mOut = new StringBuilder(); + StringBuilder mErr = new StringBuilder(); + + public String getOut() { + return mOut.toString(); + } + + public String getErr() { + return mErr.toString(); + } + + @Override + protected void outPrintln(String msg) { + mOut.append(msg); + mOut.append('\n'); + } + + @Override + protected void errPrintln(String msg) { + mErr.append(msg); + mErr.append('\n'); + } + } + + private MockLog mLog; + + @Before + public void setUp() throws Exception { + mLog = new MockLog(); + } + + @After + public void tearDown() throws Exception { + // pass + } + + @Test + public void testDebug() { + assertEquals("", mLog.getOut()); + assertEquals("", mLog.getErr()); + + mLog.setVerbose(false); + mLog.debug("Test %d", 42); + assertEquals("", mLog.getOut()); + + mLog.setVerbose(true); + mLog.debug("Test %d", 42); + + assertEquals("Test 42\n", mLog.getOut()); + assertEquals("", mLog.getErr()); + } + + @Test + public void testInfo() { + assertEquals("", mLog.getOut()); + assertEquals("", mLog.getErr()); + + mLog.info("Test %d", 43); + + assertEquals("Test 43\n", mLog.getOut()); + assertEquals("", mLog.getErr()); + } + + @Test + public void testError() { + assertEquals("", mLog.getOut()); + assertEquals("", mLog.getErr()); + + mLog.error("Test %d", 44); + + assertEquals("", mLog.getOut()); + assertEquals("Test 44\n", mLog.getErr()); + } + + @Test + public void testException() { + assertEquals("", mLog.getOut()); + assertEquals("", mLog.getErr()); + + Exception e = new Exception("My Exception"); + mLog.exception(e, "Test %d", 44); + + assertEquals("", mLog.getOut()); + assertTrue(mLog.getErr().startsWith("Test 44\njava.lang.Exception: My Exception")); + } +} diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/RenameClassAdapterTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/RenameClassAdapterTest.java new file mode 100644 index 0000000..90c6a9c --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/RenameClassAdapterTest.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.android.tools.layoutlib.create; + +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * + */ +public class RenameClassAdapterTest { + + private RenameClassAdapter mOuter; + private RenameClassAdapter mInner; + + @Before + public void setUp() throws Exception { + mOuter = new RenameClassAdapter(null, // cv + "com.pack.Old", + "org.blah.New"); + + mInner = new RenameClassAdapter(null, // cv + "com.pack.Old$Inner", + "org.blah.New$Inner"); + } + + @After + public void tearDown() throws Exception { + } + + /** + * Renames a type, e.g. "Lcom.package.My;" + * If the type doesn't need to be renamed, returns the input string as-is. + */ + @Test + public void testRenameTypeDesc() { + + // primitive types are left untouched + assertEquals("I", mOuter.renameTypeDesc("I")); + assertEquals("D", mOuter.renameTypeDesc("D")); + assertEquals("V", mOuter.renameTypeDesc("V")); + + // object types that need no renaming are left untouched + assertEquals("Lcom.package.MyClass;", mOuter.renameTypeDesc("Lcom.package.MyClass;")); + assertEquals("Lcom.package.MyClass;", mInner.renameTypeDesc("Lcom.package.MyClass;")); + + // object types that match the requirements + assertEquals("Lorg.blah.New;", mOuter.renameTypeDesc("Lcom.pack.Old;")); + assertEquals("Lorg.blah.New$Inner;", mInner.renameTypeDesc("Lcom.pack.Old$Inner;")); + // inner classes match the base type which is being renamed + assertEquals("Lorg.blah.New$Other;", mOuter.renameTypeDesc("Lcom.pack.Old$Other;")); + assertEquals("Lorg.blah.New$Other;", mInner.renameTypeDesc("Lcom.pack.Old$Other;")); + + // arrays + assertEquals("[Lorg.blah.New;", mOuter.renameTypeDesc("[Lcom.pack.Old;")); + assertEquals("[[Lorg.blah.New;", mOuter.renameTypeDesc("[[Lcom.pack.Old;")); + + assertEquals("[Lorg.blah.New;", mInner.renameTypeDesc("[Lcom.pack.Old;")); + assertEquals("[[Lorg.blah.New;", mInner.renameTypeDesc("[[Lcom.pack.Old;")); + } + + /** + * Renames an object type, e.g. "Lcom.package.MyClass;" or an array type that has an + * object element, e.g. "[Lcom.package.MyClass;" + * If the type doesn't need to be renamed, returns the internal name of the input type. + */ + @Test + public void testRenameType() { + // Skip. This is actually tested by testRenameTypeDesc above. + } + + /** + * Renames an internal type name, e.g. "com.package.MyClass". + * If the type doesn't need to be renamed, returns the input string as-is. + */ + @Test + public void testRenameInternalType() { + // a descriptor is not left untouched + assertEquals("Lorg.blah.New;", mOuter.renameInternalType("Lcom.pack.Old;")); + assertEquals("Lorg.blah.New$Inner;", mOuter.renameInternalType("Lcom.pack.Old$Inner;")); + + // an actual FQCN + assertEquals("org.blah.New", mOuter.renameInternalType("com.pack.Old")); + assertEquals("org.blah.New$Inner", mOuter.renameInternalType("com.pack.Old$Inner")); + + assertEquals("org.blah.New$Other", mInner.renameInternalType("com.pack.Old$Other")); + assertEquals("org.blah.New$Other", mInner.renameInternalType("com.pack.Old$Other")); + } + + /** + * Renames a method descriptor, i.e. applies renameType to all arguments and to the + * return value. + */ + @Test + public void testRenameMethodDesc() { + assertEquals("(IDLorg.blah.New;[Lorg.blah.New$Inner;)Lorg.blah.New$Other;", + mOuter.renameMethodDesc("(IDLcom.pack.Old;[Lcom.pack.Old$Inner;)Lcom.pack.Old$Other;")); + } + + + +} diff --git a/tools/layoutlib/create/tests/data/mock_android.jar b/tools/layoutlib/create/tests/data/mock_android.jar Binary files differnew file mode 100644 index 0000000..a7ea74f --- /dev/null +++ b/tools/layoutlib/create/tests/data/mock_android.jar diff --git a/tools/layoutlib/create/tests/data/mock_android.jardesc b/tools/layoutlib/create/tests/data/mock_android.jardesc new file mode 100644 index 0000000..95f7591 --- /dev/null +++ b/tools/layoutlib/create/tests/data/mock_android.jardesc @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="WINDOWS-1252" standalone="no"?> +<jardesc> + <jar path="C:/ralf/google/src/raphael-lapdroid/device/tools/layoutlib/create/tests/data/mock_android.jar"/> + <options buildIfNeeded="true" compress="true" descriptionLocation="/layoutlib_create/tests/data/mock_android.jardesc" exportErrors="true" exportWarnings="true" includeDirectoryEntries="false" overwrite="false" saveDescription="true" storeRefactorings="false" useSourceFolders="false"/> + <storedRefactorings deprecationInfo="true" structuralOnly="false"/> + <selectedProjects/> + <manifest generateManifest="true" manifestLocation="" manifestVersion="1.0" reuseManifest="false" saveManifest="false" usesManifest="true"> + <sealing sealJar="false"> + <packagesToSeal/> + <packagesToUnSeal/> + </sealing> + </manifest> + <selectedElements exportClassFiles="true" exportJavaFiles="false" exportOutputFolder="false"> + <javaElement handleIdentifier="=layoutlib_create/tests<mock_android.widget"/> + <javaElement handleIdentifier="=layoutlib_create/tests<mock_android.view"/> + <javaElement handleIdentifier="=layoutlib_create/tests<mock_android.dummy"/> + </selectedElements> +</jardesc> diff --git a/tools/layoutlib/create/tests/mock_android/dummy/InnerTest.java b/tools/layoutlib/create/tests/mock_android/dummy/InnerTest.java new file mode 100644 index 0000000..e355ead --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/dummy/InnerTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.dummy; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +public class InnerTest { + + private int mSomeField; + private MyStaticInnerClass mInnerInstance; + private MyIntEnum mTheIntEnum; + private MyGenerics1<int[][], InnerTest, MyIntEnum, float[]> mGeneric1; + + public class NotStaticInner2 extends NotStaticInner1 { + + } + + public class NotStaticInner1 { + + public void someThing() { + mSomeField = 2; + mInnerInstance = null; + } + + } + + private static class MyStaticInnerClass { + + } + + private static class DerivingClass extends InnerTest { + + } + + // enums are a kind of inner static class + public enum MyIntEnum { + VALUE0(0), + VALUE1(1), + VALUE2(2); + + MyIntEnum(int myInt) { + this.myInt = myInt; + } + final int myInt; + } + + public static class MyGenerics1<T, U, V, W> { + public MyGenerics1() { + int a = 1; + } + } + + public <X> void genericMethod1(X a, X[] a) { + } + + public <X, Y> void genericMethod2(X a, List<Y> b) { + } + + public <X, Y> void genericMethod3(X a, List<Y extends InnerTest> b) { + } + + public <T extends InnerTest> void genericMethod4(T[] a, Collection<T> b, Collection<?> c) { + Iterator<T> i = b.iterator(); + } + + public void someMethod(InnerTest self) { + mSomeField = self.mSomeField; + MyStaticInnerClass m = new MyStaticInnerClass(); + mInnerInstance = m; + mTheIntEnum = null; + mGeneric1 = new MyGenerics1(); + genericMethod(new DerivingClass[0], new ArrayList<DerivingClass>(), new ArrayList<InnerTest>()); + } +} diff --git a/tools/layoutlib/create/tests/mock_android/view/View.java b/tools/layoutlib/create/tests/mock_android/view/View.java new file mode 100644 index 0000000..a80a98d --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/view/View.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.view; + +public class View { + +} diff --git a/tools/layoutlib/create/tests/mock_android/view/ViewGroup.java b/tools/layoutlib/create/tests/mock_android/view/ViewGroup.java new file mode 100644 index 0000000..466470f --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/view/ViewGroup.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.view; + +public class ViewGroup extends View { + + public class MarginLayoutParams extends LayoutParams { + + } + + public class LayoutParams { + + } + +} diff --git a/tools/layoutlib/create/tests/mock_android/widget/LinearLayout.java b/tools/layoutlib/create/tests/mock_android/widget/LinearLayout.java new file mode 100644 index 0000000..3870a63 --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/widget/LinearLayout.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.widget; + +import mock_android.view.ViewGroup; + +public class LinearLayout extends ViewGroup { + + public class LayoutParams extends mock_android.view.ViewGroup.LayoutParams { + + } + +} diff --git a/tools/layoutlib/create/tests/mock_android/widget/TableLayout.java b/tools/layoutlib/create/tests/mock_android/widget/TableLayout.java new file mode 100644 index 0000000..e455e7d --- /dev/null +++ b/tools/layoutlib/create/tests/mock_android/widget/TableLayout.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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 mock_android.widget; + +import mock_android.view.ViewGroup; + +public class TableLayout extends ViewGroup { + + public class LayoutParams extends MarginLayoutParams { + + } + +} diff --git a/tools/localize/Android.mk b/tools/localize/Android.mk new file mode 100644 index 0000000..186177f --- /dev/null +++ b/tools/localize/Android.mk @@ -0,0 +1,56 @@ +# +# Copyright 2006 The Android Open Source Project +# +# Android Asset Packaging Tool +# + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + file_utils.cpp \ + localize.cpp \ + merge_res_and_xliff.cpp \ + res_check.cpp \ + xmb.cpp \ + Configuration.cpp \ + Perforce.cpp \ + SourcePos.cpp \ + Values.cpp \ + ValuesFile.cpp \ + XLIFFFile.cpp \ + XMLHandler.cpp + +LOCAL_C_INCLUDES := \ + external/expat/lib \ + build/libs/host/include + +LOCAL_CFLAGS += -g -O0 + +LOCAL_STATIC_LIBRARIES := \ + libexpat \ + libhost \ + libutils \ + libcutils + +ifeq ($(HOST_OS),linux) +LOCAL_LDLIBS += -lrt +endif + + +LOCAL_MODULE := localize + +ifeq (a,a) + LOCAL_CFLAGS += -DLOCALIZE_WITH_TESTS + LOCAL_SRC_FILES += \ + test.cpp \ + localize_test.cpp \ + merge_res_and_xliff_test.cpp \ + Perforce_test.cpp \ + ValuesFile_test.cpp \ + XLIFFFile_test.cpp \ + XMLHandler_test.cpp +endif + +include $(BUILD_HOST_EXECUTABLE) + diff --git a/tools/localize/Configuration.cpp b/tools/localize/Configuration.cpp new file mode 100644 index 0000000..56addbd --- /dev/null +++ b/tools/localize/Configuration.cpp @@ -0,0 +1,76 @@ +#include "Configuration.h" +#include <string.h> + +int +Configuration::Compare(const Configuration& that) const +{ + int n; + + n = locale.compare(that.locale); + if (n != 0) return n; + + n = vendor.compare(that.vendor); + if (n != 0) return n; + + n = orientation.compare(that.orientation); + if (n != 0) return n; + + n = density.compare(that.density); + if (n != 0) return n; + + n = touchscreen.compare(that.touchscreen); + if (n != 0) return n; + + n = keyboard.compare(that.keyboard); + if (n != 0) return n; + + n = navigation.compare(that.navigation); + if (n != 0) return n; + + n = screenSize.compare(that.screenSize); + if (n != 0) return n; + + return 0; +} + +string +Configuration::ToString() const +{ + string s; + if (locale.length() > 0) { + if (s.length() > 0) { + s += "-"; + } + s += locale; + } + return s; +} + +bool +split_locale(const string& in, string* language, string* region) +{ + const int len = in.length(); + if (len == 2) { + if (isalpha(in[0]) && isalpha(in[1])) { + *language = in; + region->clear(); + return true; + } else { + return false; + } + } + else if (len == 5) { + if (isalpha(in[0]) && isalpha(in[1]) && (in[2] == '_' || in[2] == '-') + && isalpha(in[3]) && isalpha(in[4])) { + language->assign(in.c_str(), 2); + region->assign(in.c_str()+3, 2); + return true; + } else { + return false; + } + } + else { + return false; + } +} + diff --git a/tools/localize/Configuration.h b/tools/localize/Configuration.h new file mode 100644 index 0000000..f91bf04 --- /dev/null +++ b/tools/localize/Configuration.h @@ -0,0 +1,38 @@ +#ifndef CONFIGURATION_H +#define CONFIGURATION_H + +#include <string> + +using namespace std; + +struct Configuration +{ + string locale; + string vendor; + string orientation; + string density; + string touchscreen; + string keyboard; + string navigation; + string screenSize; + + // Compare two configurations + int Compare(const Configuration& that) const; + + inline bool operator<(const Configuration& that) const { return Compare(that) < 0; } + inline bool operator<=(const Configuration& that) const { return Compare(that) <= 0; } + inline bool operator==(const Configuration& that) const { return Compare(that) == 0; } + inline bool operator!=(const Configuration& that) const { return Compare(that) != 0; } + inline bool operator>=(const Configuration& that) const { return Compare(that) >= 0; } + inline bool operator>(const Configuration& that) const { return Compare(that) > 0; } + + // Parse a directory name, like "values-en-rUS". Return the first segment in resType. + bool ParseDiectoryName(const string& dir, string* resType); + + string ToString() const; +}; + +bool split_locale(const string& in, string* language, string* region); + + +#endif // CONFIGURATION_H diff --git a/tools/localize/Perforce.cpp b/tools/localize/Perforce.cpp new file mode 100644 index 0000000..3425668 --- /dev/null +++ b/tools/localize/Perforce.cpp @@ -0,0 +1,230 @@ +#include "Perforce.h" +#include "log.h" +#include <string.h> +#include <stdlib.h> +#include <sstream> +#include <sys/types.h> +#include <unistd.h> +#include <sys/wait.h> + +using namespace std; + +extern char** environ; + +int +Perforce::RunCommand(const string& cmd, string* result, bool printOnFailure) +{ + int err; + int outPipe[2]; + int errPipe[2]; + pid_t pid; + + log_printf("Perforce::RunCommand: %s\n", cmd.c_str()); + + err = pipe(outPipe); + err |= pipe(errPipe); + if (err == -1) { + printf("couldn't create pipe. exiting.\n"); + exit(1); + return -1; + } + + pid = fork(); + if (pid == -1) { + printf("couldn't fork. eixiting\n"); + exit(1); + return -1; + } + else if (pid == 0) { + char const* args[] = { + "/bin/sh", + "-c", + cmd.c_str(), + NULL + }; + close(outPipe[0]); + close(errPipe[0]); + dup2(outPipe[1], 1); + dup2(errPipe[1], 2); + execve(args[0], (char* const*)args, environ); + // done + } + + close(outPipe[1]); + close(errPipe[1]); + + result->clear(); + + char buf[1024]; + + // stdout + while (true) { + size_t amt = read(outPipe[0], buf, sizeof(buf)); + result->append(buf, amt); + if (amt <= 0) { + break; + } + } + + // stderr -- the messages are short so it ought to just fit in the buffer + string error; + while (true) { + size_t amt = read(errPipe[0], buf, sizeof(buf)); + error.append(buf, amt); + if (amt <= 0) { + break; + } + } + + close(outPipe[0]); + close(errPipe[0]); + + waitpid(pid, &err, 0); + if (WIFEXITED(err)) { + err = WEXITSTATUS(err); + } else { + err = -1; + } + if (err != 0 && printOnFailure) { + write(2, error.c_str(), error.length()); + } + return err; +} + +int +Perforce::GetResourceFileNames(const string& version, const string& base, + const vector<string>& apps, vector<string>* results, + bool printOnFailure) +{ + int err; + string text; + stringstream cmd; + + cmd << "p4 files"; + + const size_t I = apps.size(); + for (size_t i=0; i<I; i++) { + cmd << " \"" << base << '/' << apps[i] << "/res/values/strings.xml@" << version << '"'; + } + + err = RunCommand(cmd.str(), &text, printOnFailure); + + const char* str = text.c_str(); + while (*str) { + const char* lineend = strchr(str, '\n'); + if (lineend == str) { + str++; + continue; + } + if (lineend-str > 1023) { + fprintf(stderr, "line too long!\n"); + return 1; + } + + string s(str, lineend-str); + + char filename[1024]; + char edit[1024]; + int count = sscanf(str, "%[^#]#%*d - %s change %*d %*[^\n]\n", filename, edit); + + if (count == 2 && 0 != strcmp("delete", edit)) { + results->push_back(string(filename)); + } + + str = lineend + 1; + } + + return err; +} + +int +Perforce::GetFile(const string& file, const string& version, string* result, + bool printOnFailure) +{ + stringstream cmd; + cmd << "p4 print -q \"" << file << '@' << version << '"'; + return RunCommand(cmd.str(), result, printOnFailure); +} + +string +Perforce::GetCurrentChange(bool printOnFailure) +{ + int err; + string text; + + err = RunCommand("p4 changes -m 1 \\#have", &text, printOnFailure); + if (err != 0) { + return ""; + } + + long long n; + int count = sscanf(text.c_str(), "Change %lld on", &n); + if (count != 1) { + return ""; + } + + char result[100]; + sprintf(result, "%lld", n); + + return string(result); +} + +static int +do_files(const string& op, const vector<string>& files, bool printOnFailure) +{ + string text; + stringstream cmd; + + cmd << "p4 " << op; + + const size_t I = files.size(); + for (size_t i=0; i<I; i++) { + cmd << " \"" << files[i] << "\""; + } + + return Perforce::RunCommand(cmd.str(), &text, printOnFailure); +} + +int +Perforce::EditFiles(const vector<string>& files, bool printOnFailure) +{ + return do_files("edit", files, printOnFailure); +} + +int +Perforce::AddFiles(const vector<string>& files, bool printOnFailure) +{ + return do_files("add", files, printOnFailure); +} + +int +Perforce::DeleteFiles(const vector<string>& files, bool printOnFailure) +{ + return do_files("delete", files, printOnFailure); +} + +string +Perforce::Where(const string& depotPath, bool printOnFailure) +{ + int err; + string text; + string cmd = "p4 where "; + cmd += depotPath; + + err = RunCommand(cmd, &text, printOnFailure); + if (err != 0) { + return ""; + } + + size_t index = text.find(' '); + if (index == text.npos) { + return ""; + } + index = text.find(' ', index+1)+1; + if (index == text.npos) { + return ""; + } + + return text.substr(index, text.length()-index-1); +} + diff --git a/tools/localize/Perforce.h b/tools/localize/Perforce.h new file mode 100644 index 0000000..522797d --- /dev/null +++ b/tools/localize/Perforce.h @@ -0,0 +1,25 @@ +#ifndef PERFORCE_H +#define PERFORCE_H + +#include <string> +#include <vector> + +using namespace std; + +class Perforce +{ +public: + static int RunCommand(const string& cmd, string* result, bool printOnFailure); + static int GetResourceFileNames(const string& version, const string& base, + const vector<string>& apps, vector<string>* result, + bool printOnFailure); + static int GetFile(const string& file, const string& version, string* result, + bool printOnFailure); + static string GetCurrentChange(bool printOnFailure); + static int EditFiles(const vector<string>& filename, bool printOnFailure); + static int AddFiles(const vector<string>& files, bool printOnFailure); + static int DeleteFiles(const vector<string>& files, bool printOnFailure); + static string Where(const string& depotPath, bool printOnFailure); +}; + +#endif // PERFORCE_H diff --git a/tools/localize/Perforce_test.cpp b/tools/localize/Perforce_test.cpp new file mode 100644 index 0000000..142b20e --- /dev/null +++ b/tools/localize/Perforce_test.cpp @@ -0,0 +1,62 @@ +#include "Perforce.h" +#include <stdio.h> + +static int +RunCommand_test() +{ + string result; + int err = Perforce::RunCommand("p4 help csommands", &result, true); + printf("err=%d result=[[%s]]\n", err, result.c_str()); + return 0; +} + +static int +GetResourceFileNames_test() +{ + vector<string> results; + vector<string> apps; + apps.push_back("apps/common"); + apps.push_back("apps/Contacts"); + int err = Perforce::GetResourceFileNames("43019", "//device", apps, &results, true); + if (err != 0) { + return err; + } + if (results.size() != 2) { + return 1; + } + if (results[0] != "//device/apps/common/res/values/strings.xml") { + return 1; + } + if (results[1] != "//device/apps/Contacts/res/values/strings.xml") { + return 1; + } + if (false) { + for (size_t i=0; i<results.size(); i++) { + printf("[%zd] '%s'\n", i, results[i].c_str()); + } + } + return 0; +} + +static int +GetFile_test() +{ + string result; + int err = Perforce::GetFile("//device/Makefile", "296", &result, true); + printf("err=%d result=[[%s]]\n", err, result.c_str()); + return 0; +} + +int +Perforce_test() +{ + bool all = false; + int err = 0; + + if (all) err |= RunCommand_test(); + if (all) err |= GetResourceFileNames_test(); + if (all) err |= GetFile_test(); + + return err; +} + diff --git a/tools/localize/SourcePos.cpp b/tools/localize/SourcePos.cpp new file mode 100644 index 0000000..9d7c5c6 --- /dev/null +++ b/tools/localize/SourcePos.cpp @@ -0,0 +1,166 @@ +#include "SourcePos.h" + +#include <stdarg.h> +#include <set> + +using namespace std; + +const SourcePos GENERATED_POS("<generated>", -1); + +// ErrorPos +// ============================================================================= +struct ErrorPos +{ + string file; + int line; + string error; + + ErrorPos(); + ErrorPos(const ErrorPos& that); + ErrorPos(const string& file, int line, const string& error); + ~ErrorPos(); + bool operator<(const ErrorPos& rhs) const; + bool operator==(const ErrorPos& rhs) const; + ErrorPos& operator=(const ErrorPos& rhs); + + void Print(FILE* to) const; +}; + +static set<ErrorPos> g_errors; + +ErrorPos::ErrorPos() +{ +} + +ErrorPos::ErrorPos(const ErrorPos& that) + :file(that.file), + line(that.line), + error(that.error) +{ +} + +ErrorPos::ErrorPos(const string& f, int l, const string& e) + :file(f), + line(l), + error(e) +{ +} + +ErrorPos::~ErrorPos() +{ +} + +bool +ErrorPos::operator<(const ErrorPos& rhs) const +{ + if (this->file < rhs.file) return true; + if (this->file == rhs.file) { + if (this->line < rhs.line) return true; + if (this->line == rhs.line) { + if (this->error < rhs.error) return true; + } + } + return false; +} + +bool +ErrorPos::operator==(const ErrorPos& rhs) const +{ + return this->file == rhs.file + && this->line == rhs.line + && this->error == rhs.error; +} + +ErrorPos& +ErrorPos::operator=(const ErrorPos& rhs) +{ + this->file = rhs.file; + this->line = rhs.line; + this->error = rhs.error; + return *this; +} + +void +ErrorPos::Print(FILE* to) const +{ + if (this->line >= 0) { + fprintf(to, "%s:%d: %s\n", this->file.c_str(), this->line, this->error.c_str()); + } else { + fprintf(to, "%s: %s\n", this->file.c_str(), this->error.c_str()); + } +} + +// SourcePos +// ============================================================================= +SourcePos::SourcePos(const string& f, int l) + : file(f), line(l) +{ +} + +SourcePos::SourcePos(const SourcePos& that) + : file(that.file), line(that.line) +{ +} + +SourcePos::SourcePos() + : file("???", 0) +{ +} + +SourcePos::~SourcePos() +{ +} + +string +SourcePos::ToString() const +{ + char buf[1024]; + if (this->line >= 0) { + snprintf(buf, sizeof(buf)-1, "%s:%d", this->file.c_str(), this->line); + } else { + snprintf(buf, sizeof(buf)-1, "%s:", this->file.c_str()); + } + buf[sizeof(buf)-1] = '\0'; + return string(buf); +} + +int +SourcePos::Error(const char* fmt, ...) const +{ + int retval=0; + char buf[1024]; + va_list ap; + va_start(ap, fmt); + retval = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + char* p = buf + retval - 1; + while (p > buf && *p == '\n') { + *p = '\0'; + p--; + } + ErrorPos err(this->file, this->line, string(buf)); + if (g_errors.find(err) == g_errors.end()) { + err.Print(stderr); + g_errors.insert(err); + } + return retval; +} + +bool +SourcePos::HasErrors() +{ + return g_errors.size() > 0; +} + +void +SourcePos::PrintErrors(FILE* to) +{ + set<ErrorPos>::const_iterator it; + for (it=g_errors.begin(); it!=g_errors.end(); it++) { + it->Print(to); + } +} + + + + diff --git a/tools/localize/SourcePos.h b/tools/localize/SourcePos.h new file mode 100644 index 0000000..5027129 --- /dev/null +++ b/tools/localize/SourcePos.h @@ -0,0 +1,28 @@ +#ifndef SOURCEPOS_H +#define SOURCEPOS_H + +#include <string> + +using namespace std; + +class SourcePos +{ +public: + string file; + int line; + + SourcePos(const string& f, int l); + SourcePos(const SourcePos& that); + SourcePos(); + ~SourcePos(); + + string ToString() const; + int Error(const char* fmt, ...) const; + + static bool HasErrors(); + static void PrintErrors(FILE* to); +}; + +extern const SourcePos GENERATED_POS; + +#endif // SOURCEPOS_H diff --git a/tools/localize/Values.cpp b/tools/localize/Values.cpp new file mode 100644 index 0000000..e396f8b --- /dev/null +++ b/tools/localize/Values.cpp @@ -0,0 +1,134 @@ +#include "Values.h" +#include <stdlib.h> + + +// ===================================================================================== +StringResource::StringResource(const SourcePos& p, const string& f, const Configuration& c, + const string& i, int ix, XMLNode* v, const int ve, const string& vs, + const string& cmnt) + :pos(p), + file(f), + config(c), + id(i), + index(ix), + value(v), + version(ve), + versionString(vs), + comment(cmnt) +{ +} + +StringResource::StringResource() + :pos(), + file(), + config(), + id(), + index(-1), + value(NULL), + version(), + versionString(), + comment() +{ +} + +StringResource::StringResource(const StringResource& that) + :pos(that.pos), + file(that.file), + config(that.config), + id(that.id), + index(that.index), + value(that.value), + version(that.version), + versionString(that.versionString), + comment(that.comment) +{ +} + +int +StringResource::Compare(const StringResource& that) const +{ + if (file != that.file) { + return file < that.file ? -1 : 1; + } + if (id != that.id) { + return id < that.id ? -1 : 1; + } + if (index != that.index) { + return index - that.index; + } + if (config != that.config) { + return config < that.config ? -1 : 1; + } + if (version != that.version) { + return version < that.version ? -1 : 1; + } + return 0; +} + +string +StringResource::TypedID() const +{ + string result; + if (index < 0) { + result = "string:"; + } else { + char n[20]; + sprintf(n, "%d:", index); + result = "array:"; + result += n; + } + result += id; + return result; +} + +static void +split(const string& raw, vector<string>*parts) +{ + size_t index = 0; + while (true) { + size_t next = raw.find(':', index); + if (next != raw.npos) { + parts->push_back(string(raw, index, next-index)); + index = next + 1; + } else { + parts->push_back(string(raw, index)); + break; + } + } +} + +bool +StringResource::ParseTypedID(const string& raw, string* id, int* index) +{ + vector<string> parts; + split(raw, &parts); + + const size_t N = parts.size(); + + for (size_t i=0; i<N; i++) { + if (parts[i].length() == 0) { + return false; + } + } + + if (N == 2 && parts[0] == "string") { + *id = parts[1]; + *index = -1; + return true; + } + else if (N == 3 && parts[0] == "array") { + char* p; + int n = (int)strtol(parts[1].c_str(), &p, 0); + if (*p == '\0') { + *id = parts[2]; + *index = n; + return true; + } else { + return false; + } + } + else { + return false; + } +} + diff --git a/tools/localize/Values.h b/tools/localize/Values.h new file mode 100644 index 0000000..0a60b6d --- /dev/null +++ b/tools/localize/Values.h @@ -0,0 +1,48 @@ +#ifndef VALUES_H +#define VALUES_H + +#include "Configuration.h" +#include "XMLHandler.h" + +#include <string> + +using namespace std; + +enum { + CURRENT_VERSION, + OLD_VERSION +}; + +struct StringResource +{ + StringResource(); + StringResource(const SourcePos& pos, const string& file, const Configuration& config, + const string& id, int index, XMLNode* value, + int version, const string& versionString, const string& comment = ""); + StringResource(const StringResource& that); + + // Compare two configurations + int Compare(const StringResource& that) const; + + inline bool operator<(const StringResource& that) const { return Compare(that) < 0; } + inline bool operator<=(const StringResource& that) const { return Compare(that) <= 0; } + inline bool operator==(const StringResource& that) const { return Compare(that) == 0; } + inline bool operator!=(const StringResource& that) const { return Compare(that) != 0; } + inline bool operator>=(const StringResource& that) const { return Compare(that) >= 0; } + inline bool operator>(const StringResource& that) const { return Compare(that) > 0; } + + string TypedID() const; + static bool ParseTypedID(const string& typed, string* id, int* index); + + SourcePos pos; + string file; + Configuration config; + string id; + int index; + XMLNode* value; + int version; + string versionString; + string comment; +}; + +#endif // VALUES_H diff --git a/tools/localize/ValuesFile.cpp b/tools/localize/ValuesFile.cpp new file mode 100644 index 0000000..bd6f494 --- /dev/null +++ b/tools/localize/ValuesFile.cpp @@ -0,0 +1,266 @@ +#include "ValuesFile.h" + +#include "XMLHandler.h" + +#include <algorithm> +#include <fcntl.h> +#include <expat.h> +#include <unistd.h> +#include <errno.h> + +using namespace std; + +const char* const ANDROID_XMLNS = "http://schemas.android.com/apk/res/android"; +const char* const XLIFF_XMLNS = "urn:oasis:names:tc:xliff:document:1.2"; + +const char *const NS_MAP[] = { + "android", ANDROID_XMLNS, + "xliff", XLIFF_XMLNS, + NULL, NULL +}; + +const XMLNamespaceMap ANDROID_NAMESPACES(NS_MAP); + + +// ===================================================================================== +class ArrayHandler : public XMLHandler +{ +public: + ArrayHandler(ValuesFile* vf, int version, const string& versionString, const string& id); + + virtual int OnStartElement(const SourcePos& pos, const string& ns, const string& name, + const vector<XMLAttribute>& attrs, XMLHandler** next); + virtual int OnText(const SourcePos& pos, const string& text); + virtual int OnComment(const SourcePos& pos, const string& text); + +private: + ValuesFile* m_vf; + int m_version; + int m_index; + string m_versionString; + string m_id; + string m_comment; +}; + +ArrayHandler::ArrayHandler(ValuesFile* vf, int version, const string& versionString, + const string& id) + :m_vf(vf), + m_version(version), + m_index(0), + m_versionString(versionString), + m_id(id) +{ +} + +int +ArrayHandler::OnStartElement(const SourcePos& pos, const string& ns, const string& name, + const vector<XMLAttribute>& attrs, XMLHandler** next) +{ + if (ns == "" && name == "item") { + XMLNode* node = XMLNode::NewElement(pos, ns, name, attrs, XMLNode::EXACT); + m_vf->AddString(StringResource(pos, pos.file, m_vf->GetConfiguration(), + m_id, m_index, node, m_version, m_versionString, + trim_string(m_comment))); + *next = new NodeHandler(node, XMLNode::EXACT); + m_index++; + m_comment = ""; + return 0; + } else { + pos.Error("invalid <%s> element inside <array>\n", name.c_str()); + return 1; + } +} + +int +ArrayHandler::OnText(const SourcePos& pos, const string& text) +{ + return 0; +} + +int +ArrayHandler::OnComment(const SourcePos& pos, const string& text) +{ + m_comment += text; + return 0; +} + +// ===================================================================================== +class ValuesHandler : public XMLHandler +{ +public: + ValuesHandler(ValuesFile* vf, int version, const string& versionString); + + virtual int OnStartElement(const SourcePos& pos, const string& ns, const string& name, + const vector<XMLAttribute>& attrs, XMLHandler** next); + virtual int OnText(const SourcePos& pos, const string& text); + virtual int OnComment(const SourcePos& pos, const string& text); + +private: + ValuesFile* m_vf; + int m_version; + string m_versionString; + string m_comment; +}; + +ValuesHandler::ValuesHandler(ValuesFile* vf, int version, const string& versionString) + :m_vf(vf), + m_version(version), + m_versionString(versionString) +{ +} + +int +ValuesHandler::OnStartElement(const SourcePos& pos, const string& ns, const string& name, + const vector<XMLAttribute>& attrs, XMLHandler** next) +{ + if (ns == "" && name == "string") { + string id = XMLAttribute::Find(attrs, "", "name", ""); + XMLNode* node = XMLNode::NewElement(pos, ns, name, attrs, XMLNode::EXACT); + m_vf->AddString(StringResource(pos, pos.file, m_vf->GetConfiguration(), + id, -1, node, m_version, m_versionString, + trim_string(m_comment))); + *next = new NodeHandler(node, XMLNode::EXACT); + } + else if (ns == "" && name == "array") { + string id = XMLAttribute::Find(attrs, "", "name", ""); + *next = new ArrayHandler(m_vf, m_version, m_versionString, id); + } + m_comment = ""; + return 0; +} + +int +ValuesHandler::OnText(const SourcePos& pos, const string& text) +{ + return 0; +} + +int +ValuesHandler::OnComment(const SourcePos& pos, const string& text) +{ + m_comment += text; + return 0; +} + +// ===================================================================================== +ValuesFile::ValuesFile(const Configuration& config) + :m_config(config), + m_strings(), + m_arrays() +{ +} + +ValuesFile::~ValuesFile() +{ +} + +ValuesFile* +ValuesFile::ParseFile(const string& filename, const Configuration& config, + int version, const string& versionString) +{ + ValuesFile* result = new ValuesFile(config); + + TopElementHandler top("", "resources", new ValuesHandler(result, version, versionString)); + XMLHandler::ParseFile(filename, &top); + + return result; +} + +ValuesFile* +ValuesFile::ParseString(const string& filename, const string& text, const Configuration& config, + int version, const string& versionString) +{ + ValuesFile* result = new ValuesFile(config); + + TopElementHandler top("", "resources", new ValuesHandler(result, version, versionString)); + XMLHandler::ParseString(filename, text, &top); + + return result; +} + +const Configuration& +ValuesFile::GetConfiguration() const +{ + return m_config; +} + +void +ValuesFile::AddString(const StringResource& str) +{ + if (str.index < 0) { + m_strings.insert(str); + } else { + m_arrays[str.id].insert(str); + } +} + +set<StringResource> +ValuesFile::GetStrings() const +{ + set<StringResource> result = m_strings; + + for (map<string,set<StringResource> >::const_iterator it = m_arrays.begin(); + it != m_arrays.end(); it++) { + result.insert(it->second.begin(), it->second.end()); + } + + return result; +} + +XMLNode* +ValuesFile::ToXMLNode() const +{ + XMLNode* root; + + // <resources> + { + vector<XMLAttribute> attrs; + ANDROID_NAMESPACES.AddToAttributes(&attrs); + root = XMLNode::NewElement(GENERATED_POS, "", "resources", attrs, XMLNode::PRETTY); + } + + // <array> + for (map<string,set<StringResource> >::const_iterator it = m_arrays.begin(); + it != m_arrays.end(); it++) { + vector<XMLAttribute> arrayAttrs; + arrayAttrs.push_back(XMLAttribute("", "name", it->first)); + const set<StringResource>& items = it->second; + XMLNode* arrayNode = XMLNode::NewElement(items.begin()->pos, "", "array", arrayAttrs, + XMLNode::PRETTY); + root->EditChildren().push_back(arrayNode); + + // <item> + for (set<StringResource>::const_iterator item = items.begin(); + item != items.end(); item++) { + XMLNode* itemNode = item->value->Clone(); + itemNode->SetName("", "item"); + itemNode->EditAttributes().clear(); + arrayNode->EditChildren().push_back(itemNode); + } + } + + // <string> + for (set<StringResource>::const_iterator it=m_strings.begin(); it!=m_strings.end(); it++) { + const StringResource& str = *it; + vector<XMLAttribute> attrs; + XMLNode* strNode = str.value->Clone(); + strNode->SetName("", "string"); + strNode->EditAttributes().clear(); + strNode->EditAttributes().push_back(XMLAttribute("", "name", str.id)); + root->EditChildren().push_back(strNode); + } + + return root; +} + +string +ValuesFile::ToString() const +{ + XMLNode* xml = ToXMLNode(); + string s = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; + s += xml->ToString(ANDROID_NAMESPACES); + delete xml; + s += '\n'; + return s; +} + diff --git a/tools/localize/ValuesFile.h b/tools/localize/ValuesFile.h new file mode 100644 index 0000000..752fd78 --- /dev/null +++ b/tools/localize/ValuesFile.h @@ -0,0 +1,52 @@ +#ifndef VALUES_FILE_H +#define VALUES_FILE_H + +#include "SourcePos.h" +#include "Configuration.h" +#include "XMLHandler.h" +#include "Values.h" + +#include <string> +#include <set> + +using namespace std; + +extern const XMLNamespaceMap ANDROID_NAMESPACES; + +class ValuesFile +{ +public: + ValuesFile(const Configuration& config); + + static ValuesFile* ParseFile(const string& filename, const Configuration& config, + int version, const string& versionString); + static ValuesFile* ParseString(const string& filename, const string& text, + const Configuration& config, + int version, const string& versionString); + ~ValuesFile(); + + const Configuration& GetConfiguration() const; + + void AddString(const StringResource& str); + set<StringResource> GetStrings() const; + + // exports this file as a n XMLNode, you own this object + XMLNode* ToXMLNode() const; + + // writes the ValuesFile out to a string in the canonical format (i.e. writes the contents of + // ToXMLNode()). + string ToString() const; + +private: + class ParseState; + friend class ValuesFile::ParseState; + friend class StringHandler; + + ValuesFile(); + + Configuration m_config; + set<StringResource> m_strings; + map<string,set<StringResource> > m_arrays; +}; + +#endif // VALUES_FILE_H diff --git a/tools/localize/ValuesFile_test.cpp b/tools/localize/ValuesFile_test.cpp new file mode 100644 index 0000000..56d2ec2 --- /dev/null +++ b/tools/localize/ValuesFile_test.cpp @@ -0,0 +1,54 @@ +#include "ValuesFile.h" +#include <stdio.h> + +int +ValuesFile_test() +{ + int err = 0; + Configuration config; + config.locale = "zz_ZZ"; + ValuesFile* vf = ValuesFile::ParseFile("testdata/values/strings.xml", config, + OLD_VERSION, "1"); + + const set<StringResource>& strings = vf->GetStrings(); + string canonical = vf->ToString(); + + if (false) { + printf("Strings (%zd)\n", strings.size()); + for (set<StringResource>::const_iterator it=strings.begin(); + it!=strings.end(); it++) { + const StringResource& str = *it; + printf("%s: '%s'[%d]='%s' (%s) <!-- %s -->\n", str.pos.ToString().c_str(), + str.id.c_str(), str.index, + str.value->ContentsToString(ANDROID_NAMESPACES).c_str(), + str.config.ToString().c_str(), str.comment.c_str()); + } + + printf("XML:[[%s]]\n", canonical.c_str()); + } + + const char * const EXPECTED = + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<resources xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + " xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n" + " <array name=\"emailAddressTypes\">\n" + " <item>Email</item>\n" + " <item>Home</item>\n" + " <item>Work</item>\n" + " <item>Other\\u2026</item>\n" + " </array>\n" + " <string name=\"test1\">Discard</string>\n" + " <string name=\"test2\">a<b>b<i>c</i></b>d</string>\n" + " <string name=\"test3\">a<xliff:g a=\"b\" xliff:a=\"asdf\">bBb</xliff:g>C</string>\n" + "</resources>\n"; + + if (canonical != EXPECTED) { + fprintf(stderr, "ValuesFile_test failed\n"); + fprintf(stderr, "canonical=[[%s]]\n", canonical.c_str()); + fprintf(stderr, "EXPECTED=[[%s]]\n", EXPECTED); + err = 1; + } + + delete vf; + return err; +} diff --git a/tools/localize/XLIFFFile.cpp b/tools/localize/XLIFFFile.cpp new file mode 100644 index 0000000..51f81de --- /dev/null +++ b/tools/localize/XLIFFFile.cpp @@ -0,0 +1,609 @@ +#include "XLIFFFile.h" + +#include <algorithm> +#include <sys/time.h> +#include <time.h> + +const char* const XLIFF_XMLNS = "urn:oasis:names:tc:xliff:document:1.2"; + +const char *const NS_MAP[] = { + "", XLIFF_XMLNS, + "xml", XMLNS_XMLNS, + NULL, NULL +}; + +const XMLNamespaceMap XLIFF_NAMESPACES(NS_MAP); + +int +XLIFFFile::File::Compare(const XLIFFFile::File& that) const +{ + if (filename != that.filename) { + return filename < that.filename ? -1 : 1; + } + return 0; +} + +// ===================================================================================== +XLIFFFile::XLIFFFile() +{ +} + +XLIFFFile::~XLIFFFile() +{ +} + +static XMLNode* +get_unique_node(const XMLNode* parent, const string& ns, const string& name, bool required) +{ + size_t count = parent->CountElementsByName(ns, name); + if (count == 1) { + return parent->GetElementByNameAt(ns, name, 0); + } else { + if (required) { + SourcePos pos = count == 0 + ? parent->Position() + : parent->GetElementByNameAt(XLIFF_XMLNS, name, 1)->Position(); + pos.Error("<%s> elements must contain exactly one <%s> element", + parent->Name().c_str(), name.c_str()); + } + return NULL; + } +} + +XLIFFFile* +XLIFFFile::Parse(const string& filename) +{ + XLIFFFile* result = new XLIFFFile(); + + XMLNode* root = NodeHandler::ParseFile(filename, XMLNode::PRETTY); + if (root == NULL) { + return NULL; + } + + // <file> + vector<XMLNode*> files = root->GetElementsByName(XLIFF_XMLNS, "file"); + for (size_t i=0; i<files.size(); i++) { + XMLNode* file = files[i]; + + string datatype = file->GetAttribute("", "datatype", ""); + string originalFile = file->GetAttribute("", "original", ""); + + Configuration sourceConfig; + sourceConfig.locale = file->GetAttribute("", "source-language", ""); + result->m_sourceConfig = sourceConfig; + + Configuration targetConfig; + targetConfig.locale = file->GetAttribute("", "target-language", ""); + result->m_targetConfig = targetConfig; + + result->m_currentVersion = file->GetAttribute("", "build-num", ""); + result->m_oldVersion = "old"; + + // <body> + XMLNode* body = get_unique_node(file, XLIFF_XMLNS, "body", true); + if (body == NULL) continue; + + // <trans-unit> + vector<XMLNode*> transUnits = body->GetElementsByName(XLIFF_XMLNS, "trans-unit"); + for (size_t j=0; j<transUnits.size(); j++) { + XMLNode* transUnit = transUnits[j]; + + string rawID = transUnit->GetAttribute("", "id", ""); + if (rawID == "") { + transUnit->Position().Error("<trans-unit> tag requires an id"); + continue; + } + string id; + int index; + + if (!StringResource::ParseTypedID(rawID, &id, &index)) { + transUnit->Position().Error("<trans-unit> has invalid id '%s'\n", rawID.c_str()); + continue; + } + + // <source> + XMLNode* source = get_unique_node(transUnit, XLIFF_XMLNS, "source", false); + if (source != NULL) { + XMLNode* node = source->Clone(); + node->SetPrettyRecursive(XMLNode::EXACT); + result->AddStringResource(StringResource(source->Position(), originalFile, + sourceConfig, id, index, node, CURRENT_VERSION, + result->m_currentVersion)); + } + + // <target> + XMLNode* target = get_unique_node(transUnit, XLIFF_XMLNS, "target", false); + if (target != NULL) { + XMLNode* node = target->Clone(); + node->SetPrettyRecursive(XMLNode::EXACT); + result->AddStringResource(StringResource(target->Position(), originalFile, + targetConfig, id, index, node, CURRENT_VERSION, + result->m_currentVersion)); + } + + // <alt-trans> + XMLNode* altTrans = get_unique_node(transUnit, XLIFF_XMLNS, "alt-trans", false); + if (altTrans != NULL) { + // <source> + XMLNode* altSource = get_unique_node(altTrans, XLIFF_XMLNS, "source", false); + if (altSource != NULL) { + XMLNode* node = altSource->Clone(); + node->SetPrettyRecursive(XMLNode::EXACT); + result->AddStringResource(StringResource(altSource->Position(), + originalFile, sourceConfig, id, index, node, OLD_VERSION, + result->m_oldVersion)); + } + + // <target> + XMLNode* altTarget = get_unique_node(altTrans, XLIFF_XMLNS, "target", false); + if (altTarget != NULL) { + XMLNode* node = altTarget->Clone(); + node->SetPrettyRecursive(XMLNode::EXACT); + result->AddStringResource(StringResource(altTarget->Position(), + originalFile, targetConfig, id, index, node, OLD_VERSION, + result->m_oldVersion)); + } + } + } + } + delete root; + return result; +} + +XLIFFFile* +XLIFFFile::Create(const Configuration& sourceConfig, const Configuration& targetConfig, + const string& currentVersion) +{ + XLIFFFile* result = new XLIFFFile(); + result->m_sourceConfig = sourceConfig; + result->m_targetConfig = targetConfig; + result->m_currentVersion = currentVersion; + return result; +} + +set<string> +XLIFFFile::Files() const +{ + set<string> result; + for (vector<File>::const_iterator f = m_files.begin(); f != m_files.end(); f++) { + result.insert(f->filename); + } + return result; +} + +void +XLIFFFile::AddStringResource(const StringResource& str) +{ + string id = str.TypedID(); + + File* f = NULL; + const size_t I = m_files.size(); + for (size_t i=0; i<I; i++) { + if (m_files[i].filename == str.file) { + f = &m_files[i]; + break; + } + } + if (f == NULL) { + File file; + file.filename = str.file; + m_files.push_back(file); + f = &m_files[I]; + } + + const size_t J = f->transUnits.size(); + TransUnit* g = NULL; + for (size_t j=0; j<J; j++) { + if (f->transUnits[j].id == id) { + g = &f->transUnits[j]; + } + } + if (g == NULL) { + TransUnit group; + group.id = id; + f->transUnits.push_back(group); + g = &f->transUnits[J]; + } + + StringResource* res = find_string_res(*g, str); + if (res == NULL) { + return ; + } + if (res->id != "") { + str.pos.Error("Duplicate string resource: %s", res->id.c_str()); + res->pos.Error("Previous definition here"); + return ; + } + *res = str; + + m_strings.insert(str); +} + +void +XLIFFFile::Filter(bool (*func)(const string&,const TransUnit&,void*), void* cookie) +{ + const size_t I = m_files.size(); + for (size_t ix=0, i=I-1; ix<I; ix++, i--) { + File& file = m_files[i]; + + const size_t J = file.transUnits.size(); + for (size_t jx=0, j=J-1; jx<J; jx++, j--) { + TransUnit& tu = file.transUnits[j]; + + bool keep = func(file.filename, tu, cookie); + if (!keep) { + if (tu.source.id != "") { + m_strings.erase(tu.source); + } + if (tu.target.id != "") { + m_strings.erase(tu.target); + } + if (tu.altSource.id != "") { + m_strings.erase(tu.altSource); + } + if (tu.altTarget.id != "") { + m_strings.erase(tu.altTarget); + } + file.transUnits.erase(file.transUnits.begin()+j); + } + } + if (file.transUnits.size() == 0) { + m_files.erase(m_files.begin()+i); + } + } +} + +void +XLIFFFile::Map(void (*func)(const string&,TransUnit*,void*), void* cookie) +{ + const size_t I = m_files.size(); + for (size_t i=0; i<I; i++) { + File& file = m_files[i]; + + const size_t J = file.transUnits.size(); + for (size_t j=0; j<J; j++) { + func(file.filename, &(file.transUnits[j]), cookie); + } + } +} + +TransUnit* +XLIFFFile::EditTransUnit(const string& filename, const string& id) +{ + const size_t I = m_files.size(); + for (size_t ix=0, i=I-1; ix<I; ix++, i--) { + File& file = m_files[i]; + if (file.filename == filename) { + const size_t J = file.transUnits.size(); + for (size_t jx=0, j=J-1; jx<J; jx++, j--) { + TransUnit& tu = file.transUnits[j]; + if (tu.id == id) { + return &tu; + } + } + } + } + return NULL; +} + +StringResource* +XLIFFFile::find_string_res(TransUnit& g, const StringResource& str) +{ + int index; + if (str.version == CURRENT_VERSION) { + index = 0; + } + else if (str.version == OLD_VERSION) { + index = 2; + } + else { + str.pos.Error("Internal Error %s:%d\n", __FILE__, __LINE__); + return NULL; + } + if (str.config == m_sourceConfig) { + // index += 0; + } + else if (str.config == m_targetConfig) { + index += 1; + } + else { + str.pos.Error("unknown config for string %s: %s", str.id.c_str(), + str.config.ToString().c_str()); + return NULL; + } + switch (index) { + case 0: + return &g.source; + case 1: + return &g.target; + case 2: + return &g.altSource; + case 3: + return &g.altTarget; + } + str.pos.Error("Internal Error %s:%d\n", __FILE__, __LINE__); + return NULL; +} + +int +convert_html_to_xliff(const XMLNode* original, const string& name, XMLNode* addTo, int* phID) +{ + int err = 0; + if (original->Type() == XMLNode::TEXT) { + addTo->EditChildren().push_back(original->Clone()); + return 0; + } else { + string ctype; + if (original->Namespace() == "") { + if (original->Name() == "b") { + ctype = "bold"; + } + else if (original->Name() == "i") { + ctype = "italic"; + } + else if (original->Name() == "u") { + ctype = "underline"; + } + } + if (ctype != "") { + vector<XMLAttribute> attrs; + attrs.push_back(XMLAttribute(XLIFF_XMLNS, "ctype", ctype)); + XMLNode* copy = XMLNode::NewElement(original->Position(), XLIFF_XMLNS, "g", + attrs, XMLNode::EXACT); + + const vector<XMLNode*>& children = original->Children(); + size_t I = children.size(); + for (size_t i=0; i<I; i++) { + err |= convert_html_to_xliff(children[i], name, copy, phID); + } + return err; + } + else { + if (original->Namespace() == XLIFF_XMLNS) { + addTo->EditChildren().push_back(original->Clone()); + return 0; + } else { + if (original->Namespace() == "") { + // flatten out the tag into ph tags -- but only if there is no namespace + // that's still unsupported because propagating the xmlns attribute is hard. + vector<XMLAttribute> attrs; + char idStr[30]; + (*phID)++; + sprintf(idStr, "id-%d", *phID); + attrs.push_back(XMLAttribute(XLIFF_XMLNS, "id", idStr)); + + if (original->Children().size() == 0) { + XMLNode* ph = XMLNode::NewElement(original->Position(), XLIFF_XMLNS, + "ph", attrs, XMLNode::EXACT); + ph->EditChildren().push_back( + XMLNode::NewText(original->Position(), + original->ToString(XLIFF_NAMESPACES), + XMLNode::EXACT)); + addTo->EditChildren().push_back(ph); + } else { + XMLNode* begin = XMLNode::NewElement(original->Position(), XLIFF_XMLNS, + "bpt", attrs, XMLNode::EXACT); + begin->EditChildren().push_back( + XMLNode::NewText(original->Position(), + original->OpenTagToString(XLIFF_NAMESPACES, XMLNode::EXACT), + XMLNode::EXACT)); + XMLNode* end = XMLNode::NewElement(original->Position(), XLIFF_XMLNS, + "ept", attrs, XMLNode::EXACT); + string endText = "</"; + endText += original->Name(); + endText += ">"; + end->EditChildren().push_back(XMLNode::NewText(original->Position(), + endText, XMLNode::EXACT)); + + addTo->EditChildren().push_back(begin); + + const vector<XMLNode*>& children = original->Children(); + size_t I = children.size(); + for (size_t i=0; i<I; i++) { + err |= convert_html_to_xliff(children[i], name, addTo, phID); + } + + addTo->EditChildren().push_back(end); + } + return err; + } else { + original->Position().Error("invalid <%s> element in <%s> tag\n", + original->Name().c_str(), name.c_str()); + return 1; + } + } + } + } +} + +XMLNode* +create_string_node(const StringResource& str, const string& name) +{ + vector<XMLAttribute> attrs; + attrs.push_back(XMLAttribute(XMLNS_XMLNS, "space", "preserve")); + XMLNode* node = XMLNode::NewElement(str.pos, XLIFF_XMLNS, name, attrs, XMLNode::EXACT); + + const vector<XMLNode*>& children = str.value->Children(); + size_t I = children.size(); + int err = 0; + for (size_t i=0; i<I; i++) { + int phID = 0; + err |= convert_html_to_xliff(children[i], name, node, &phID); + } + + if (err != 0) { + delete node; + } + return node; +} + +static bool +compare_id(const TransUnit& lhs, const TransUnit& rhs) +{ + string lid, rid; + int lindex, rindex; + StringResource::ParseTypedID(lhs.id, &lid, &lindex); + StringResource::ParseTypedID(rhs.id, &rid, &rindex); + if (lid < rid) return true; + if (lid == rid && lindex < rindex) return true; + return false; +} + +XMLNode* +XLIFFFile::ToXMLNode() const +{ + XMLNode* root; + size_t N; + + // <xliff> + { + vector<XMLAttribute> attrs; + XLIFF_NAMESPACES.AddToAttributes(&attrs); + attrs.push_back(XMLAttribute(XLIFF_XMLNS, "version", "1.2")); + root = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, "xliff", attrs, XMLNode::PRETTY); + } + + vector<TransUnit> groups; + + // <file> + vector<File> files = m_files; + sort(files.begin(), files.end()); + const size_t I = files.size(); + for (size_t i=0; i<I; i++) { + const File& file = files[i]; + + vector<XMLAttribute> fileAttrs; + fileAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "datatype", "x-android-res")); + fileAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "original", file.filename)); + + struct timeval tv; + struct timezone tz; + gettimeofday(&tv, &tz); + fileAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "date", trim_string(ctime(&tv.tv_sec)))); + + fileAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "source-language", m_sourceConfig.locale)); + fileAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "target-language", m_targetConfig.locale)); + fileAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "build-num", m_currentVersion)); + + XMLNode* fileNode = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, "file", fileAttrs, + XMLNode::PRETTY); + root->EditChildren().push_back(fileNode); + + // <body> + XMLNode* bodyNode = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, "body", + vector<XMLAttribute>(), XMLNode::PRETTY); + fileNode->EditChildren().push_back(bodyNode); + + // <trans-unit> + vector<TransUnit> transUnits = file.transUnits; + sort(transUnits.begin(), transUnits.end(), compare_id); + const size_t J = transUnits.size(); + for (size_t j=0; j<J; j++) { + const TransUnit& transUnit = transUnits[j]; + + vector<XMLAttribute> tuAttrs; + + // strings start with string: + tuAttrs.push_back(XMLAttribute(XLIFF_XMLNS, "id", transUnit.id)); + XMLNode* transUnitNode = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, "trans-unit", + tuAttrs, XMLNode::PRETTY); + bodyNode->EditChildren().push_back(transUnitNode); + + // <extradata> + if (transUnit.source.comment != "") { + vector<XMLAttribute> extradataAttrs; + XMLNode* extraNode = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, "extradata", + extradataAttrs, XMLNode::EXACT); + transUnitNode->EditChildren().push_back(extraNode); + extraNode->EditChildren().push_back( + XMLNode::NewText(GENERATED_POS, transUnit.source.comment, + XMLNode::PRETTY)); + } + + // <source> + if (transUnit.source.id != "") { + transUnitNode->EditChildren().push_back( + create_string_node(transUnit.source, "source")); + } + + // <target> + if (transUnit.target.id != "") { + transUnitNode->EditChildren().push_back( + create_string_node(transUnit.target, "target")); + } + + // <alt-trans> + if (transUnit.altSource.id != "" || transUnit.altTarget.id != "" + || transUnit.rejectComment != "") { + vector<XMLAttribute> altTransAttrs; + XMLNode* altTransNode = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, "alt-trans", + altTransAttrs, XMLNode::PRETTY); + transUnitNode->EditChildren().push_back(altTransNode); + + // <extradata> + if (transUnit.rejectComment != "") { + vector<XMLAttribute> extradataAttrs; + XMLNode* extraNode = XMLNode::NewElement(GENERATED_POS, XLIFF_XMLNS, + "extradata", extradataAttrs, + XMLNode::EXACT); + altTransNode->EditChildren().push_back(extraNode); + extraNode->EditChildren().push_back( + XMLNode::NewText(GENERATED_POS, transUnit.rejectComment, + XMLNode::PRETTY)); + } + + // <source> + if (transUnit.altSource.id != "") { + altTransNode->EditChildren().push_back( + create_string_node(transUnit.altSource, "source")); + } + + // <target> + if (transUnit.altTarget.id != "") { + altTransNode->EditChildren().push_back( + create_string_node(transUnit.altTarget, "target")); + } + } + + } + } + + return root; +} + + +string +XLIFFFile::ToString() const +{ + XMLNode* xml = ToXMLNode(); + string s = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; + s += xml->ToString(XLIFF_NAMESPACES); + delete xml; + s += '\n'; + return s; +} + +Stats +XLIFFFile::GetStats(const string& config) const +{ + Stats stat; + stat.config = config; + stat.files = m_files.size(); + stat.toBeTranslated = 0; + stat.noComments = 0; + + for (vector<File>::const_iterator file=m_files.begin(); file!=m_files.end(); file++) { + stat.toBeTranslated += file->transUnits.size(); + + for (vector<TransUnit>::const_iterator tu=file->transUnits.begin(); + tu!=file->transUnits.end(); tu++) { + if (tu->source.comment == "") { + stat.noComments++; + } + } + } + + stat.totalStrings = stat.toBeTranslated; + + return stat; +} diff --git a/tools/localize/XLIFFFile.h b/tools/localize/XLIFFFile.h new file mode 100644 index 0000000..a93d479 --- /dev/null +++ b/tools/localize/XLIFFFile.h @@ -0,0 +1,98 @@ +#ifndef XLIFF_FILE_H +#define XLIFF_FILE_H + +#include "Values.h" + +#include "Configuration.h" + +#include <set> + +using namespace std; + +extern const XMLNamespaceMap XLIFF_NAMESPACES; + +extern const char*const XLIFF_XMLNS; + +struct Stats +{ + string config; + size_t files; + size_t toBeTranslated; + size_t noComments; + size_t totalStrings; +}; + +struct TransUnit { + string id; + StringResource source; + StringResource target; + StringResource altSource; + StringResource altTarget; + string rejectComment; +}; + +class XLIFFFile +{ +public: + static XLIFFFile* Parse(const string& filename); + static XLIFFFile* Create(const Configuration& sourceConfig, const Configuration& targetConfig, + const string& currentVersion); + ~XLIFFFile(); + + inline const Configuration& SourceConfig() const { return m_sourceConfig; } + inline const Configuration& TargetConfig() const { return m_targetConfig; } + + inline const string& CurrentVersion() const { return m_currentVersion; } + inline const string& OldVersion() const { return m_oldVersion; } + + set<string> Files() const; + + void AddStringResource(const StringResource& res); + inline set<StringResource> const& GetStringResources() const { return m_strings; } + bool FindStringResource(const string& filename, int version, bool source); + + void Filter(bool (*func)(const string&,const TransUnit&,void*), void* cookie); + void Map(void (*func)(const string&,TransUnit*,void*), void* cookie); + + TransUnit* EditTransUnit(const string& file, const string& id); + + // exports this file as a n XMLNode, you own this object + XMLNode* ToXMLNode() const; + + // writes the ValuesFile out to a string in the canonical format (i.e. writes the contents of + // ToXMLNode()). + string ToString() const; + + Stats GetStats(const string& config) const; + +private: + struct File { + int Compare(const File& that) const; + + inline bool operator<(const File& that) const { return Compare(that) < 0; } + inline bool operator<=(const File& that) const { return Compare(that) <= 0; } + inline bool operator==(const File& that) const { return Compare(that) == 0; } + inline bool operator!=(const File& that) const { return Compare(that) != 0; } + inline bool operator>=(const File& that) const { return Compare(that) >= 0; } + inline bool operator>(const File& that) const { return Compare(that) > 0; } + + string filename; + vector<TransUnit> transUnits; + }; + + XLIFFFile(); + StringResource* find_string_res(TransUnit& g, const StringResource& str); + + Configuration m_sourceConfig; + Configuration m_targetConfig; + + string m_currentVersion; + string m_oldVersion; + + set<StringResource> m_strings; + vector<File> m_files; +}; + +int convert_html_to_xliff(const XMLNode* original, const string& name, XMLNode* addTo, int* phID); + +#endif // XLIFF_FILE_H diff --git a/tools/localize/XLIFFFile_test.cpp b/tools/localize/XLIFFFile_test.cpp new file mode 100644 index 0000000..52ed4c3 --- /dev/null +++ b/tools/localize/XLIFFFile_test.cpp @@ -0,0 +1,115 @@ +#include "XLIFFFile.h" +#include <stdio.h> +#include "ValuesFile.h" + +XMLNode* create_string_node(const StringResource& str, const string& name); + +static int +Parse_test() +{ + XLIFFFile* xf = XLIFFFile::Parse("testdata/xliff1.xliff"); + if (xf == NULL) { + return 1; + } + + set<StringResource> const& strings = xf->GetStringResources(); + + if (false) { + for (set<StringResource>::iterator it=strings.begin(); it!=strings.end(); it++) { + const StringResource& str = *it; + printf("STRING!!! id=%s index=%d value='%s' pos=%s file=%s version=%d(%s)\n", + str.id.c_str(), str.index, + str.value->ContentsToString(ANDROID_NAMESPACES).c_str(), + str.pos.ToString().c_str(), str.file.c_str(), str.version, + str.versionString.c_str()); + } + printf("XML:[[%s]]\n", xf->ToString().c_str()); + } + + delete xf; + return 0; +} + +static XMLNode* +add_html_tag(XMLNode* addTo, const string& tag) +{ + vector<XMLAttribute> attrs; + XMLNode* node = XMLNode::NewElement(GENERATED_POS, "", tag, attrs, XMLNode::EXACT); + addTo->EditChildren().push_back(node); + return node; +} + +static int +create_string_node_test() +{ + int err = 0; + StringResource res; + vector<XMLAttribute> attrs; + res.value = XMLNode::NewElement(GENERATED_POS, "", "something", attrs, XMLNode::EXACT); + res.value->EditChildren().push_back(XMLNode::NewText(GENERATED_POS, " begin ", XMLNode::EXACT)); + + XMLNode* child; + + child = add_html_tag(res.value, "b"); + child->EditChildren().push_back(XMLNode::NewText(GENERATED_POS, "b", XMLNode::EXACT)); + + child = add_html_tag(res.value, "i"); + child->EditChildren().push_back(XMLNode::NewText(GENERATED_POS, "i", XMLNode::EXACT)); + + child = add_html_tag(child, "b"); + child->EditChildren().push_back(XMLNode::NewText(GENERATED_POS, "b", XMLNode::EXACT)); + + child = add_html_tag(res.value, "u"); + child->EditChildren().push_back(XMLNode::NewText(GENERATED_POS, "u", XMLNode::EXACT)); + + + res.value->EditChildren().push_back(XMLNode::NewText(GENERATED_POS, " end ", XMLNode::EXACT)); + + XMLNode* xliff = create_string_node(res, "blah"); + + string oldString = res.value->ToString(XLIFF_NAMESPACES); + string newString = xliff->ToString(XLIFF_NAMESPACES); + + if (false) { + printf("OLD=\"%s\"\n", oldString.c_str()); + printf("NEW=\"%s\"\n", newString.c_str()); + } + + const char* const EXPECTED_OLD + = "<something> begin <b>b</b><i>i<b>b</b></i><u>u</u> end </something>"; + if (oldString != EXPECTED_OLD) { + fprintf(stderr, "oldString mismatch:\n"); + fprintf(stderr, " expected='%s'\n", EXPECTED_OLD); + fprintf(stderr, " actual='%s'\n", oldString.c_str()); + err |= 1; + } + + const char* const EXPECTED_NEW + = "<blah xml:space=\"preserve\"> begin <g ctype=\"bold\">b</g>" + "<g ctype=\"italic\">i<g ctype=\"bold\">b</g></g><g ctype=\"underline\">u</g>" + " end </blah>"; + if (newString != EXPECTED_NEW) { + fprintf(stderr, "newString mismatch:\n"); + fprintf(stderr, " expected='%s'\n", EXPECTED_NEW); + fprintf(stderr, " actual='%s'\n", newString.c_str()); + err |= 1; + } + + if (err != 0) { + fprintf(stderr, "create_string_node_test failed\n"); + } + return err; +} + +int +XLIFFFile_test() +{ + bool all = true; + int err = 0; + + if (all) err |= Parse_test(); + if (all) err |= create_string_node_test(); + + return err; +} + diff --git a/tools/localize/XMLHandler.cpp b/tools/localize/XMLHandler.cpp new file mode 100644 index 0000000..3fab211 --- /dev/null +++ b/tools/localize/XMLHandler.cpp @@ -0,0 +1,793 @@ +#include "XMLHandler.h" + +#include <algorithm> +#include <expat.h> +#include <stdio.h> +#include <string.h> +#include <fcntl.h> +#include <unistd.h> +#include <errno.h> + +#define NS_SEPARATOR 1 +#define MORE_INDENT " " + +static string +xml_text_escape(const string& s) +{ + string result; + const size_t N = s.length(); + for (size_t i=0; i<N; i++) { + char c = s[i]; + switch (c) { + case '<': + result += "<"; + break; + case '>': + result += ">"; + break; + case '&': + result += "&"; + break; + default: + result += c; + break; + } + } + return result; +} + +static string +xml_attr_escape(const string& s) +{ + string result; + const size_t N = s.length(); + for (size_t i=0; i<N; i++) { + char c = s[i]; + switch (c) { + case '\"': + result += """; + break; + default: + result += c; + break; + } + } + return result; +} + +XMLNamespaceMap::XMLNamespaceMap() +{ +} + +XMLNamespaceMap::XMLNamespaceMap(char const*const* nspaces) + +{ + while (*nspaces) { + m_map[nspaces[1]] = nspaces[0]; + nspaces += 2; + } +} + +string +XMLNamespaceMap::Get(const string& ns) const +{ + if (ns == "xml") { + return ns; + } + map<string,string>::const_iterator it = m_map.find(ns); + if (it == m_map.end()) { + return ""; + } else { + return it->second; + } +} + +string +XMLNamespaceMap::GetPrefix(const string& ns) const +{ + if (ns == "") { + return ""; + } + map<string,string>::const_iterator it = m_map.find(ns); + if (it != m_map.end()) { + if (it->second == "") { + return ""; + } else { + return it->second + ":"; + } + } else { + return ":"; // invalid + } +} + +void +XMLNamespaceMap::AddToAttributes(vector<XMLAttribute>* attrs) const +{ + map<string,string>::const_iterator it; + for (it=m_map.begin(); it!=m_map.end(); it++) { + if (it->second == "xml") { + continue; + } + XMLAttribute attr; + if (it->second == "") { + attr.name = "xmlns"; + } else { + attr.name = "xmlns:"; + attr.name += it->second; + } + attr.value = it->first; + attrs->push_back(attr); + } +} + +XMLAttribute::XMLAttribute() +{ +} + +XMLAttribute::XMLAttribute(const XMLAttribute& that) + :ns(that.ns), + name(that.name), + value(that.value) +{ +} + +XMLAttribute::XMLAttribute(string n, string na, string v) + :ns(n), + name(na), + value(v) +{ +} + +XMLAttribute::~XMLAttribute() +{ +} + +int +XMLAttribute::Compare(const XMLAttribute& that) const +{ + if (ns != that.ns) { + return ns < that.ns ? -1 : 1; + } + if (name != that.name) { + return name < that.name ? -1 : 1; + } + return 0; +} + +string +XMLAttribute::Find(const vector<XMLAttribute>& list, const string& ns, const string& name, + const string& def) +{ + const size_t N = list.size(); + for (size_t i=0; i<N; i++) { + const XMLAttribute& attr = list[i]; + if (attr.ns == ns && attr.name == name) { + return attr.value; + } + } + return def; +} + +struct xml_handler_data { + vector<XMLHandler*> stack; + XML_Parser parser; + vector<vector<XMLAttribute>*> attributes; + string filename; +}; + +XMLNode::XMLNode() +{ +} + +XMLNode::~XMLNode() +{ +// for_each(m_children.begin(), m_children.end(), delete_object<XMLNode>); +} + +XMLNode* +XMLNode::Clone() const +{ + switch (m_type) { + case ELEMENT: { + XMLNode* e = XMLNode::NewElement(m_pos, m_ns, m_name, m_attrs, m_pretty); + const size_t N = m_children.size(); + for (size_t i=0; i<N; i++) { + e->m_children.push_back(m_children[i]->Clone()); + } + return e; + } + case TEXT: { + return XMLNode::NewText(m_pos, m_text, m_pretty); + } + default: + return NULL; + } +} + +XMLNode* +XMLNode::NewElement(const SourcePos& pos, const string& ns, const string& name, + const vector<XMLAttribute>& attrs, int pretty) +{ + XMLNode* node = new XMLNode(); + node->m_type = ELEMENT; + node->m_pretty = pretty; + node->m_pos = pos; + node->m_ns = ns; + node->m_name = name; + node->m_attrs = attrs; + return node; +} + +XMLNode* +XMLNode::NewText(const SourcePos& pos, const string& text, int pretty) +{ + XMLNode* node = new XMLNode(); + node->m_type = TEXT; + node->m_pretty = pretty; + node->m_pos = pos; + node->m_text = text; + return node; +} + +void +XMLNode::SetPrettyRecursive(int value) +{ + m_pretty = value; + const size_t N = m_children.size(); + for (size_t i=0; i<N; i++) { + m_children[i]->SetPrettyRecursive(value); + } +} + +string +XMLNode::ContentsToString(const XMLNamespaceMap& nspaces) const +{ + return contents_to_string(nspaces, ""); +} + +string +XMLNode::ToString(const XMLNamespaceMap& nspaces) const +{ + return to_string(nspaces, ""); +} + +string +XMLNode::OpenTagToString(const XMLNamespaceMap& nspaces, int pretty) const +{ + return open_tag_to_string(nspaces, "", pretty); +} + +string +XMLNode::contents_to_string(const XMLNamespaceMap& nspaces, const string& indent) const +{ + string result; + const size_t N = m_children.size(); + for (size_t i=0; i<N; i++) { + const XMLNode* child = m_children[i]; + switch (child->Type()) { + case ELEMENT: + if (m_pretty == PRETTY) { + result += '\n'; + result += indent; + } + case TEXT: + result += child->to_string(nspaces, indent); + break; + } + } + return result; +} + +string +trim_string(const string& str) +{ + const char* p = str.c_str(); + while (*p && isspace(*p)) { + p++; + } + const char* q = str.c_str() + str.length() - 1; + while (q > p && isspace(*q)) { + q--; + } + q++; + return string(p, q-p); +} + +string +XMLNode::open_tag_to_string(const XMLNamespaceMap& nspaces, const string& indent, int pretty) const +{ + if (m_type != ELEMENT) { + return ""; + } + string result = "<"; + result += nspaces.GetPrefix(m_ns); + result += m_name; + + vector<XMLAttribute> attrs = m_attrs; + + sort(attrs.begin(), attrs.end()); + + const size_t N = attrs.size(); + for (size_t i=0; i<N; i++) { + const XMLAttribute& attr = attrs[i]; + if (i == 0 || m_pretty == EXACT || pretty == EXACT) { + result += ' '; + } + else { + result += "\n"; + result += indent; + result += MORE_INDENT; + result += MORE_INDENT; + } + result += nspaces.GetPrefix(attr.ns); + result += attr.name; + result += "=\""; + result += xml_attr_escape(attr.value); + result += '\"'; + } + + if (m_children.size() > 0) { + result += '>'; + } else { + result += " />"; + } + return result; +} + +string +XMLNode::to_string(const XMLNamespaceMap& nspaces, const string& indent) const +{ + switch (m_type) + { + case TEXT: { + if (m_pretty == EXACT) { + return xml_text_escape(m_text); + } else { + return xml_text_escape(trim_string(m_text)); + } + } + case ELEMENT: { + string result = open_tag_to_string(nspaces, indent, PRETTY); + + if (m_children.size() > 0) { + result += contents_to_string(nspaces, indent + MORE_INDENT); + + if (m_pretty == PRETTY && m_children.size() > 0) { + result += '\n'; + result += indent; + } + + result += "</"; + result += nspaces.GetPrefix(m_ns); + result += m_name; + result += '>'; + } + return result; + } + default: + return ""; + } +} + +string +XMLNode::CollapseTextContents() const +{ + if (m_type == TEXT) { + return m_text; + } + else if (m_type == ELEMENT) { + string result; + + const size_t N=m_children.size(); + for (size_t i=0; i<N; i++) { + result += m_children[i]->CollapseTextContents(); + } + + return result; + } + else { + return ""; + } +} + +vector<XMLNode*> +XMLNode::GetElementsByName(const string& ns, const string& name) const +{ + vector<XMLNode*> result; + const size_t N=m_children.size(); + for (size_t i=0; i<N; i++) { + XMLNode* child = m_children[i]; + if (child->m_type == ELEMENT && child->m_ns == ns && child->m_name == name) { + result.push_back(child); + } + } + return result; +} + +XMLNode* +XMLNode::GetElementByNameAt(const string& ns, const string& name, size_t index) const +{ + vector<XMLNode*> result; + const size_t N=m_children.size(); + for (size_t i=0; i<N; i++) { + XMLNode* child = m_children[i]; + if (child->m_type == ELEMENT && child->m_ns == ns && child->m_name == name) { + if (index == 0) { + return child; + } else { + index--; + } + } + } + return NULL; +} + +size_t +XMLNode::CountElementsByName(const string& ns, const string& name) const +{ + size_t result = 0; + const size_t N=m_children.size(); + for (size_t i=0; i<N; i++) { + XMLNode* child = m_children[i]; + if (child->m_type == ELEMENT && child->m_ns == ns && child->m_name == name) { + result++; + } + } + return result; +} + +string +XMLNode::GetAttribute(const string& ns, const string& name, const string& def) const +{ + return XMLAttribute::Find(m_attrs, ns, name, def); +} + +static void +parse_namespace(const char* data, string* ns, string* name) +{ + const char* p = strchr(data, NS_SEPARATOR); + if (p != NULL) { + ns->assign(data, p-data); + name->assign(p+1); + } else { + ns->assign(""); + name->assign(data); + } +} + +static void +convert_attrs(const char** in, vector<XMLAttribute>* out) +{ + while (*in) { + XMLAttribute attr; + parse_namespace(in[0], &attr.ns, &attr.name); + attr.value = in[1]; + out->push_back(attr); + in += 2; + } +} + +static bool +list_contains(const vector<XMLHandler*>& stack, XMLHandler* handler) +{ + const size_t N = stack.size(); + for (size_t i=0; i<N; i++) { + if (stack[i] == handler) { + return true; + } + } + return false; +} + +static void XMLCALL +start_element_handler(void *userData, const char *name, const char **attrs) +{ + xml_handler_data* data = (xml_handler_data*)userData; + + XMLHandler* handler = data->stack[data->stack.size()-1]; + + SourcePos pos(data->filename, (int)XML_GetCurrentLineNumber(data->parser)); + string nsString; + string nameString; + XMLHandler* next = handler; + vector<XMLAttribute> attributes; + + parse_namespace(name, &nsString, &nameString); + convert_attrs(attrs, &attributes); + + handler->OnStartElement(pos, nsString, nameString, attributes, &next); + + if (next == NULL) { + next = handler; + } + + if (next != handler) { + next->elementPos = pos; + next->elementNamespace = nsString; + next->elementName = nameString; + next->elementAttributes = attributes; + } + + data->stack.push_back(next); +} + +static void XMLCALL +end_element_handler(void *userData, const char *name) +{ + xml_handler_data* data = (xml_handler_data*)userData; + + XMLHandler* handler = data->stack[data->stack.size()-1]; + data->stack.pop_back(); + + SourcePos pos(data->filename, (int)XML_GetCurrentLineNumber(data->parser)); + + if (!list_contains(data->stack, handler)) { + handler->OnDone(pos); + if (data->stack.size() > 1) { + // not top one + delete handler; + } + } + + handler = data->stack[data->stack.size()-1]; + + string nsString; + string nameString; + + parse_namespace(name, &nsString, &nameString); + + handler->OnEndElement(pos, nsString, nameString); +} + +static void XMLCALL +text_handler(void *userData, const XML_Char *s, int len) +{ + xml_handler_data* data = (xml_handler_data*)userData; + XMLHandler* handler = data->stack[data->stack.size()-1]; + SourcePos pos(data->filename, (int)XML_GetCurrentLineNumber(data->parser)); + handler->OnText(pos, string(s, len)); +} + +static void XMLCALL +comment_handler(void *userData, const char *comment) +{ + xml_handler_data* data = (xml_handler_data*)userData; + XMLHandler* handler = data->stack[data->stack.size()-1]; + SourcePos pos(data->filename, (int)XML_GetCurrentLineNumber(data->parser)); + handler->OnComment(pos, string(comment)); +} + +bool +XMLHandler::ParseFile(const string& filename, XMLHandler* handler) +{ + char buf[16384]; + int fd = open(filename.c_str(), O_RDONLY); + if (fd < 0) { + SourcePos(filename, -1).Error("Unable to open file for read: %s", strerror(errno)); + return false; + } + + XML_Parser parser = XML_ParserCreateNS(NULL, NS_SEPARATOR); + xml_handler_data state; + state.stack.push_back(handler); + state.parser = parser; + state.filename = filename; + + XML_SetUserData(parser, &state); + XML_SetElementHandler(parser, start_element_handler, end_element_handler); + XML_SetCharacterDataHandler(parser, text_handler); + XML_SetCommentHandler(parser, comment_handler); + + ssize_t len; + bool done; + do { + len = read(fd, buf, sizeof(buf)); + done = len < (ssize_t)sizeof(buf); + if (len < 0) { + SourcePos(filename, -1).Error("Error reading file: %s\n", strerror(errno)); + close(fd); + return false; + } + if (XML_Parse(parser, buf, len, done) == XML_STATUS_ERROR) { + SourcePos(filename, (int)XML_GetCurrentLineNumber(parser)).Error( + "Error parsing XML: %s\n", XML_ErrorString(XML_GetErrorCode(parser))); + close(fd); + return false; + } + } while (!done); + + XML_ParserFree(parser); + + close(fd); + + return true; +} + +bool +XMLHandler::ParseString(const string& filename, const string& text, XMLHandler* handler) +{ + XML_Parser parser = XML_ParserCreateNS(NULL, NS_SEPARATOR); + xml_handler_data state; + state.stack.push_back(handler); + state.parser = parser; + state.filename = filename; + + XML_SetUserData(parser, &state); + XML_SetElementHandler(parser, start_element_handler, end_element_handler); + XML_SetCharacterDataHandler(parser, text_handler); + XML_SetCommentHandler(parser, comment_handler); + + if (XML_Parse(parser, text.c_str(), text.size(), true) == XML_STATUS_ERROR) { + SourcePos(filename, (int)XML_GetCurrentLineNumber(parser)).Error( + "Error parsing XML: %s\n", XML_ErrorString(XML_GetErrorCode(parser))); + return false; + } + + XML_ParserFree(parser); + + return true; +} + +XMLHandler::XMLHandler() +{ +} + +XMLHandler::~XMLHandler() +{ +} + +int +XMLHandler::OnStartElement(const SourcePos& pos, const string& ns, const string& name, + const vector<XMLAttribute>& attrs, XMLHandler** next) +{ + return 0; +} + +int +XMLHandler::OnEndElement(const SourcePos& pos, const string& ns, const string& name) +{ + return 0; +} + +int +XMLHandler::OnText(const SourcePos& pos, const string& text) +{ + return 0; +} + +int +XMLHandler::OnComment(const SourcePos& pos, const string& text) +{ + return 0; +} + +int +XMLHandler::OnDone(const SourcePos& pos) +{ + return 0; +} + +TopElementHandler::TopElementHandler(const string& ns, const string& name, XMLHandler* next) + :m_ns(ns), + m_name(name), + m_next(next) +{ +} + +int +TopElementHandler::OnStartElement(const SourcePos& pos, const string& ns, const string& name, + const vector<XMLAttribute>& attrs, XMLHandler** next) +{ + *next = m_next; + return 0; +} + +int +TopElementHandler::OnEndElement(const SourcePos& pos, const string& ns, const string& name) +{ + return 0; +} + +int +TopElementHandler::OnText(const SourcePos& pos, const string& text) +{ + return 0; +} + +int +TopElementHandler::OnDone(const SourcePos& pos) +{ + return 0; +} + + +NodeHandler::NodeHandler(XMLNode* root, int pretty) + :m_root(root), + m_pretty(pretty) +{ + if (root != NULL) { + m_nodes.push_back(root); + } +} + +NodeHandler::~NodeHandler() +{ +} + +int +NodeHandler::OnStartElement(const SourcePos& pos, const string& ns, const string& name, + const vector<XMLAttribute>& attrs, XMLHandler** next) +{ + int pretty; + if (XMLAttribute::Find(attrs, XMLNS_XMLNS, "space", "") == "preserve") { + pretty = XMLNode::EXACT; + } else { + if (m_root == NULL) { + pretty = m_pretty; + } else { + pretty = m_nodes[m_nodes.size()-1]->Pretty(); + } + } + XMLNode* n = XMLNode::NewElement(pos, ns, name, attrs, pretty); + if (m_root == NULL) { + m_root = n; + } else { + m_nodes[m_nodes.size()-1]->EditChildren().push_back(n); + } + m_nodes.push_back(n); + return 0; +} + +int +NodeHandler::OnEndElement(const SourcePos& pos, const string& ns, const string& name) +{ + m_nodes.pop_back(); + return 0; +} + +int +NodeHandler::OnText(const SourcePos& pos, const string& text) +{ + if (m_root == NULL) { + return 1; + } + XMLNode* n = XMLNode::NewText(pos, text, m_nodes[m_nodes.size()-1]->Pretty()); + m_nodes[m_nodes.size()-1]->EditChildren().push_back(n); + return 0; +} + +int +NodeHandler::OnComment(const SourcePos& pos, const string& text) +{ + return 0; +} + +int +NodeHandler::OnDone(const SourcePos& pos) +{ + return 0; +} + +XMLNode* +NodeHandler::ParseFile(const string& filename, int pretty) +{ + NodeHandler handler(NULL, pretty); + if (!XMLHandler::ParseFile(filename, &handler)) { + fprintf(stderr, "error parsing file: %s\n", filename.c_str()); + return NULL; + } + return handler.Root(); +} + +XMLNode* +NodeHandler::ParseString(const string& filename, const string& text, int pretty) +{ + NodeHandler handler(NULL, pretty); + if (!XMLHandler::ParseString(filename, text, &handler)) { + fprintf(stderr, "error parsing file: %s\n", filename.c_str()); + return NULL; + } + return handler.Root(); +} + + diff --git a/tools/localize/XMLHandler.h b/tools/localize/XMLHandler.h new file mode 100644 index 0000000..1130710 --- /dev/null +++ b/tools/localize/XMLHandler.h @@ -0,0 +1,197 @@ +#ifndef XML_H +#define XML_H + +#include "SourcePos.h" + +#include <string> +#include <vector> +#include <map> + +#define XMLNS_XMLNS "http://www.w3.org/XML/1998/namespace" + +using namespace std; + +string trim_string(const string& str); + +struct XMLAttribute +{ + string ns; + string name; + string value; + + XMLAttribute(); + XMLAttribute(const XMLAttribute& that); + XMLAttribute(string ns, string name, string value); + ~XMLAttribute(); + + int Compare(const XMLAttribute& that) const; + + inline bool operator<(const XMLAttribute& that) const { return Compare(that) < 0; } + inline bool operator<=(const XMLAttribute& that) const { return Compare(that) <= 0; } + inline bool operator==(const XMLAttribute& that) const { return Compare(that) == 0; } + inline bool operator!=(const XMLAttribute& that) const { return Compare(that) != 0; } + inline bool operator>=(const XMLAttribute& that) const { return Compare(that) >= 0; } + inline bool operator>(const XMLAttribute& that) const { return Compare(that) > 0; } + + static string Find(const vector<XMLAttribute>& list, + const string& ns, const string& name, const string& def); +}; + +class XMLNamespaceMap +{ +public: + XMLNamespaceMap(); + XMLNamespaceMap(char const*const* nspaces); + string Get(const string& ns) const; + string GetPrefix(const string& ns) const; + void AddToAttributes(vector<XMLAttribute>* attrs) const; +private: + map<string,string> m_map; +}; + +struct XMLNode +{ +public: + enum { + EXACT = 0, + PRETTY = 1 + }; + + enum { + ELEMENT = 0, + TEXT = 1 + }; + + static XMLNode* NewElement(const SourcePos& pos, const string& ns, const string& name, + const vector<XMLAttribute>& attrs, int pretty); + static XMLNode* NewText(const SourcePos& pos, const string& text, int pretty); + + ~XMLNode(); + + // a deep copy + XMLNode* Clone() const; + + inline int Type() const { return m_type; } + inline int Pretty() const { return m_pretty; } + void SetPrettyRecursive(int value); + string ContentsToString(const XMLNamespaceMap& nspaces) const; + string ToString(const XMLNamespaceMap& nspaces) const; + string OpenTagToString(const XMLNamespaceMap& nspaces, int pretty) const; + + string CollapseTextContents() const; + + inline const SourcePos& Position() const { return m_pos; } + + // element + inline string Namespace() const { return m_ns; } + inline string Name() const { return m_name; } + inline void SetName(const string& ns, const string& n) { m_ns = ns; m_name = n; } + inline const vector<XMLAttribute>& Attributes() const { return m_attrs; } + inline vector<XMLAttribute>& EditAttributes() { return m_attrs; } + inline const vector<XMLNode*>& Children() const { return m_children; } + inline vector<XMLNode*>& EditChildren() { return m_children; } + vector<XMLNode*> GetElementsByName(const string& ns, const string& name) const; + XMLNode* GetElementByNameAt(const string& ns, const string& name, size_t index) const; + size_t CountElementsByName(const string& ns, const string& name) const; + string GetAttribute(const string& ns, const string& name, const string& def) const; + + // text + inline string Text() const { return m_text; } + +private: + XMLNode(); + XMLNode(const XMLNode&); + + string contents_to_string(const XMLNamespaceMap& nspaces, const string& indent) const; + string to_string(const XMLNamespaceMap& nspaces, const string& indent) const; + string open_tag_to_string(const XMLNamespaceMap& nspaces, const string& indent, + int pretty) const; + + int m_type; + int m_pretty; + SourcePos m_pos; + + // element + string m_ns; + string m_name; + vector<XMLAttribute> m_attrs; + vector<XMLNode*> m_children; + + // text + string m_text; +}; + +class XMLHandler +{ +public: + // information about the element that started us + SourcePos elementPos; + string elementNamespace; + string elementName; + vector<XMLAttribute> elementAttributes; + + XMLHandler(); + virtual ~XMLHandler(); + + XMLHandler* parent; + + virtual int OnStartElement(const SourcePos& pos, const string& ns, const string& name, + const vector<XMLAttribute>& attrs, XMLHandler** next); + virtual int OnEndElement(const SourcePos& pos, const string& ns, const string& name); + virtual int OnText(const SourcePos& pos, const string& text); + virtual int OnComment(const SourcePos& pos, const string& text); + virtual int OnDone(const SourcePos& pos); + + static bool ParseFile(const string& filename, XMLHandler* handler); + static bool ParseString(const string& filename, const string& text, XMLHandler* handler); +}; + +class TopElementHandler : public XMLHandler +{ +public: + TopElementHandler(const string& ns, const string& name, XMLHandler* next); + + virtual int OnStartElement(const SourcePos& pos, const string& ns, const string& name, + const vector<XMLAttribute>& attrs, XMLHandler** next); + virtual int OnEndElement(const SourcePos& pos, const string& ns, const string& name); + virtual int OnText(const SourcePos& pos, const string& text); + virtual int OnDone(const SourcePos& endPos); + +private: + string m_ns; + string m_name; + XMLHandler* m_next; +}; + +class NodeHandler : public XMLHandler +{ +public: + // after it's done, you own everything created and added to root + NodeHandler(XMLNode* root, int pretty); + ~NodeHandler(); + + virtual int OnStartElement(const SourcePos& pos, const string& ns, const string& name, + const vector<XMLAttribute>& attrs, XMLHandler** next); + virtual int OnEndElement(const SourcePos& pos, const string& ns, const string& name); + virtual int OnText(const SourcePos& pos, const string& text); + virtual int OnComment(const SourcePos& pos, const string& text); + virtual int OnDone(const SourcePos& endPos); + + inline XMLNode* Root() const { return m_root; } + + static XMLNode* ParseFile(const string& filename, int pretty); + static XMLNode* ParseString(const string& filename, const string& text, int pretty); + +private: + XMLNode* m_root; + int m_pretty; + vector<XMLNode*> m_nodes; +}; + +template <class T> +static void delete_object(T* obj) +{ + delete obj; +} + +#endif // XML_H diff --git a/tools/localize/XMLHandler_test.cpp b/tools/localize/XMLHandler_test.cpp new file mode 100644 index 0000000..1c81c0c --- /dev/null +++ b/tools/localize/XMLHandler_test.cpp @@ -0,0 +1,133 @@ +#include "XMLHandler.h" +#include <stdio.h> +#include <unistd.h> +#include <fcntl.h> + +const char *const NS_MAP[] = { + "xml", XMLNS_XMLNS, + NULL, NULL +}; + +const XMLNamespaceMap NO_NAMESPACES(NS_MAP); + +char const*const EXPECTED_EXACT = + "<ASDF>\n" + " <a id=\"system\" old-cl=\"1\" new-cl=\"43019\">\n" + " <app dir=\"apps/common\" />\n" + " </a>\n" + " <a id=\"samples\" old-cl=\"1\" new-cl=\"43019\">asdf\n" + " <app dir=\"samples/NotePad\" />\n" + " <app dir=\"samples/LunarLander\" />\n" + " <something>a<b>,</b>b </something>\n" + " <exact xml:space=\"preserve\">a<b>,</b>b </exact>\n" + " </a>\n" + "</ASDF>\n"; + +char const*const EXPECTED_PRETTY = + "<ASDF>\n" + " <a id=\"system\"\n" + " old-cl=\"1\"\n" + " new-cl=\"43019\">\n" + " <app dir=\"apps/common\" />\n" + " </a>\n" + " <a id=\"samples\"\n" + " old-cl=\"1\"\n" + " new-cl=\"43019\">asdf\n" + " <app dir=\"samples/NotePad\" />\n" + " <app dir=\"samples/LunarLander\" />\n" + " <something>a\n" + " <b>,\n" + " </b>b \n" + " </something>\n" + " <exact xml:space=\"preserve\">a<b>,</b>b </exact>\n" + " </a>\n" + "</ASDF>\n"; + +static string +read_file(const string& filename) +{ + char buf[1024]; + int fd = open(filename.c_str(), O_RDONLY); + if (fd < 0) { + return ""; + } + string result; + while (true) { + ssize_t len = read(fd, buf, sizeof(buf)-1); + buf[len] = '\0'; + if (len <= 0) { + break; + } + result.append(buf, len); + } + close(fd); + return result; +} + +static int +ParseFile_EXACT_test() +{ + XMLNode* root = NodeHandler::ParseFile("testdata/xml.xml", XMLNode::EXACT); + if (root == NULL) { + return 1; + } + string result = root->ToString(NO_NAMESPACES); + delete root; + //printf("[[%s]]\n", result.c_str()); + return result == EXPECTED_EXACT; +} + +static int +ParseFile_PRETTY_test() +{ + XMLNode* root = NodeHandler::ParseFile("testdata/xml.xml", XMLNode::PRETTY); + if (root == NULL) { + return 1; + } + string result = root->ToString(NO_NAMESPACES); + delete root; + //printf("[[%s]]\n", result.c_str()); + return result == EXPECTED_PRETTY; +} + +static int +ParseString_EXACT_test() +{ + string text = read_file("testdata/xml.xml"); + XMLNode* root = NodeHandler::ParseString("testdata/xml.xml", text, XMLNode::EXACT); + if (root == NULL) { + return 1; + } + string result = root->ToString(NO_NAMESPACES); + delete root; + //printf("[[%s]]\n", result.c_str()); + return result == EXPECTED_EXACT; +} + +static int +ParseString_PRETTY_test() +{ + string text = read_file("testdata/xml.xml"); + XMLNode* root = NodeHandler::ParseString("testdata/xml.xml", text, XMLNode::PRETTY); + if (root == NULL) { + return 1; + } + string result = root->ToString(NO_NAMESPACES); + delete root; + //printf("[[%s]]\n", result.c_str()); + return result == EXPECTED_PRETTY; +} + +int +XMLHandler_test() +{ + int err = 0; + bool all = true; + + if (all) err |= ParseFile_EXACT_test(); + if (all) err |= ParseFile_PRETTY_test(); + if (all) err |= ParseString_EXACT_test(); + if (all) err |= ParseString_PRETTY_test(); + + return err; +} diff --git a/tools/localize/XMLNode.h b/tools/localize/XMLNode.h new file mode 100644 index 0000000..bfb9f55 --- /dev/null +++ b/tools/localize/XMLNode.h @@ -0,0 +1,19 @@ +#ifndef XMLNODE_H +#define XMLNODE_H + +#include <string> + +using namespace std; + +struct XMLAttribute +{ + string ns; + string name; + string value; + + static string Find(const vector<XMLAttribute>& list, + const string& ns, const string& name, const string& def); +}; + + +#endif // XMLNODE_H diff --git a/tools/localize/file_utils.cpp b/tools/localize/file_utils.cpp new file mode 100644 index 0000000..bb82a9c --- /dev/null +++ b/tools/localize/file_utils.cpp @@ -0,0 +1,143 @@ +#include <string.h> +#include <stdlib.h> +#include <unistd.h> +#include "file_utils.h" +#include "Perforce.h" +#include <sys/fcntl.h> +#include <sys/stat.h> +#include <errno.h> +#include <host/Directories.h> +#include "log.h" + +string +translated_file_name(const string& file, const string& locale) +{ + const char* str = file.c_str(); + const char* p = str + file.length(); + const char* rest = NULL; + const char* values = p; + + while (p > str) { + p--; + if (*p == '/') { + rest = values; + values = p; + if (0 == strncmp("values", values+1, rest-values-1)) { + break; + } + } + } + values++; + + string result(str, values-str); + result.append(values, rest-values); + + string language, region; + if (locale == "") { + language = ""; + region = ""; + } + else if (!split_locale(locale, &language, ®ion)) { + return ""; + } + + if (language != "") { + result += '-'; + result += language; + } + if (region != "") { + result += "-r"; + result += region; + } + + result += rest; + + return result; +} + +ValuesFile* +get_values_file(const string& filename, const Configuration& configuration, + int version, const string& versionString, bool printOnFailure) +{ + int err; + string text; + + log_printf("get_values_file filename=%s\n", filename.c_str()); + err = Perforce::GetFile(filename, versionString, &text, printOnFailure); + if (err != 0 || text == "") { + return NULL; + } + + ValuesFile* result = ValuesFile::ParseString(filename, text, configuration, version, + versionString); + if (result == NULL) { + fprintf(stderr, "unable to parse file: %s\n", filename.c_str()); + exit(1); + } + return result; +} + +ValuesFile* +get_local_values_file(const string& filename, const Configuration& configuration, + int version, const string& versionString, bool printOnFailure) +{ + int err; + string text; + char buf[2049]; + int fd; + ssize_t amt; + + fd = open(filename.c_str(), O_RDONLY); + if (fd == -1) { + fprintf(stderr, "unable to open file: %s\n", filename.c_str()); + return NULL; + } + + while ((amt = read(fd, buf, sizeof(buf)-1)) > 0) { + text.append(buf, amt); + } + + close(fd); + + if (text == "") { + return NULL; + } + + ValuesFile* result = ValuesFile::ParseString(filename, text, configuration, version, + versionString); + if (result == NULL) { + fprintf(stderr, "unable to parse file: %s\n", filename.c_str()); + exit(1); + } + return result; +} + +void +print_file_status(size_t j, size_t J, const string& message) +{ + printf("\r%s file %zd of %zd...", message.c_str(), j, J); + fflush(stdout); +} + +int +write_to_file(const string& filename, const string& text) +{ + mkdirs(parent_dir(filename).c_str()); + int fd = open(filename.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0666); + if (fd < 0) { + fprintf(stderr, "unable to open file for write (%s): %s\n", strerror(errno), + filename.c_str()); + return -1; + } + + ssize_t amt = write(fd, text.c_str(), text.length()); + + close(fd); + + if (amt < 0) { + return amt; + } + return amt == (ssize_t)text.length() ? 0 : -1; +} + + diff --git a/tools/localize/file_utils.h b/tools/localize/file_utils.h new file mode 100644 index 0000000..3b3fa21 --- /dev/null +++ b/tools/localize/file_utils.h @@ -0,0 +1,21 @@ +#ifndef FILE_UTILS_H +#define FILE_UTILS_H + +#include "ValuesFile.h" +#include "Configuration.h" +#include <string> + +using namespace std; + +string translated_file_name(const string& file, const string& locale); + +ValuesFile* get_values_file(const string& filename, const Configuration& configuration, + int version, const string& versionString, bool printOnFailure); +ValuesFile* get_local_values_file(const string& filename, const Configuration& configuration, + int version, const string& versionString, bool printOnFailure); + +void print_file_status(size_t j, size_t J, const string& message = "Reading"); +int write_to_file(const string& filename, const string& text); + + +#endif // FILE_UTILS_H diff --git a/tools/localize/localize.cpp b/tools/localize/localize.cpp new file mode 100644 index 0000000..c0d84cc --- /dev/null +++ b/tools/localize/localize.cpp @@ -0,0 +1,767 @@ +#include "SourcePos.h" +#include "ValuesFile.h" +#include "XLIFFFile.h" +#include "Perforce.h" +#include "merge_res_and_xliff.h" +#include "localize.h" +#include "file_utils.h" +#include "res_check.h" +#include "xmb.h" + +#include <host/pseudolocalize.h> + +#include <stdlib.h> +#include <stdarg.h> +#include <sstream> +#include <stdio.h> +#include <string.h> + +using namespace std; + +FILE* g_logFile = NULL; + +int test(); + +int +read_settings(const string& filename, map<string,Settings>* result, const string& rootDir) +{ + XMLNode* root = NodeHandler::ParseFile(filename, XMLNode::PRETTY); + if (root == NULL) { + SourcePos(filename, -1).Error("Error reading file."); + return 1; + } + + // <configuration> + vector<XMLNode*> configNodes = root->GetElementsByName("", "configuration"); + const size_t I = configNodes.size(); + for (size_t i=0; i<I; i++) { + const XMLNode* configNode = configNodes[i]; + + Settings settings; + settings.id = configNode->GetAttribute("", "id", ""); + if (settings.id == "") { + configNode->Position().Error("<configuration> needs an id attribute."); + delete root; + return 1; + } + + settings.oldVersion = configNode->GetAttribute("", "old-cl", ""); + + settings.currentVersion = configNode->GetAttribute("", "new-cl", ""); + if (settings.currentVersion == "") { + configNode->Position().Error("<configuration> needs a new-cl attribute."); + delete root; + return 1; + } + + // <app> + vector<XMLNode*> appNodes = configNode->GetElementsByName("", "app"); + + const size_t J = appNodes.size(); + for (size_t j=0; j<J; j++) { + const XMLNode* appNode = appNodes[j]; + + string dir = appNode->GetAttribute("", "dir", ""); + if (dir == "") { + appNode->Position().Error("<app> needs a dir attribute."); + delete root; + return 1; + } + + settings.apps.push_back(dir); + } + + // <reject> + vector<XMLNode*> rejectNodes = configNode->GetElementsByName("", "reject"); + + const size_t K = rejectNodes.size(); + for (size_t k=0; k<K; k++) { + const XMLNode* rejectNode = rejectNodes[k]; + + Reject reject; + + reject.file = rejectNode->GetAttribute("", "file", ""); + if (reject.file == "") { + rejectNode->Position().Error("<reject> needs a file attribute."); + delete root; + return 1; + } + string f = reject.file; + reject.file = rootDir; + reject.file += '/'; + reject.file += f; + + reject.name = rejectNode->GetAttribute("", "name", ""); + if (reject.name == "") { + rejectNode->Position().Error("<reject> needs a name attribute."); + delete root; + return 1; + } + + reject.comment = trim_string(rejectNode->CollapseTextContents()); + + settings.reject.push_back(reject); + } + + (*result)[settings.id] = settings; + } + + delete root; + return 0; +} + + +static void +ValuesFile_to_XLIFFFile(const ValuesFile* values, XLIFFFile* xliff, const string& englishFilename) +{ + const set<StringResource>& strings = values->GetStrings(); + for (set<StringResource>::const_iterator it=strings.begin(); it!=strings.end(); it++) { + StringResource res = *it; + res.file = englishFilename; + xliff->AddStringResource(res); + } +} + +static bool +contains_reject(const Settings& settings, const string& file, const TransUnit& tu) +{ + const string name = tu.id; + const vector<Reject>& reject = settings.reject; + const size_t I = reject.size(); + for (size_t i=0; i<I; i++) { + const Reject& r = reject[i]; + if (r.file == file && r.name == name) { + return true; + } + } + return false; +} + +/** + * If it's been rejected, then we keep whatever info we have. + * + * Implements this truth table: + * + * S AT AS Keep + * ----------------------- + * 0 0 0 0 (this case can't happen) + * 0 0 1 0 (it was there, never translated, and removed) + * 0 1 0 0 (somehow it got translated, but it was removed) + * 0 1 1 0 (it was removed after having been translated) + * + * 1 0 0 1 (it was just added) + * 1 0 1 1 (it was added, has been changed, but it never got translated) + * 1 1 0 1 (somehow it got translated, but we don't know based on what) + * 1 1 1 0/1 (it's in both. 0 if S=AS b/c there's no need to retranslate if they're + * the same. 1 if S!=AS because S changed, so it should be retranslated) + * + * The first four are cases where, whatever happened in the past, the string isn't there + * now, so it shouldn't be in the XLIFF file. + * + * For cases 4 and 5, the string has never been translated, so get it translated. + * + * For case 6, it's unclear where the translated version came from, so we're conservative + * and send it back for them to have another shot at. + * + * For case 7, we have some data. We have two choices. We could rely on the translator's + * translation memory or tools to notice that the strings haven't changed, and populate the + * <target> field themselves. Or if the string hasn't changed since last time, we can just + * not even tell them about it. As the project nears the end, it will be convenient to see + * the xliff files reducing in size, so we pick the latter. Obviously, if the string has + * changed, then we need to get it retranslated. + */ +bool +keep_this_trans_unit(const string& file, const TransUnit& unit, void* cookie) +{ + const Settings* settings = reinterpret_cast<const Settings*>(cookie); + + if (contains_reject(*settings, file, unit)) { + return true; + } + + if (unit.source.id == "") { + return false; + } + if (unit.altTarget.id == "" || unit.altSource.id == "") { + return true; + } + return unit.source.value->ContentsToString(XLIFF_NAMESPACES) + != unit.altSource.value->ContentsToString(XLIFF_NAMESPACES); +} + +int +validate_config(const string& settingsFile, const map<string,Settings>& settings, + const string& config) +{ + if (settings.find(config) == settings.end()) { + SourcePos(settingsFile, -1).Error("settings file does not contain setting: %s\n", + config.c_str()); + return 1; + } + return 0; +} + +int +validate_configs(const string& settingsFile, const map<string,Settings>& settings, + const vector<string>& configs) +{ + int err = 0; + for (size_t i=0; i<configs.size(); i++) { + string config = configs[i]; + err |= validate_config(settingsFile, settings, config); + } + return err; +} + +int +select_files(vector<string> *resFiles, const string& config, + const map<string,Settings>& settings, const string& rootDir) +{ + int err; + vector<vector<string> > allResFiles; + vector<string> configs; + configs.push_back(config); + err = select_files(&allResFiles, configs, settings, rootDir); + if (err == 0) { + *resFiles = allResFiles[0]; + } + return err; +} + +int +select_files(vector<vector<string> > *allResFiles, const vector<string>& configs, + const map<string,Settings>& settings, const string& rootDir) +{ + int err; + printf("Selecting files..."); + fflush(stdout); + + for (size_t i=0; i<configs.size(); i++) { + const string& config = configs[i]; + const Settings& setting = settings.find(config)->second; + + vector<string> resFiles; + err = Perforce::GetResourceFileNames(setting.currentVersion, rootDir, + setting.apps, &resFiles, true); + if (err != 0) { + fprintf(stderr, "error with perforce. bailing\n"); + return err; + } + + allResFiles->push_back(resFiles); + } + return 0; +} + +static int +do_export(const string& settingsFile, const string& rootDir, const string& outDir, + const string& targetLocale, const vector<string>& configs) +{ + bool success = true; + int err; + + if (false) { + printf("settingsFile=%s\n", settingsFile.c_str()); + printf("rootDir=%s\n", rootDir.c_str()); + printf("outDir=%s\n", outDir.c_str()); + for (size_t i=0; i<configs.size(); i++) { + printf("config[%zd]=%s\n", i, configs[i].c_str()); + } + } + + map<string,Settings> settings; + err = read_settings(settingsFile, &settings, rootDir); + if (err != 0) { + return err; + } + + err = validate_configs(settingsFile, settings, configs); + if (err != 0) { + return err; + } + + vector<vector<string> > allResFiles; + err = select_files(&allResFiles, configs, settings, rootDir); + if (err != 0) { + return err; + } + + size_t totalFileCount = 0; + for (size_t i=0; i<allResFiles.size(); i++) { + totalFileCount += allResFiles[i].size(); + } + totalFileCount *= 3; // we try all 3 versions of the file + + size_t fileProgress = 0; + vector<Stats> stats; + vector<pair<string,XLIFFFile*> > xliffs; + + for (size_t i=0; i<configs.size(); i++) { + const string& config = configs[i]; + const Settings& setting = settings[config]; + + if (false) { + fprintf(stderr, "Configuration: %s (%zd of %zd)\n", config.c_str(), i+1, + configs.size()); + fprintf(stderr, " Old CL: %s\n", setting.oldVersion.c_str()); + fprintf(stderr, " Current CL: %s\n", setting.currentVersion.c_str()); + } + + Configuration english; + english.locale = "en_US"; + Configuration translated; + translated.locale = targetLocale; + XLIFFFile* xliff = XLIFFFile::Create(english, translated, setting.currentVersion); + + const vector<string>& resFiles = allResFiles[i]; + const size_t J = resFiles.size(); + for (size_t j=0; j<J; j++) { + string resFile = resFiles[j]; + + // parse the files into a ValuesFile + // pull out the strings and add them to the XLIFFFile + + // current file + print_file_status(++fileProgress, totalFileCount); + ValuesFile* currentFile = get_values_file(resFile, english, CURRENT_VERSION, + setting.currentVersion, true); + if (currentFile != NULL) { + ValuesFile_to_XLIFFFile(currentFile, xliff, resFile); + //printf("currentFile=[%s]\n", currentFile->ToString().c_str()); + } else { + fprintf(stderr, "error reading file %s@%s\n", resFile.c_str(), + setting.currentVersion.c_str()); + success = false; + } + + // old file + print_file_status(++fileProgress, totalFileCount); + ValuesFile* oldFile = get_values_file(resFile, english, OLD_VERSION, + setting.oldVersion, false); + if (oldFile != NULL) { + ValuesFile_to_XLIFFFile(oldFile, xliff, resFile); + //printf("oldFile=[%s]\n", oldFile->ToString().c_str()); + } + + // translated version + // (get the head of the tree for the most recent translation, but it's considered + // the old one because the "current" one hasn't been made yet, and this goes into + // the <alt-trans> tag if necessary + print_file_status(++fileProgress, totalFileCount); + string transFilename = translated_file_name(resFile, targetLocale); + ValuesFile* transFile = get_values_file(transFilename, translated, OLD_VERSION, + setting.currentVersion, false); + if (transFile != NULL) { + ValuesFile_to_XLIFFFile(transFile, xliff, resFile); + } + + delete currentFile; + delete oldFile; + delete transFile; + } + + Stats beforeFilterStats = xliff->GetStats(config); + + // run through the XLIFFFile and strip out TransUnits that have identical + // old and current source values and are not in the reject list, or just + // old values and no source values + xliff->Filter(keep_this_trans_unit, (void*)&setting); + + Stats afterFilterStats = xliff->GetStats(config); + afterFilterStats.totalStrings = beforeFilterStats.totalStrings; + + // add the reject comments + for (vector<Reject>::const_iterator reject = setting.reject.begin(); + reject != setting.reject.end(); reject++) { + TransUnit* tu = xliff->EditTransUnit(reject->file, reject->name); + tu->rejectComment = reject->comment; + } + + // config-locale-current_cl.xliff + stringstream filename; + if (outDir != "") { + filename << outDir << '/'; + } + filename << config << '-' << targetLocale << '-' << setting.currentVersion << ".xliff"; + xliffs.push_back(pair<string,XLIFFFile*>(filename.str(), xliff)); + + stats.push_back(afterFilterStats); + } + + // today is a good day to die + if (!success || SourcePos::HasErrors()) { + return 1; + } + + // write the XLIFF files + printf("\nWriting %zd file%s...\n", xliffs.size(), xliffs.size() == 1 ? "" : "s"); + for (vector<pair<string,XLIFFFile*> >::iterator it = xliffs.begin(); it != xliffs.end(); it++) { + const string& filename = it->first; + XLIFFFile* xliff = it->second; + string text = xliff->ToString(); + write_to_file(filename, text); + } + + // the stats + printf("\n" + " to without total\n" + " config files translate comments strings\n" + "-----------------------------------------------------------------------\n"); + Stats totals; + totals.config = "total"; + totals.files = 0; + totals.toBeTranslated = 0; + totals.noComments = 0; + totals.totalStrings = 0; + for (vector<Stats>::iterator it=stats.begin(); it!=stats.end(); it++) { + string cfg = it->config; + if (cfg.length() > 20) { + cfg.resize(20); + } + printf(" %-20s %-9zd %-9zd %-9zd %-19zd\n", cfg.c_str(), it->files, + it->toBeTranslated, it->noComments, it->totalStrings); + totals.files += it->files; + totals.toBeTranslated += it->toBeTranslated; + totals.noComments += it->noComments; + totals.totalStrings += it->totalStrings; + } + if (stats.size() > 1) { + printf("-----------------------------------------------------------------------\n" + " %-20s %-9zd %-9zd %-9zd %-19zd\n", totals.config.c_str(), totals.files, + totals.toBeTranslated, totals.noComments, totals.totalStrings); + } + printf("\n"); + return 0; +} + +struct PseudolocalizeSettings { + XLIFFFile* xliff; + bool expand; +}; + + +string +pseudolocalize_string(const string& source, const PseudolocalizeSettings* settings) +{ + return pseudolocalize_string(source); +} + +static XMLNode* +pseudolocalize_xml_node(const XMLNode* source, const PseudolocalizeSettings* settings) +{ + if (source->Type() == XMLNode::TEXT) { + return XMLNode::NewText(source->Position(), pseudolocalize_string(source->Text(), settings), + source->Pretty()); + } else { + XMLNode* target; + if (source->Namespace() == XLIFF_XMLNS && source->Name() == "g") { + // XXX don't translate these + target = XMLNode::NewElement(source->Position(), source->Namespace(), + source->Name(), source->Attributes(), source->Pretty()); + } else { + target = XMLNode::NewElement(source->Position(), source->Namespace(), + source->Name(), source->Attributes(), source->Pretty()); + } + + const vector<XMLNode*>& children = source->Children(); + const size_t I = children.size(); + for (size_t i=0; i<I; i++) { + target->EditChildren().push_back(pseudolocalize_xml_node(children[i], settings)); + } + + return target; + } +} + +void +pseudolocalize_trans_unit(const string&file, TransUnit* unit, void* cookie) +{ + const PseudolocalizeSettings* settings = (PseudolocalizeSettings*)cookie; + + const StringResource& source = unit->source; + StringResource* target = &unit->target; + *target = source; + + target->config = settings->xliff->TargetConfig(); + + delete target->value; + target->value = pseudolocalize_xml_node(source.value, settings); +} + +int +pseudolocalize_xliff(XLIFFFile* xliff, bool expand) +{ + PseudolocalizeSettings settings; + + settings.xliff = xliff; + settings.expand = expand; + xliff->Map(pseudolocalize_trans_unit, &settings); + return 0; +} + +static int +do_pseudo(const string& infile, const string& outfile, bool expand) +{ + int err; + + XLIFFFile* xliff = XLIFFFile::Parse(infile); + if (xliff == NULL) { + return 1; + } + + pseudolocalize_xliff(xliff, expand); + + err = write_to_file(outfile, xliff->ToString()); + + delete xliff; + + return err; +} + +void +log_printf(const char *fmt, ...) +{ + int ret; + va_list ap; + + if (g_logFile != NULL) { + va_start(ap, fmt); + ret = vfprintf(g_logFile, fmt, ap); + va_end(ap); + fflush(g_logFile); + } +} + +void +close_log_file() +{ + if (g_logFile != NULL) { + fclose(g_logFile); + } +} + +void +open_log_file(const char* file) +{ + g_logFile = fopen(file, "w"); + printf("log file: %s -- %p\n", file, g_logFile); + atexit(close_log_file); +} + +static int +usage() +{ + fprintf(stderr, + "usage: localize export OPTIONS CONFIGS...\n" + " REQUIRED OPTIONS\n" + " --settings SETTINGS The settings file to use. See CONFIGS below.\n" + " --root TREE_ROOT The location in Perforce of the files. e.g. //device\n" + " --target LOCALE The target locale. See LOCALES below.\n" + "\n" + " OPTIONAL OPTIONS\n" + " --out DIR Directory to put the output files. Defaults to the\n" + " current directory if not supplied. Files are\n" + " named as follows:\n" + " CONFIG-LOCALE-CURRENT_CL.xliff\n" + "\n" + "\n" + "usage: localize import XLIFF_FILE...\n" + "\n" + "Import a translated XLIFF file back into the tree.\n" + "\n" + "\n" + "usage: localize xlb XMB_FILE VALUES_FILES...\n" + "\n" + "Read resource files from the tree file and write the corresponding XLB file\n" + "\n" + "Supply all of the android resource files (values files) to export after that.\n" + "\n" + "\n" + "\n" + "CONFIGS\n" + "\n" + "LOCALES\n" + "Locales are specified in the form en_US They will be processed correctly\n" + "to locate the resouce files in the tree.\n" + "\n" + "\n" + "usage: localize pseudo OPTIONS INFILE [OUTFILE]\n" + " OPTIONAL OPTIONS\n" + " --big Pad strings so they get longer.\n" + "\n" + "Read INFILE, an XLIFF file, and output a pseudotranslated version of that file. If\n" + "OUTFILE is specified, the results are written there; otherwise, the results are\n" + "written back to INFILE.\n" + "\n" + "\n" + "usage: localize rescheck FILES...\n" + "\n" + "Reads the base strings and prints warnings about bad resources from the given files.\n" + "\n"); + return 1; +} + +int +main(int argc, const char** argv) +{ + //open_log_file("log.txt"); + //g_logFile = stdout; + + if (argc == 2 && 0 == strcmp(argv[1], "--test")) { + return test(); + } + + if (argc < 2) { + return usage(); + } + + int index = 1; + + if (0 == strcmp("export", argv[index])) { + string settingsFile; + string rootDir; + string outDir; + string baseLocale = "en"; + string targetLocale; + string language, region; + vector<string> configs; + + index++; + while (index < argc) { + if (0 == strcmp("--settings", argv[index])) { + settingsFile = argv[index+1]; + index += 2; + } + else if (0 == strcmp("--root", argv[index])) { + rootDir = argv[index+1]; + index += 2; + } + else if (0 == strcmp("--out", argv[index])) { + outDir = argv[index+1]; + index += 2; + } + else if (0 == strcmp("--target", argv[index])) { + targetLocale = argv[index+1]; + index += 2; + } + else if (argv[index][0] == '-') { + fprintf(stderr, "unknown argument %s\n", argv[index]); + return usage(); + } + else { + break; + } + } + for (; index<argc; index++) { + configs.push_back(argv[index]); + } + + if (settingsFile == "" || rootDir == "" || configs.size() == 0 || targetLocale == "") { + return usage(); + } + if (!split_locale(targetLocale, &language, ®ion)) { + fprintf(stderr, "illegal --target locale: '%s'\n", targetLocale.c_str()); + return usage(); + } + + + return do_export(settingsFile, rootDir, outDir, targetLocale, configs); + } + else if (0 == strcmp("import", argv[index])) { + vector<string> xliffFilenames; + + index++; + for (; index<argc; index++) { + xliffFilenames.push_back(argv[index]); + } + + return do_merge(xliffFilenames); + } + else if (0 == strcmp("xlb", argv[index])) { + string outfile; + vector<string> resFiles; + + index++; + if (argc < index+1) { + return usage(); + } + + outfile = argv[index]; + + index++; + for (; index<argc; index++) { + resFiles.push_back(argv[index]); + } + + return do_xlb_export(outfile, resFiles); + } + else if (0 == strcmp("pseudo", argv[index])) { + string infile; + string outfile; + bool big = false; + + index++; + while (index < argc) { + if (0 == strcmp("--big", argv[index])) { + big = true; + index += 1; + } + else if (argv[index][0] == '-') { + fprintf(stderr, "unknown argument %s\n", argv[index]); + return usage(); + } + else { + break; + } + } + + if (index == argc-1) { + infile = argv[index]; + outfile = argv[index]; + } + else if (index == argc-2) { + infile = argv[index]; + outfile = argv[index+1]; + } + else { + fprintf(stderr, "unknown argument %s\n", argv[index]); + return usage(); + } + + return do_pseudo(infile, outfile, big); + } + else if (0 == strcmp("rescheck", argv[index])) { + vector<string> files; + + index++; + while (index < argc) { + if (argv[index][0] == '-') { + fprintf(stderr, "unknown argument %s\n", argv[index]); + return usage(); + } + else { + break; + } + } + for (; index<argc; index++) { + files.push_back(argv[index]); + } + + if (files.size() == 0) { + return usage(); + } + + return do_rescheck(files); + } + else { + return usage(); + } + + if (SourcePos::HasErrors()) { + SourcePos::PrintErrors(stderr); + return 1; + } + + return 0; +} + diff --git a/tools/localize/localize.h b/tools/localize/localize.h new file mode 100644 index 0000000..615d14e --- /dev/null +++ b/tools/localize/localize.h @@ -0,0 +1,40 @@ +#ifndef LOCALIZE_H +#define LOCALIZE_H + +#include "XLIFFFile.h" + +#include <map> +#include <string> + +using namespace std; + +struct Reject +{ + string file; + string name; + string comment; +}; + +struct Settings +{ + string id; + string oldVersion; + string currentVersion; + vector<string> apps; + vector<Reject> reject; +}; + +int read_settings(const string& filename, map<string,Settings>* result, const string& rootDir); +string translated_file_name(const string& file, const string& locale); +bool keep_this_trans_unit(const string& file, const TransUnit& unit, void* cookie); +int validate_config(const string& settingsFile, const map<string,Settings>& settings, + const string& configs); +int validate_configs(const string& settingsFile, const map<string,Settings>& settings, + const vector<string>& configs); +int select_files(vector<string> *resFiles, const string& config, + const map<string,Settings>& settings, const string& rootDir); +int select_files(vector<vector<string> > *allResFiles, const vector<string>& configs, + const map<string,Settings>& settings, const string& rootDir); + + +#endif // LOCALIZE_H diff --git a/tools/localize/localize_test.cpp b/tools/localize/localize_test.cpp new file mode 100644 index 0000000..63d904c --- /dev/null +++ b/tools/localize/localize_test.cpp @@ -0,0 +1,219 @@ +#include "XLIFFFile.h" +#include "ValuesFile.h" +#include "localize.h" + +int pseudolocalize_xliff(XLIFFFile* xliff, bool expand); + +static int +test_filename(const string& file, const string& locale, const string& expected) +{ + string result = translated_file_name(file, locale); + if (result != expected) { + fprintf(stderr, "translated_file_name test failed\n"); + fprintf(stderr, " locale='%s'\n", locale.c_str()); + fprintf(stderr, " expected='%s'\n", expected.c_str()); + fprintf(stderr, " result='%s'\n", result.c_str()); + return 1; + } else { + if (false) { + fprintf(stderr, "translated_file_name test passed\n"); + fprintf(stderr, " locale='%s'\n", locale.c_str()); + fprintf(stderr, " expected='%s'\n", expected.c_str()); + fprintf(stderr, " result='%s'\n", result.c_str()); + } + return 0; + } +} + +static int +translated_file_name_test() +{ + bool all = true; + int err = 0; + + if (all) err |= test_filename("//device/samples/NotePad/res/values/strings.xml", "zz_ZZ", + "//device/samples/NotePad/res/values-zz-rZZ/strings.xml"); + + if (all) err |= test_filename("//device/samples/NotePad/res/values/strings.xml", "zz", + "//device/samples/NotePad/res/values-zz/strings.xml"); + + if (all) err |= test_filename("//device/samples/NotePad/res/values/strings.xml", "", + "//device/samples/NotePad/res/values/strings.xml"); + + return err; +} + +bool +return_false(const string&, const TransUnit& unit, void* cookie) +{ + return false; +} + +static int +delete_trans_units() +{ + XLIFFFile* xliff = XLIFFFile::Parse("testdata/strip_xliff.xliff"); + if (xliff == NULL) { + printf("couldn't read file\n"); + return 1; + } + if (false) { + printf("XLIFF was [[%s]]\n", xliff->ToString().c_str()); + } + + xliff->Filter(return_false, NULL); + + if (false) { + printf("XLIFF is [[%s]]\n", xliff->ToString().c_str()); + + set<StringResource> const& strings = xliff->GetStringResources(); + printf("strings.size=%zd\n", strings.size()); + for (set<StringResource>::iterator it=strings.begin(); it!=strings.end(); it++) { + const StringResource& str = *it; + printf("STRING!!! id=%s value='%s' pos=%s file=%s version=%d(%s)\n", str.id.c_str(), + str.value->ContentsToString(ANDROID_NAMESPACES).c_str(), + str.pos.ToString().c_str(), str.file.c_str(), str.version, + str.versionString.c_str()); + } + } + + return 0; +} + +static int +filter_trans_units() +{ + XLIFFFile* xliff = XLIFFFile::Parse("testdata/strip_xliff.xliff"); + if (xliff == NULL) { + printf("couldn't read file\n"); + return 1; + } + + if (false) { + printf("XLIFF was [[%s]]\n", xliff->ToString().c_str()); + } + + Settings setting; + xliff->Filter(keep_this_trans_unit, &setting); + + if (false) { + printf("XLIFF is [[%s]]\n", xliff->ToString().c_str()); + + set<StringResource> const& strings = xliff->GetStringResources(); + printf("strings.size=%zd\n", strings.size()); + for (set<StringResource>::iterator it=strings.begin(); it!=strings.end(); it++) { + const StringResource& str = *it; + printf("STRING!!! id=%s value='%s' pos=%s file=%s version=%d(%s)\n", str.id.c_str(), + str.value->ContentsToString(ANDROID_NAMESPACES).c_str(), + str.pos.ToString().c_str(), str.file.c_str(), str.version, + str.versionString.c_str()); + } + } + + return 0; +} + +static int +settings_test() +{ + int err; + map<string,Settings> settings; + map<string,Settings>::iterator it; + + err = read_settings("testdata/config.xml", &settings, "//asdf"); + if (err != 0) { + return err; + } + + if (false) { + for (it=settings.begin(); it!=settings.end(); it++) { + const Settings& setting = it->second; + printf("CONFIG:\n"); + printf(" id='%s'\n", setting.id.c_str()); + printf(" oldVersion='%s'\n", setting.oldVersion.c_str()); + printf(" currentVersion='%s'\n", setting.currentVersion.c_str()); + int i=0; + for (vector<string>::const_iterator app=setting.apps.begin(); + app!=setting.apps.end(); app++) { + printf(" apps[%02d]='%s'\n", i, app->c_str()); + i++; + } + i=0; + for (vector<Reject>::const_iterator reject=setting.reject.begin(); + reject!=setting.reject.end(); reject++) { + i++; + printf(" reject[%02d]=('%s','%s','%s')\n", i, reject->file.c_str(), + reject->name.c_str(), reject->comment.c_str()); + } + } + } + + for (it=settings.begin(); it!=settings.end(); it++) { + const Settings& setting = it->second; + if (it->first != setting.id) { + fprintf(stderr, "it->first='%s' setting.id='%s'\n", it->first.c_str(), + setting.id.c_str()); + err |= 1; + } + } + + + return err; +} + +static int +test_one_pseudo(bool big, const char* expected) +{ + XLIFFFile* xliff = XLIFFFile::Parse("testdata/pseudo.xliff"); + if (xliff == NULL) { + printf("couldn't read file\n"); + return 1; + } + if (false) { + printf("XLIFF was [[%s]]\n", xliff->ToString().c_str()); + } + + pseudolocalize_xliff(xliff, big); + string newString = xliff->ToString(); + delete xliff; + + if (false) { + printf("XLIFF is [[%s]]\n", newString.c_str()); + } + + if (false && newString != expected) { + fprintf(stderr, "xliff didn't translate as expected\n"); + fprintf(stderr, "newString=[[%s]]\n", newString.c_str()); + fprintf(stderr, "expected=[[%s]]\n", expected); + return 1; + } + + return 0; +} + +static int +pseudolocalize_test() +{ + int err = 0; + + err |= test_one_pseudo(false, ""); + //err |= test_one_pseudo(true, ""); + + return err; +} + +int +localize_test() +{ + bool all = true; + int err = 0; + + if (all) err |= translated_file_name_test(); + if (all) err |= delete_trans_units(); + if (all) err |= filter_trans_units(); + if (all) err |= settings_test(); + if (all) err |= pseudolocalize_test(); + + return err; +} + diff --git a/tools/localize/log.h b/tools/localize/log.h new file mode 100644 index 0000000..4a5fa7f --- /dev/null +++ b/tools/localize/log.h @@ -0,0 +1,7 @@ +#ifndef LOG_H +#define LOG_H + +void log_printf(const char* fmt, ...); + +#endif // LOG_H + diff --git a/tools/localize/merge_res_and_xliff.cpp b/tools/localize/merge_res_and_xliff.cpp new file mode 100644 index 0000000..58a6554 --- /dev/null +++ b/tools/localize/merge_res_and_xliff.cpp @@ -0,0 +1,391 @@ +#include "merge_res_and_xliff.h" + +#include "file_utils.h" +#include "Perforce.h" +#include "log.h" + +static set<StringResource>::const_iterator +find_id(const set<StringResource>& s, const string& id, int index) +{ + for (set<StringResource>::const_iterator it = s.begin(); it != s.end(); it++) { + if (it->id == id && it->index == index) { + return it; + } + } + return s.end(); +} + +static set<StringResource>::const_iterator +find_in_xliff(const set<StringResource>& s, const string& filename, const string& id, int index, + int version, const Configuration& config) +{ + for (set<StringResource>::const_iterator it = s.begin(); it != s.end(); it++) { + if (it->file == filename && it->id == id && it->index == index && it->version == version + && it->config == config) { + return it; + } + } + return s.end(); +} + + +static void +printit(const set<StringResource>& s, const set<StringResource>::const_iterator& it) +{ + if (it == s.end()) { + printf("(none)\n"); + } else { + printf("id=%s index=%d config=%s file=%s value='%s'\n", it->id.c_str(), it->index, + it->config.ToString().c_str(), it->file.c_str(), + it->value->ToString(ANDROID_NAMESPACES).c_str()); + } +} + +StringResource +convert_resource(const StringResource& s, const string& file, const Configuration& config, + int version, const string& versionString) +{ + return StringResource(s.pos, file, config, s.id, s.index, s.value ? s.value->Clone() : NULL, + version, versionString, s.comment); +} + +static bool +resource_has_contents(const StringResource& res) +{ + XMLNode* value = res.value; + if (value == NULL) { + return false; + } + string contents = value->ContentsToString(ANDROID_NAMESPACES); + return contents != ""; +} + +ValuesFile* +merge_res_and_xliff(const ValuesFile* en_currentFile, + const ValuesFile* xx_currentFile, const ValuesFile* xx_oldFile, + const string& filename, const XLIFFFile* xliffFile) +{ + bool success = true; + + Configuration en_config = xliffFile->SourceConfig(); + Configuration xx_config = xliffFile->TargetConfig(); + string currentVersion = xliffFile->CurrentVersion(); + + ValuesFile* result = new ValuesFile(xx_config); + + set<StringResource> en_cur = en_currentFile->GetStrings(); + set<StringResource> xx_cur = xx_currentFile->GetStrings(); + set<StringResource> xx_old = xx_oldFile->GetStrings(); + set<StringResource> xliff = xliffFile->GetStringResources(); + + // for each string in en_current + for (set<StringResource>::const_iterator en_c = en_cur.begin(); + en_c != en_cur.end(); en_c++) { + set<StringResource>::const_iterator xx_c = find_id(xx_cur, en_c->id, en_c->index); + set<StringResource>::const_iterator xx_o = find_id(xx_old, en_c->id, en_c->index); + set<StringResource>::const_iterator xlf = find_in_xliff(xliff, en_c->file, en_c->id, + en_c->index, CURRENT_VERSION, xx_config); + + if (false) { + printf("\nen_c: "); printit(en_cur, en_c); + printf("xx_c: "); printit(xx_cur, xx_c); + printf("xx_o: "); printit(xx_old, xx_o); + printf("xlf: "); printit(xliff, xlf); + } + + // if it changed between xx_old and xx_current, use xx_current + // (someone changed it by hand) + if (xx_o != xx_old.end() && xx_c != xx_cur.end()) { + string xx_o_value = xx_o->value->ToString(ANDROID_NAMESPACES); + string xx_c_value = xx_c->value->ToString(ANDROID_NAMESPACES); + if (xx_o_value != xx_c_value && xx_c_value != "") { + StringResource r(convert_resource(*xx_c, filename, xx_config, + CURRENT_VERSION, currentVersion)); + if (resource_has_contents(r)) { + result->AddString(r); + } + continue; + } + } + + // if it is present in xliff, use that + // (it just got translated) + if (xlf != xliff.end() && xlf->value->ToString(ANDROID_NAMESPACES) != "") { + StringResource r(convert_resource(*xlf, filename, xx_config, + CURRENT_VERSION, currentVersion)); + if (resource_has_contents(r)) { + result->AddString(r); + } + } + + // if it is present in xx_current, use that + // (it was already translated, and not retranslated) + // don't filter out empty strings if they were added by hand, the above code just + // guarantees that this tool never adds an empty one. + if (xx_c != xx_cur.end()) { + StringResource r(convert_resource(*xx_c, filename, xx_config, + CURRENT_VERSION, currentVersion)); + result->AddString(r); + } + + // othwerwise, leave it out. The resource fall-through code will use the English + // one at runtime, and the xliff export code will pick it up for translation next time. + } + + if (success) { + return result; + } else { + delete result; + return NULL; + } +} + + +struct MergedFile { + XLIFFFile* xliff; + string xliffFilename; + string original; + string translated; + ValuesFile* en_current; + ValuesFile* xx_current; + ValuesFile* xx_old; + ValuesFile* xx_new; + string xx_new_text; + string xx_new_filename; + bool new_file; + bool deleted_file; + + MergedFile(); + MergedFile(const MergedFile&); +}; + +struct compare_filenames { + bool operator()(const MergedFile& lhs, const MergedFile& rhs) const + { + return lhs.original < rhs.original; + } +}; + +MergedFile::MergedFile() + :xliff(NULL), + xliffFilename(), + original(), + translated(), + en_current(NULL), + xx_current(NULL), + xx_old(NULL), + xx_new(NULL), + xx_new_text(), + xx_new_filename(), + new_file(false), + deleted_file(false) +{ +} + +MergedFile::MergedFile(const MergedFile& that) + :xliff(that.xliff), + xliffFilename(that.xliffFilename), + original(that.original), + translated(that.translated), + en_current(that.en_current), + xx_current(that.xx_current), + xx_old(that.xx_old), + xx_new(that.xx_new), + xx_new_text(that.xx_new_text), + xx_new_filename(that.xx_new_filename), + new_file(that.new_file), + deleted_file(that.deleted_file) +{ +} + + +typedef set<MergedFile, compare_filenames> MergedFileSet; + +int +do_merge(const vector<string>& xliffFilenames) +{ + int err = 0; + MergedFileSet files; + + printf("\rPreparing..."); fflush(stdout); + string currentChange = Perforce::GetCurrentChange(true); + + // for each xliff, make a MergedFile record and do a little error checking + for (vector<string>::const_iterator xliffFilename=xliffFilenames.begin(); + xliffFilename!=xliffFilenames.end(); xliffFilename++) { + XLIFFFile* xliff = XLIFFFile::Parse(*xliffFilename); + if (xliff == NULL) { + fprintf(stderr, "localize import: unable to read file %s\n", xliffFilename->c_str()); + err = 1; + continue; + } + + set<string> xf = xliff->Files(); + for (set<string>::const_iterator f=xf.begin(); f!=xf.end(); f++) { + MergedFile mf; + mf.xliff = xliff; + mf.xliffFilename = *xliffFilename; + mf.original = *f; + mf.translated = translated_file_name(mf.original, xliff->TargetConfig().locale); + log_printf("mf.translated=%s mf.original=%s locale=%s\n", mf.translated.c_str(), + mf.original.c_str(), xliff->TargetConfig().locale.c_str()); + + if (files.find(mf) != files.end()) { + fprintf(stderr, "%s: duplicate string resources for file %s\n", + xliffFilename->c_str(), f->c_str()); + fprintf(stderr, "%s: previously defined here.\n", + files.find(mf)->xliffFilename.c_str()); + err = 1; + continue; + } + files.insert(mf); + } + } + + size_t deletedFileCount = 0; + size_t J = files.size() * 3; + size_t j = 1; + // Read all of the files from perforce. + for (MergedFileSet::iterator mf = files.begin(); mf != files.end(); mf++) { + MergedFile* file = const_cast<MergedFile*>(&(*mf)); + // file->en_current + print_file_status(j++, J); + file->en_current = get_values_file(file->original, file->xliff->SourceConfig(), + CURRENT_VERSION, currentChange, true); + if (file->en_current == NULL) { + // deleted file + file->deleted_file = true; + deletedFileCount++; + continue; + } + + // file->xx_current; + print_file_status(j++, J); + file->xx_current = get_values_file(file->translated, file->xliff->TargetConfig(), + CURRENT_VERSION, currentChange, false); + if (file->xx_current == NULL) { + file->xx_current = new ValuesFile(file->xliff->TargetConfig()); + file->new_file = true; + } + + // file->xx_old (note that the xliff's current version is our old version, because that + // was the current version when it was exported) + print_file_status(j++, J); + file->xx_old = get_values_file(file->translated, file->xliff->TargetConfig(), + OLD_VERSION, file->xliff->CurrentVersion(), false); + if (file->xx_old == NULL) { + file->xx_old = new ValuesFile(file->xliff->TargetConfig()); + file->new_file = true; + } + } + + // merge them + for (MergedFileSet::iterator mf = files.begin(); mf != files.end(); mf++) { + MergedFile* file = const_cast<MergedFile*>(&(*mf)); + if (file->deleted_file) { + continue; + } + file->xx_new = merge_res_and_xliff(file->en_current, file->xx_current, file->xx_old, + file->original, file->xliff); + } + + // now is a good time to stop if there was an error + if (err != 0) { + return err; + } + + // locate the files + j = 1; + for (MergedFileSet::iterator mf = files.begin(); mf != files.end(); mf++) { + MergedFile* file = const_cast<MergedFile*>(&(*mf)); + print_file_status(j++, J, "Locating"); + + file->xx_new_filename = Perforce::Where(file->translated, true); + if (file->xx_new_filename == "") { + fprintf(stderr, "\nWas not able to determine the location of depot file %s\n", + file->translated.c_str()); + err = 1; + } + } + + if (err != 0) { + return err; + } + + // p4 edit the files + // only do this if it changed - no need to submit files that haven't changed meaningfully + vector<string> filesToEdit; + vector<string> filesToAdd; + vector<string> filesToDelete; + for (MergedFileSet::iterator mf = files.begin(); mf != files.end(); mf++) { + MergedFile* file = const_cast<MergedFile*>(&(*mf)); + if (file->deleted_file) { + filesToDelete.push_back(file->xx_new_filename); + continue; + } + string xx_current_text = file->xx_current->ToString(); + string xx_new_text = file->xx_new->ToString(); + if (xx_current_text != xx_new_text) { + if (file->xx_new->GetStrings().size() == 0) { + file->deleted_file = true; + filesToDelete.push_back(file->xx_new_filename); + } else { + file->xx_new_text = xx_new_text; + if (file->new_file) { + filesToAdd.push_back(file->xx_new_filename); + } else { + filesToEdit.push_back(file->xx_new_filename); + } + } + } + } + if (filesToAdd.size() == 0 && filesToEdit.size() == 0 && deletedFileCount == 0) { + printf("\nAll of the files are the same. Nothing to change.\n"); + return 0; + } + if (filesToEdit.size() > 0) { + printf("\np4 editing files...\n"); + if (0 != Perforce::EditFiles(filesToEdit, true)) { + return 1; + } + } + + + printf("\n"); + + for (MergedFileSet::iterator mf = files.begin(); mf != files.end(); mf++) { + MergedFile* file = const_cast<MergedFile*>(&(*mf)); + if (file->deleted_file) { + continue; + } + if (file->xx_new_text != "" && file->xx_new_filename != "") { + if (0 != write_to_file(file->xx_new_filename, file->xx_new_text)) { + err = 1; + } + } + } + + if (err != 0) { + return err; + } + + if (filesToAdd.size() > 0) { + printf("p4 adding %zd new files...\n", filesToAdd.size()); + err = Perforce::AddFiles(filesToAdd, true); + } + + if (filesToDelete.size() > 0) { + printf("p4 deleting %zd removed files...\n", filesToDelete.size()); + err = Perforce::DeleteFiles(filesToDelete, true); + } + + if (err != 0) { + return err; + } + + printf("\n" + "Theoretically, this merge was successfull. Next you should\n" + "review the diffs, get a code review, and submit it. Enjoy.\n\n"); + return 0; +} + diff --git a/tools/localize/merge_res_and_xliff.h b/tools/localize/merge_res_and_xliff.h new file mode 100644 index 0000000..acf2fff --- /dev/null +++ b/tools/localize/merge_res_and_xliff.h @@ -0,0 +1,13 @@ +#ifndef MERGE_RES_AND_XLIFF_H +#define MERGE_RES_AND_XLIFF_H + +#include "ValuesFile.h" +#include "XLIFFFile.h" + +ValuesFile* merge_res_and_xliff(const ValuesFile* en_current, + const ValuesFile* xx_current, const ValuesFile* xx_old, + const string& filename, const XLIFFFile* xliff); + +int do_merge(const vector<string>& xliffFilenames); + +#endif // MERGE_RES_AND_XLIFF_H diff --git a/tools/localize/merge_res_and_xliff_test.cpp b/tools/localize/merge_res_and_xliff_test.cpp new file mode 100644 index 0000000..5a2b0f4 --- /dev/null +++ b/tools/localize/merge_res_and_xliff_test.cpp @@ -0,0 +1,47 @@ +#include "merge_res_and_xliff.h" + + +int +merge_test() +{ + Configuration english; + english.locale = "en_US"; + Configuration translated; + translated.locale = "zz_ZZ"; + + ValuesFile* en_current = ValuesFile::ParseFile("testdata/merge_en_current.xml", english, + CURRENT_VERSION, "3"); + if (en_current == NULL) { + fprintf(stderr, "merge_test: unable to read testdata/merge_en_current.xml\n"); + return 1; + } + + ValuesFile* xx_current = ValuesFile::ParseFile("testdata/merge_xx_current.xml", translated, + CURRENT_VERSION, "3"); + if (xx_current == NULL) { + fprintf(stderr, "merge_test: unable to read testdata/merge_xx_current.xml\n"); + return 1; + } + ValuesFile* xx_old = ValuesFile::ParseFile("testdata/merge_xx_old.xml", translated, + OLD_VERSION, "2"); + if (xx_old == NULL) { + fprintf(stderr, "merge_test: unable to read testdata/merge_xx_old.xml\n"); + return 1; + } + + XLIFFFile* xliff = XLIFFFile::Parse("testdata/merge.xliff"); + + ValuesFile* result = merge_res_and_xliff(en_current, xx_current, xx_old, + "//device/tools/localize/testdata/res/values/strings.xml", xliff); + + if (result == NULL) { + fprintf(stderr, "merge_test: result is NULL\n"); + return 1; + } + + printf("======= RESULT =======\n%s===============\n", result->ToString().c_str()); + + return 0; +} + + diff --git a/tools/localize/res_check.cpp b/tools/localize/res_check.cpp new file mode 100644 index 0000000..0fab98a --- /dev/null +++ b/tools/localize/res_check.cpp @@ -0,0 +1,106 @@ +#include "res_check.h" +#include "localize.h" +#include "file_utils.h" +#include "ValuesFile.h" + +#include <stdio.h> + +static int check_file(const ValuesFile* file); +static int check_value(const SourcePos& pos, const XMLNode* value); +static int scan_for_unguarded_format(const SourcePos& pos, const XMLNode* value, int depth = 0); + +int +do_rescheck(const vector<string>& files) +{ + int err; + + Configuration english; + english.locale = "en_US"; + + for (size_t i=0; i<files.size(); i++) { + const string filename = files[i]; + ValuesFile* valuesFile = get_local_values_file(filename, english, CURRENT_VERSION, + "0", true); + if (valuesFile != NULL) { + err |= check_file(valuesFile); + delete valuesFile; + } else { + err |= 1; + } + } + + return err; +} + +static int +check_file(const ValuesFile* file) +{ + int err = 0; + set<StringResource> strings = file->GetStrings(); + for (set<StringResource>::iterator it=strings.begin(); it!=strings.end(); it++) { + XMLNode* value = it->value; + if (value != NULL) { + err |= check_value(it->pos, value); + } + } + return err; +} + +static bool +contains_percent(const string& str) +{ + const size_t len = str.length(); + for (size_t i=0; i<len; i++) { + char c = str[i]; + if (c == '%') { + return true; + } + } + return false; +} + +static int +check_value(const SourcePos& pos, const XMLNode* value) +{ + int err = 0; + err |= scan_for_unguarded_format(pos, value); + return err; +} + +static bool +is_xliff_block(const string& ns, const string& name) +{ + if (ns == XLIFF_XMLNS) { + return name == "g"; + } else { + return false; + } +} + +static int +scan_for_unguarded_format(const SourcePos& pos, const string& string) +{ + bool containsPercent = contains_percent(string); + if (containsPercent) { + pos.Error("unguarded percent: '%s'\n", string.c_str()); + } + return 0; +} + +static int +scan_for_unguarded_format(const SourcePos& pos, const XMLNode* value, int depth) +{ + if (value->Type() == XMLNode::ELEMENT) { + int err = 0; + if (depth == 0 || !is_xliff_block(value->Namespace(), value->Name())) { + const vector<XMLNode*>& children = value->Children(); + for (size_t i=0; i<children.size(); i++) { + err |= scan_for_unguarded_format(pos, children[i], depth+1); + } + } + return err; + } else { + return scan_for_unguarded_format(pos, value->Text()); + } +} + diff --git a/tools/localize/res_check.h b/tools/localize/res_check.h new file mode 100644 index 0000000..86e7ce6 --- /dev/null +++ b/tools/localize/res_check.h @@ -0,0 +1,12 @@ +#ifndef RESCHECK_H +#define RESCHECK_H + +#include <map> +#include <string> +#include <vector> + +using namespace std; + +int do_rescheck(const vector<string>& files); + +#endif // RESCHECK_H diff --git a/tools/localize/test.cpp b/tools/localize/test.cpp new file mode 100644 index 0000000..5fa2c17 --- /dev/null +++ b/tools/localize/test.cpp @@ -0,0 +1,31 @@ +#include "SourcePos.h" +#include <stdio.h> + +int ValuesFile_test(); +int XLIFFFile_test(); +int XMLHandler_test(); +int Perforce_test(); +int localize_test(); +int merge_test(); + +int +test() +{ + bool all = true; + int err = 0; + + if (all) err |= XMLHandler_test(); + if (all) err |= ValuesFile_test(); + if (all) err |= XLIFFFile_test(); + if (all) err |= Perforce_test(); + if (all) err |= localize_test(); + if (all) err |= merge_test(); + + if (err != 0) { + fprintf(stderr, "some tests failed\n"); + } else { + fprintf(stderr, "all tests passed\n"); + } + + return err; +} diff --git a/tools/localize/testdata/config.xml b/tools/localize/testdata/config.xml new file mode 100644 index 0000000..affa140 --- /dev/null +++ b/tools/localize/testdata/config.xml @@ -0,0 +1,15 @@ +<localize-config> + <configuration id="system" + old-cl="1" + new-cl="43019"> + <app dir="apps/common" /> + </configuration> + <configuration id="samples" + old-cl="24801" + new-cl="43019"> + <app dir="samples/NotePad" /> + <reject file="samples/NotePad/res/values/strings.xml" name="string:menu_delete"> + QA says this sounds <b>rude</b>. + </reject> + </configuration> +</localize-config> diff --git a/tools/localize/testdata/import.xliff b/tools/localize/testdata/import.xliff new file mode 100644 index 0000000..b99b739 --- /dev/null +++ b/tools/localize/testdata/import.xliff @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" + version="1.2" + > + <file datatype="x-android-res" + original="//device/tools/localize/testdata/res/values/strings.xml" + product-version="1.0" + date="08:10:54 12/07/07 PST" + source-language="en_US" + product-name="kila" + target-language="zz_ZZ" + build-num="44391" + > + <body> + <trans-unit id="string:changed_in_xx"> + <source>aaa</source> + <target>AAA</target> + </trans-unit> + <trans-unit id="string:first_translation"> + <source>bbb</source> + <target>BBB</target> + </trans-unit> + <trans-unit id="string:deleted_string"> + <source>ddd</source> + <target>DDDD</target> + </trans-unit> + <trans-unit id="array:0:growing_array"> + <source>1-One</source> + <target>1-oNE</target> + </trans-unit> + <trans-unit id="array:1:growing_array"> + <source>1-Two</source> + <target>1-tWO</target> + </trans-unit> + <trans-unit id="array:2:growing_array"> + <source>1-Three</source> + <target>1-tHREE</target> + </trans-unit> + <trans-unit id="array:0:shrinking_array"> + <source>2-One</source> + <target>2-oNE</target> + </trans-unit> + <trans-unit id="array:1:shrinking_array"> + <source>2-Two</source> + <target>2-tWO</target> + </trans-unit> + <trans-unit id="array:2:shrinking_array"> + <source>2-Three</source> + <target>2-tHREE</target> + </trans-unit> + <trans-unit id="array:3:shrinking_array"> + <source>2-Four</source> + <target>2-fOUR</target> + </trans-unit> + <trans-unit id="array:0:deleted_array"> + <source>4-One</source> + <target>4-oNE</target> + </trans-unit> + <trans-unit id="array:1:deleted_array"> + <source>4-Two</source> + <target>4-tWO</target> + </trans-unit> + <trans-unit id="array:2:deleted_array"> + <source>4-Three</source> + <target>4-tHREE</target> + </trans-unit> + + </body> + </file> +</xliff> + + diff --git a/tools/localize/testdata/merge.xliff b/tools/localize/testdata/merge.xliff new file mode 100644 index 0000000..2b78c45 --- /dev/null +++ b/tools/localize/testdata/merge.xliff @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" + version="1.2" + > + <file datatype="x-android-res" + original="testdata/merge_en_current.xml" + product-version="1.0" + date="08:10:54 12/07/07 PST" + source-language="en-US" + product-name="kila" + target-language="zz-ZZ" + build-num="44391" + > + <body> + <trans-unit id="string:changed_in_xx"> + <source>aaa</source> + <target>AAA</target> + </trans-unit> + <trans-unit id="string:first_translation"> + <source>bbb</source> + <target>BBB</target> + </trans-unit> + <trans-unit id="string:deleted_string"> + <source>ddd</source> + <target>DDDD</target> + </trans-unit> + <trans-unit id="array:0:growing_array"> + <source>1-One</source> + <target>1-oNE</target> + </trans-unit> + <trans-unit id="array:1:growing_array"> + <source>1-Two</source> + <target>1-tWO</target> + </trans-unit> + <trans-unit id="array:2:growing_array"> + <source>1-Three</source> + <target>1-tHREE</target> + </trans-unit> + <trans-unit id="array:0:shrinking_array"> + <source>2-One</source> + <target>2-oNE</target> + </trans-unit> + <trans-unit id="array:1:shrinking_array"> + <source>2-Two</source> + <target>2-tWO</target> + </trans-unit> + <trans-unit id="array:2:shrinking_array"> + <source>2-Three</source> + <target>2-tHREE</target> + </trans-unit> + <trans-unit id="array:3:shrinking_array"> + <source>2-Four</source> + <target>2-fOUR</target> + </trans-unit> + <trans-unit id="array:0:deleted_array"> + <source>4-One</source> + <target>4-oNE</target> + </trans-unit> + <trans-unit id="array:1:deleted_array"> + <source>4-Two</source> + <target>4-tWO</target> + </trans-unit> + <trans-unit id="array:2:deleted_array"> + <source>4-Three</source> + <target>4-tHREE</target> + </trans-unit> + + </body> + </file> +</xliff> + + diff --git a/tools/localize/testdata/merge_en_current.xml b/tools/localize/testdata/merge_en_current.xml new file mode 100644 index 0000000..6a11e68 --- /dev/null +++ b/tools/localize/testdata/merge_en_current.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="changed_in_xx">aaa</string> + <string name="first_translation">bbb</string> + <string name="previously_translated">ccc</string> + <string name="new_string">ccc</string> + + <string name="formatted_string"><b>bold</b><i>italic<u>italic_underline</u></i><u>underline</u></string> + + <array name="growing_array"> + <!-- somebody wrote a comment! --> + <item>1-One</item> + <item>1-Two</item> + <item>1-Three</item> + <item>1-Four</item> + </array> + <array name="shrinking_array"> + <!-- somebody wrote a comment! --> + <item>2-One</item> + <item>2-Two</item> + <item>2-Three</item> + </array> + <array name="new_array"> + <!-- somebody wrote a comment! --> + <item>3-One</item> + <item>3-Two</item> + <item>3-Three</item> + </array> +</resources> diff --git a/tools/localize/testdata/merge_en_old.xml b/tools/localize/testdata/merge_en_old.xml new file mode 100644 index 0000000..933f98e --- /dev/null +++ b/tools/localize/testdata/merge_en_old.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="changed_in_xx">aaa</string> + <string name="first_translation">bbb</string> + <string name="previously_translated">ccc</string> + <string name="deleted_string">ddd</string> + + <string name="formatted_string"><b>bold</b><i>italic<u>italic_underline</u></i><u>underline</u></string> + + <array name="growing_array"> + <!-- somebody wrote a comment! --> + <item>1-One</item> + <item>1-Two</item> + <item>1-Three</item> + </array> + <array name="shrinking_array"> + <!-- somebody wrote a comment! --> + <item>2-One</item> + <item>2-Two</item> + <item>2-Three</item> + <item>2-Four</item> + </array> + <array name="deleted_array"> + <!-- somebody wrote a comment! --> + <item>4-One</item> + <item>4-Two</item> + <item>4-Three</item> + </array> +</resources> + diff --git a/tools/localize/testdata/merge_xx_current.xml b/tools/localize/testdata/merge_xx_current.xml new file mode 100644 index 0000000..c2a783d --- /dev/null +++ b/tools/localize/testdata/merge_xx_current.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="changed_in_xx">AAAA</string> + <string name="previously_translated">CCC</string> +</resources> + + diff --git a/tools/localize/testdata/merge_xx_old.xml b/tools/localize/testdata/merge_xx_old.xml new file mode 100644 index 0000000..9d3a7d8 --- /dev/null +++ b/tools/localize/testdata/merge_xx_old.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="changed_in_xx">aaa</string> + <string name="previously_translated">CCC</string> +</resources> + diff --git a/tools/localize/testdata/pseudo.xliff b/tools/localize/testdata/pseudo.xliff new file mode 100644 index 0000000..5b44f86 --- /dev/null +++ b/tools/localize/testdata/pseudo.xliff @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" + version="1.2" + > + <file datatype="x-android-res" + original="//device/tools/localization/tests/res/values/strings.xml" + product-version="1.0" + date="08:10:54 12/07/07 PST" + source-language="en-US" + product-name="kila" + target-language="zz-ZZ" + build-num="32138" + > + <body> + <trans-unit id="string:complex"> + <source>First <g id="string:complex:0" ctype="underline">underline</g>, <g id="string:complex:1" ctype="italic">italic<g id="string:complex:2" ctype="bold">italicbold</g></g> End </source> + </trans-unit> + <trans-unit id="string:complex-quoted"> + <source xml:space="preserve">First <g id="string:complex-quoted:0" ctype="underline">underline</g>, <g id="string:complex-quoted:1" ctype="italic">italic<g id="string:complex-quoted:2" ctype="bold">italicbold</g></g> End</source> + </trans-unit> + <trans-unit id="string:simple"> + <source>Simple</source> + </trans-unit> + <trans-unit id="array:0:simple"> + <source>Simple</source> + </trans-unit> + <trans-unit id="array:1:simple"> + <source>Simple</source> + </trans-unit> + <trans-unit id="string:simple-quoted"> + <source xml:space="preserve"> Quote</source> + <alt-trans> + <source xml:lang="en" xml:space="preserve"> OLD Quote</source> + <target xml:lang="xx"> OLD Ờũỡŧę</target> + </alt-trans> + </trans-unit> + </body> + </file> +</xliff> + diff --git a/tools/localize/testdata/res/values-zz-rZZ/strings.xml b/tools/localize/testdata/res/values-zz-rZZ/strings.xml new file mode 100644 index 0000000..c2a783d --- /dev/null +++ b/tools/localize/testdata/res/values-zz-rZZ/strings.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="changed_in_xx">AAAA</string> + <string name="previously_translated">CCC</string> +</resources> + + diff --git a/tools/localize/testdata/res/values/strings.xml b/tools/localize/testdata/res/values/strings.xml new file mode 100644 index 0000000..6a11e68 --- /dev/null +++ b/tools/localize/testdata/res/values/strings.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="changed_in_xx">aaa</string> + <string name="first_translation">bbb</string> + <string name="previously_translated">ccc</string> + <string name="new_string">ccc</string> + + <string name="formatted_string"><b>bold</b><i>italic<u>italic_underline</u></i><u>underline</u></string> + + <array name="growing_array"> + <!-- somebody wrote a comment! --> + <item>1-One</item> + <item>1-Two</item> + <item>1-Three</item> + <item>1-Four</item> + </array> + <array name="shrinking_array"> + <!-- somebody wrote a comment! --> + <item>2-One</item> + <item>2-Two</item> + <item>2-Three</item> + </array> + <array name="new_array"> + <!-- somebody wrote a comment! --> + <item>3-One</item> + <item>3-Two</item> + <item>3-Three</item> + </array> +</resources> diff --git a/tools/localize/testdata/strip_xliff.xliff b/tools/localize/testdata/strip_xliff.xliff new file mode 100644 index 0000000..9254cf2 --- /dev/null +++ b/tools/localize/testdata/strip_xliff.xliff @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" + version="1.2" + > + <file datatype="x-android-res" + original="//device/tools/localization/tests/res/values/strings.xml" + product-version="1.0" + date="08:10:54 12/07/07 PST" + source-language="en-US" + product-name="kila" + target-language="zz-ZZ" + build-num="32138" + > + <body> + + <trans-unit id="string:string-000-0"> + </trans-unit> + <trans-unit id="string:string-001-0"> + <alt-trans> + <source xml:lang="en" xml:space="preserve">source</source> + </alt-trans> + </trans-unit> + <trans-unit id="string:string-010-0"> + <alt-trans> + <target xml:lang="zz" xml:space="preserve">target</target> + </alt-trans> + </trans-unit> + <trans-unit id="string:string-011-0"> + <alt-trans> + <source xml:lang="en" xml:space="preserve">source</source> + <target xml:lang="zz" xml:space="preserve">target</target> + </alt-trans> + </trans-unit> + + <trans-unit id="string:string-100-1"> + <source xml:space="preserve">source</source> + </trans-unit> + <trans-unit id="string:string-101-1"> + <source xml:space="preserve">source</source> + <alt-trans> + <source xml:lang="en" xml:space="preserve">source</source> + </alt-trans> + </trans-unit> + <trans-unit id="string:string-110-1"> + <source xml:space="preserve">source</source> + <alt-trans> + <target xml:lang="zz" xml:space="preserve">target</target> + </alt-trans> + </trans-unit> + + <trans-unit id="string:string-111-0"> + <source xml:space="preserve">source</source> + <alt-trans> + <source xml:lang="en" xml:space="preserve">source</source> + <target xml:lang="zz" xml:space="preserve">target</target> + </alt-trans> + </trans-unit> + <trans-unit id="string:string-111-1"> + <source xml:space="preserve">source</source> + <alt-trans> + <source xml:lang="en" xml:space="preserve">alt-source</source> + <target xml:lang="zz" xml:space="preserve">target</target> + </alt-trans> + </trans-unit> + + </body> + </file> +</xliff> + + diff --git a/tools/localize/testdata/values/strings.xml b/tools/localize/testdata/values/strings.xml new file mode 100644 index 0000000..5e8d43d --- /dev/null +++ b/tools/localize/testdata/values/strings.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<resources + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="test1">Discard</string> + <!-- comment --> + <string name="test2">a<b>b<i>c</i></b>d</string> + <string name="test3">a<xliff:g a="b" xliff:a="asdf">bBb</xliff:g>C</string> + + <!-- Email address types from android.provider.Contacts --> + <array name="emailAddressTypes"> + <!-- somebody wrote a comment! --> + <item>Email</item> + <item>Home</item> + <item>Work</item> + <item>Other\u2026</item> + </array> +</resources> diff --git a/tools/localize/testdata/xliff1.xliff b/tools/localize/testdata/xliff1.xliff new file mode 100644 index 0000000..55a8d8e --- /dev/null +++ b/tools/localize/testdata/xliff1.xliff @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" + version="1.2" + > + <file datatype="x-android-res" + original="//device/tools/localization/tests/res/values/strings.xml" + product-version="1.0" + date="08:10:54 12/07/07 PST" + source-language="en-US" + product-name="kila" + target-language="zz-ZZ" + build-num="32138" + > + <body> + <trans-unit id="string:complex"> + <source>First <g id="string:complex:0" ctype="underline">underline</g>, <g id="string:complex:1" ctype="italic">italic<g id="string:complex:2" ctype="bold">italicbold</g></g> End </source> + <target>Ḟịṙṩŧ , Ḛŋḋ </target> + </trans-unit> + <trans-unit id="string:complex-quoted"> + <source xml:space="preserve">First <g id="string:complex-quoted:0" ctype="underline">underline</g>, <g id="string:complex-quoted:1" ctype="italic">italic<g id="string:complex-quoted:2" ctype="bold">italicbold</g></g> End</source> + <target>Ḟịṙṩŧ , Ḛŋḋ</target> + </trans-unit> + <trans-unit id="string:simple"> + <source>Simple</source> + <target>Ṩịṃṕļę</target> + </trans-unit> + <trans-unit id="array:0:simple"> + <source>Simple</source> + <target>Ṩịṃṕļę</target> + </trans-unit> + <trans-unit id="array:1:simple"> + <source>Simple</source> + <target>Ṩịṃṕļę</target> + </trans-unit> + <trans-unit id="string:simple-quoted"> + <source xml:space="preserve"> Quote</source> + <target> Ờũỡŧę</target> + <alt-trans> + <source xml:lang="en" xml:space="preserve"> OLD Quote</source> + <target xml:lang="xx"> OLD Ờũỡŧę</target> + </alt-trans> + </trans-unit> + </body> + </file> +</xliff> + diff --git a/tools/localize/testdata/xml.xml b/tools/localize/testdata/xml.xml new file mode 100644 index 0000000..ef930d0 --- /dev/null +++ b/tools/localize/testdata/xml.xml @@ -0,0 +1,16 @@ +<ASDF> + <a id="system" + old-cl="1" + new-cl="43019"> + <app dir="apps/common" /> + </a> + <a id="samples" + old-cl="1" + new-cl="43019">asdf + <app dir="samples/NotePad" /> + <app dir="samples/LunarLander" /> + <something>a<b>,</b>b </something> + <exact xml:space="preserve">a<b>,</b>b </exact> + </a> +</ASDF> + diff --git a/tools/localize/xmb.cpp b/tools/localize/xmb.cpp new file mode 100644 index 0000000..236705f --- /dev/null +++ b/tools/localize/xmb.cpp @@ -0,0 +1,181 @@ +#include "xmb.h" + +#include "file_utils.h" +#include "localize.h" +#include "ValuesFile.h" +#include "XMLHandler.h" +#include "XLIFFFile.h" + +#include <map> + +using namespace std; + +const char *const NS_MAP[] = { + "xml", XMLNS_XMLNS, + NULL, NULL +}; + +set<string> g_tags; + +static string +strip_newlines(const string& str) +{ + string res; + const size_t N = str.length(); + for (size_t i=0; i<N; i++) { + char c = str[i]; + if (c != '\n' && c != '\r') { + res += c; + } else { + res += ' '; + } + } + return res; +} + +static int +rename_id_attribute(XMLNode* node) +{ + vector<XMLAttribute>& attrs = node->EditAttributes(); + const size_t I = attrs.size(); + for (size_t i=0; i<I; i++) { + XMLAttribute attr = attrs[i]; + if (attr.name == "id") { + attr.name = "name"; + attrs.erase(attrs.begin()+i); + attrs.push_back(attr); + return 0; + } + } + return 1; +} + +static int +convert_xliff_to_ph(XMLNode* node, int* phID) +{ + int err = 0; + if (node->Type() == XMLNode::ELEMENT) { + if (node->Namespace() == XLIFF_XMLNS) { + g_tags.insert(node->Name()); + node->SetName("", "ph"); + + err = rename_id_attribute(node); + if (err != 0) { + char name[30]; + (*phID)++; + sprintf(name, "id-%d", *phID); + node->EditAttributes().push_back(XMLAttribute("", "name", name)); + err = 0; + } + } + vector<XMLNode*>& children = node->EditChildren(); + const size_t I = children.size(); + for (size_t i=0; i<I; i++) { + err |= convert_xliff_to_ph(children[i], phID); + } + } + return err; +} + +XMLNode* +resource_to_xmb_msg(const StringResource& res) +{ + // the msg element + vector<XMLAttribute> attrs; + string name = res.pos.file; + name += ":"; + name += res.TypedID(); + attrs.push_back(XMLAttribute("", "name", name)); + attrs.push_back(XMLAttribute("", "desc", strip_newlines(res.comment))); + attrs.push_back(XMLAttribute(XMLNS_XMLNS, "space", "preserve")); + XMLNode* msg = XMLNode::NewElement(res.pos, "", "msg", attrs, XMLNode::EXACT); + + // the contents are in xliff/html, convert it to xliff + int err = 0; + XMLNode* value = res.value; + string tag = value->Name(); + int phID = 0; + for (vector<XMLNode*>::const_iterator it=value->Children().begin(); + it!=value->Children().end(); it++) { + err |= convert_html_to_xliff(*it, tag, msg, &phID); + } + + if (err != 0) { + return NULL; + } + + // and then convert that to xmb + for (vector<XMLNode*>::iterator it=msg->EditChildren().begin(); + it!=msg->EditChildren().end(); it++) { + err |= convert_xliff_to_ph(*it, &phID); + } + + if (err == 0) { + return msg; + } else { + return NULL; + } +} + +int +do_xlb_export(const string& outfile, const vector<string>& resFiles) +{ + int err = 0; + + size_t totalFileCount = resFiles.size(); + + Configuration english; + english.locale = "en_US"; + + set<StringResource> allResources; + + const size_t J = resFiles.size(); + for (size_t j=0; j<J; j++) { + string resFile = resFiles[j]; + + ValuesFile* valuesFile = get_local_values_file(resFile, english, CURRENT_VERSION, "", true); + if (valuesFile != NULL) { + set<StringResource> resources = valuesFile->GetStrings(); + allResources.insert(resources.begin(), resources.end()); + } else { + fprintf(stderr, "error reading file %s\n", resFile.c_str()); + } + + delete valuesFile; + } + + // Construct the XLB xml + vector<XMLAttribute> attrs; + attrs.push_back(XMLAttribute("", "locale", "en")); + XMLNode* localizationbundle = XMLNode::NewElement(GENERATED_POS, "", "localizationbundle", + attrs, XMLNode::PRETTY); + + for (set<StringResource>::iterator it=allResources.begin(); it!=allResources.end(); it++) { + XMLNode* msg = resource_to_xmb_msg(*it); + if (msg) { + localizationbundle->EditChildren().push_back(msg); + } else { + err = 1; + } + } + +#if 0 + for (set<string>::iterator it=g_tags.begin(); it!=g_tags.end(); it++) { + printf("tag: %s\n", it->c_str()); + } + printf("err=%d\n", err); +#endif + if (err == 0) { + FILE* f = fopen(outfile.c_str(), "wb"); + if (f == NULL) { + fprintf(stderr, "can't open outputfile: %s\n", outfile.c_str()); + return 1; + } + fprintf(f, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); + fprintf(f, "%s\n", localizationbundle->ToString(NS_MAP).c_str()); + fclose(f); + } + + return err; +} + diff --git a/tools/localize/xmb.h b/tools/localize/xmb.h new file mode 100644 index 0000000..96492b1 --- /dev/null +++ b/tools/localize/xmb.h @@ -0,0 +1,11 @@ +#ifndef XMB_H +#define XMB_H + +#include <string> +#include <vector> + +using namespace std; + +int do_xlb_export(const string& outFile, const vector<string>& resFiles); + +#endif // XMB_H diff --git a/tools/makekeycodes/Android.mk b/tools/makekeycodes/Android.mk new file mode 100644 index 0000000..401d44e --- /dev/null +++ b/tools/makekeycodes/Android.mk @@ -0,0 +1,13 @@ +# Copyright 2005 The Android Open Source Project + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + makekeycodes.cpp + +LOCAL_MODULE := makekeycodes + +include $(BUILD_HOST_EXECUTABLE) + + diff --git a/tools/makekeycodes/makekeycodes.cpp b/tools/makekeycodes/makekeycodes.cpp new file mode 100644 index 0000000..16df774 --- /dev/null +++ b/tools/makekeycodes/makekeycodes.cpp @@ -0,0 +1,33 @@ +#include <stdio.h> +#include <ui/KeycodeLabels.h> + +int +main(int argc, char** argv) +{ + // TODO: Add full copyright. + printf("// Copyright (C) 2008 The Android Open Source Project\n"); + printf("//\n"); + printf("// This file is generated by makekeycodes from the definitions.\n"); + printf("// in includes/ui/KeycodeLabels.h.\n"); + printf("//\n"); + printf("// If you modify this, your changes will be overwritten.\n"); + printf("\n"); + printf("pacakge android.os;\n"); + printf("\n"); + printf("public class KeyEvent\n"); + printf("{\n"); + + for (int i=0; KEYCODES[i].literal != NULL; i++) { + printf(" public static final int KEYCODE_%s = 0x%08x;\n", + KEYCODES[i].literal, KEYCODES[i].value); + } + + printf("\n"); + for (int i=0; FLAGS[i].literal != NULL; i++) { + printf(" public static final int MODIFIER_%s = 0x%08x;\n", + FLAGS[i].literal, FLAGS[i].value); + } + + printf("}\n"); + return 0; +} diff --git a/tools/preload/20080522.compiled b/tools/preload/20080522.compiled Binary files differnew file mode 100644 index 0000000..a2af422 --- /dev/null +++ b/tools/preload/20080522.compiled diff --git a/tools/preload/Android.mk b/tools/preload/Android.mk new file mode 100644 index 0000000..e6fa103 --- /dev/null +++ b/tools/preload/Android.mk @@ -0,0 +1,23 @@ +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + ClassRank.java \ + Compile.java \ + LoadedClass.java \ + MemoryUsage.java \ + Operation.java \ + Policy.java \ + PrintCsv.java \ + PrintPsTree.java \ + Proc.java \ + Record.java \ + Root.java \ + WritePreloadedClassFile.java + +LOCAL_MODULE:= preload + +include $(BUILD_HOST_JAVA_LIBRARY) + +include $(call all-subdir-makefiles) diff --git a/tools/preload/ClassRank.java b/tools/preload/ClassRank.java new file mode 100644 index 0000000..3699b89 --- /dev/null +++ b/tools/preload/ClassRank.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2008 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. + */ + +import java.util.Comparator; + +/** + * Ranks classes for preloading based on how long their operations took + * and how early the operations happened. Higher ranked classes come first. + */ +class ClassRank implements Comparator<Operation> { + + /** + * Increase this number to add more weight to classes which were loaded + * earlier. + */ + static final int SEQUENCE_WEIGHT = 500; // 5 ms + + static final int BUCKET_SIZE = 5; + + public int compare(Operation a, Operation b) { + // Higher ranked operations should come first. + int result = rankOf(b) - rankOf(a); + if (result != 0) { + return result; + } + + // Make sure we don't drop one of two classes w/ the same rank. + // If a load and an initialization have the same rank, it's OK + // to treat the operations equally. + return a.loadedClass.name.compareTo(b.loadedClass.name); + } + + /** Ranks the given operation. */ + private static int rankOf(Operation o) { + return o.medianExclusiveTimeMicros() + + SEQUENCE_WEIGHT / (o.index / BUCKET_SIZE + 1); + } +} + + diff --git a/tools/preload/Compile.java b/tools/preload/Compile.java new file mode 100644 index 0000000..67258ef --- /dev/null +++ b/tools/preload/Compile.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2008 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. + */ + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +/** + * Parses and analyzes a log, pulling our PRELOAD information. If you have + * an emulator or device running in the background, this class will use it + * to measure and record the memory usage of each class. + * + * TODO: Should analyze lines and select substring dynamically (instead of hardcoded 19) + */ +public class Compile { + + public static void main(String[] args) throws IOException { + if (args.length != 2) { + System.err.println("Usage: Compile [log file] [output file]"); + System.exit(0); + } + + Root root = new Root(); + + List<Record> records = new ArrayList<Record>(); + + BufferedReader in = new BufferedReader(new InputStreamReader( + new FileInputStream(args[0]))); + + String line; + int lineNumber = 0; + while ((line = in.readLine()) != null) { + lineNumber++; + if (line.startsWith("I/PRELOAD")) { + try { + String clipped = line.substring(19); + records.add(new Record(clipped, lineNumber)); + } catch (RuntimeException e) { + throw new RuntimeException( + "Exception while recording line " + lineNumber + ": " + line, e); + } + } + } + + for (Record record : records) { + root.indexProcess(record); + } + + for (Record record : records) { + root.indexClassOperation(record); + } + + in.close(); + + root.toFile(args[1]); + } +} diff --git a/tools/preload/LoadedClass.java b/tools/preload/LoadedClass.java new file mode 100644 index 0000000..5782807 --- /dev/null +++ b/tools/preload/LoadedClass.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2008 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. + */ + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * A loaded class. + */ +class LoadedClass implements Serializable, Comparable<LoadedClass> { + + private static final long serialVersionUID = 0; + + /** Class name. */ + final String name; + + /** Load operations. */ + final List<Operation> loads = new ArrayList<Operation>(); + + /** Static initialization operations. */ + final List<Operation> initializations = new ArrayList<Operation>(); + + /** Memory usage gathered by loading only this class in its own VM. */ + MemoryUsage memoryUsage = MemoryUsage.NOT_AVAILABLE; + + /** + * Whether or not this class was loaded in the system class loader. + */ + final boolean systemClass; + + /** Whether or not this class will be preloaded. */ + boolean preloaded; + + /** Constructs a new class. */ + LoadedClass(String name, boolean systemClass) { + this.name = name; + this.systemClass = systemClass; + } + + void measureMemoryUsage() { + this.memoryUsage = MemoryUsage.forClass(name); + } + + int mlt = -1; + + /** Median time to load this class. */ + int medianLoadTimeMicros() { + if (mlt != -1) { + return mlt; + } + + return mlt = calculateMedian(loads); + } + + int mit = -1; + + /** Median time to initialize this class. */ + int medianInitTimeMicros() { + if (mit != -1) { + return mit; + } + + return mit = calculateMedian(initializations); + } + + /** Calculates the median duration for a list of operations. */ + private static int calculateMedian(List<Operation> operations) { + int size = operations.size(); + if (size == 0) { + return 0; + } + + int[] times = new int[size]; + for (int i = 0; i < size; i++) { + times[i] = operations.get(i).exclusiveTimeMicros(); + } + + Arrays.sort(times); + int middle = size / 2; + if (size % 2 == 1) { + // Odd + return times[middle]; + } else { + // Even -- average the two. + return (times[middle - 1] + times[middle]) / 2; + } + } + + /** + * Counts loads by apps. + */ + int appLoads() { + return operationsByApps(loads); + } + + /** + * Counts inits by apps. + */ + int appInits() { + return operationsByApps(initializations); + } + + /** + * Counts number of app operations in the given list. + */ + private static int operationsByApps(List<Operation> operations) { + int byApps = 0; + for (Operation operation : operations) { + if (operation.process.isApplication()) { + byApps++; + } + } + return byApps; + } + + public int compareTo(LoadedClass o) { + return name.compareTo(o.name); + } + + @Override + public String toString() { + return name; + } + + /** + * Returns true if this class's initialization causes the given class to + * initialize. + */ + public boolean initializes(LoadedClass clazz, Set<LoadedClass> visited) { + // Avoid infinite recursion. + if (!visited.add(this)) { + return false; + } + + if (clazz == this) { + return true; + } + + for (Operation initialization : initializations) { + if (initialization.loadedClass.initializes(clazz, visited)) { + return true; + } + } + + return false; + } +} diff --git a/tools/preload/MemoryUsage.java b/tools/preload/MemoryUsage.java new file mode 100644 index 0000000..e5dfb2a --- /dev/null +++ b/tools/preload/MemoryUsage.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2008 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. + */ + +import java.io.Serializable; +import java.io.IOException; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Memory usage information. + */ +class MemoryUsage implements Serializable { + + private static final long serialVersionUID = 0; + + static final MemoryUsage NOT_AVAILABLE = new MemoryUsage(); + + static int errorCount = 0; + static final int MAXIMUM_ERRORS = 10; // give up after this many fails + + final int nativeSharedPages; + final int javaSharedPages; + final int otherSharedPages; + final int nativePrivatePages; + final int javaPrivatePages; + final int otherPrivatePages; + + final int allocCount; + final int allocSize; + final int freedCount; + final int freedSize; + final long nativeHeapSize; + + public MemoryUsage(String line) { + String[] parsed = line.split(","); + + nativeSharedPages = Integer.parseInt(parsed[1]); + javaSharedPages = Integer.parseInt(parsed[2]); + otherSharedPages = Integer.parseInt(parsed[3]); + nativePrivatePages = Integer.parseInt(parsed[4]); + javaPrivatePages = Integer.parseInt(parsed[5]); + otherPrivatePages = Integer.parseInt(parsed[6]); + allocCount = Integer.parseInt(parsed[7]); + allocSize = Integer.parseInt(parsed[8]); + freedCount = Integer.parseInt(parsed[9]); + freedSize = Integer.parseInt(parsed[10]); + nativeHeapSize = Long.parseLong(parsed[11]); + } + + MemoryUsage() { + nativeSharedPages = -1; + javaSharedPages = -1; + otherSharedPages = -1; + nativePrivatePages = -1; + javaPrivatePages = -1; + otherPrivatePages = -1; + + allocCount = -1; + allocSize = -1; + freedCount = -1; + freedSize = -1; + nativeHeapSize = -1; + } + + MemoryUsage(int nativeSharedPages, + int javaSharedPages, + int otherSharedPages, + int nativePrivatePages, + int javaPrivatePages, + int otherPrivatePages, + int allocCount, + int allocSize, + int freedCount, + int freedSize, + long nativeHeapSize) { + this.nativeSharedPages = nativeSharedPages; + this.javaSharedPages = javaSharedPages; + this.otherSharedPages = otherSharedPages; + this.nativePrivatePages = nativePrivatePages; + this.javaPrivatePages = javaPrivatePages; + this.otherPrivatePages = otherPrivatePages; + this.allocCount = allocCount; + this.allocSize = allocSize; + this.freedCount = freedCount; + this.freedSize = freedSize; + this.nativeHeapSize = nativeHeapSize; + } + + MemoryUsage subtract(MemoryUsage baseline) { + return new MemoryUsage( + nativeSharedPages - baseline.nativeSharedPages, + javaSharedPages - baseline.javaSharedPages, + otherSharedPages - baseline.otherSharedPages, + nativePrivatePages - baseline.nativePrivatePages, + javaPrivatePages - baseline.javaPrivatePages, + otherPrivatePages - baseline.otherPrivatePages, + allocCount - baseline.allocCount, + allocSize - baseline.allocSize, + freedCount - baseline.freedCount, + freedSize - baseline.freedSize, + nativeHeapSize - baseline.nativeHeapSize); + } + + int javaHeapSize() { + return allocSize - freedSize; + } + + int javaPagesInK() { + return (javaSharedPages + javaPrivatePages) * 4; + } + + int nativePagesInK() { + return (nativeSharedPages + nativePrivatePages) * 4; + } + int otherPagesInK() { + return (otherSharedPages + otherPrivatePages) * 4; + } + + /** + * Was this information available? + */ + boolean isAvailable() { + return nativeSharedPages != -1; + } + + /** + * Measures baseline memory usage. + */ + static MemoryUsage baseline() { + return forClass(null); + } + + private static final String CLASS_PATH = "-Xbootclasspath" + + ":/system/framework/core.jar" + + ":/system/framework/ext.jar" + + ":/system/framework/framework.jar" + + ":/system/framework/framework-tests.jar" + + ":/system/framework/services.jar" + + ":/system/framework/loadclass.jar"; + + private static final String[] GET_DIRTY_PAGES = { + "adb", "-e", "shell", "dalvikvm", CLASS_PATH, "LoadClass" }; + + /** + * Measures memory usage for the given class. + */ + static MemoryUsage forClass(String className) { + + // This is a coarse approximation for determining that no device is connected, + // or that the communication protocol has changed, but we'll keep going and stop whining. + if (errorCount >= MAXIMUM_ERRORS) { + return NOT_AVAILABLE; + } + + MeasureWithTimeout measurer = new MeasureWithTimeout(className); + + new Thread(measurer).start(); + + synchronized (measurer) { + if (measurer.memoryUsage == null) { + // Wait up to 10s. + try { + measurer.wait(30000); + } catch (InterruptedException e) { + System.err.println("Interrupted waiting for measurement."); + e.printStackTrace(); + return NOT_AVAILABLE; + } + + // If it's still null. + if (measurer.memoryUsage == null) { + System.err.println("Timed out while measuring " + + className + "."); + return NOT_AVAILABLE; + } + } + + System.err.println("Got memory usage for " + className + "."); + return measurer.memoryUsage; + } + } + + static class MeasureWithTimeout implements Runnable { + + final String className; + MemoryUsage memoryUsage = null; + + MeasureWithTimeout(String className) { + this.className = className; + } + + public void run() { + MemoryUsage measured = measure(); + + synchronized (this) { + memoryUsage = measured; + notifyAll(); + } + } + + private MemoryUsage measure() { + String[] commands = GET_DIRTY_PAGES; + if (className != null) { + List<String> commandList = new ArrayList<String>( + GET_DIRTY_PAGES.length + 1); + commandList.addAll(Arrays.asList(commands)); + commandList.add(className); + commands = commandList.toArray(new String[commandList.size()]); + } + + try { + final Process process = Runtime.getRuntime().exec(commands); + + final InputStream err = process.getErrorStream(); + + // Send error output to stderr. + Thread errThread = new Thread() { + @Override + public void run() { + copy(err, System.err); + } + }; + errThread.setDaemon(true); + errThread.start(); + + BufferedReader in = new BufferedReader( + new InputStreamReader(process.getInputStream())); + String line = in.readLine(); + if (line == null || !line.startsWith("DECAFBAD,")) { + System.err.println("Got bad response for " + className + + ": " + line); + errorCount += 1; + return NOT_AVAILABLE; + } + + in.close(); + err.close(); + process.destroy(); + + return new MemoryUsage(line); + } catch (IOException e) { + System.err.println("Error getting stats for " + + className + "."); + e.printStackTrace(); + return NOT_AVAILABLE; + } + } + + } + + /** + * Copies from one stream to another. + */ + private static void copy(InputStream in, OutputStream out) { + byte[] buffer = new byte[1024]; + int read; + try { + while ((read = in.read(buffer)) > -1) { + out.write(buffer, 0, read); + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/tools/preload/Operation.java b/tools/preload/Operation.java new file mode 100644 index 0000000..4f1938e --- /dev/null +++ b/tools/preload/Operation.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2008 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. + */ + +import java.util.List; +import java.util.ArrayList; +import java.io.Serializable; + +/** + * An operation with a duration. Could represent a class load or initialization. + */ +class Operation implements Serializable { + + private static final long serialVersionUID = 0; + + /** + * Type of operation. + */ + enum Type { + LOAD, INIT + } + + /** Process this operation occurred in. */ + final Proc process; + + /** Start time for this operation. */ + final long startTimeNanos; + + /** Index of this operation relative to its process. */ + final int index; + + /** Type of operation. */ + final Type type; + + /** End time for this operation. */ + long endTimeNanos = -1; + + /** The class that this operation loaded or initialized. */ + final LoadedClass loadedClass; + + /** Other operations that occurred during this one. */ + final List<Operation> subops = new ArrayList<Operation>(); + + /** Constructs a new operation. */ + Operation(Proc process, LoadedClass loadedClass, long startTimeNanos, + int index, Type type) { + this.process = process; + this.loadedClass = loadedClass; + this.startTimeNanos = startTimeNanos; + this.index = index; + this.type = type; + } + + /** + * Returns how long this class initialization and all the nested class + * initializations took. + */ + private long inclusiveTimeNanos() { + if (endTimeNanos == -1) { + throw new IllegalStateException("End time hasn't been set yet: " + + loadedClass.name); + } + + return endTimeNanos - startTimeNanos; + } + + /** + * Returns how long this class initialization took. + */ + int exclusiveTimeMicros() { + long exclusive = inclusiveTimeNanos(); + + for (Operation child : subops) { + exclusive -= child.inclusiveTimeNanos(); + } + + if (exclusive < 0) { + throw new AssertionError(loadedClass.name); + } + + return nanosToMicros(exclusive); + } + + /** Gets the median time that this operation took across all processes. */ + int medianExclusiveTimeMicros() { + switch (type) { + case LOAD: return loadedClass.medianLoadTimeMicros(); + case INIT: return loadedClass.medianInitTimeMicros(); + default: throw new AssertionError(); + } + } + + /** + * Converts nanoseconds to microseconds. + * + * @throws RuntimeException if overflow occurs + */ + private static int nanosToMicros(long nanos) { + long micros = nanos / 1000; + int microsInt = (int) micros; + if (microsInt != micros) { + throw new RuntimeException("Integer overflow: " + nanos); + } + return microsInt; + } + + /** + * Primarily for debugger support + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(type.toString()); + sb.append(' '); + sb.append(loadedClass.toString()); + if (subops.size() > 0) { + sb.append(" ("); + sb.append(subops.size()); + sb.append(" sub ops)"); + } + return sb.toString(); + } + +} diff --git a/tools/preload/Policy.java b/tools/preload/Policy.java new file mode 100644 index 0000000..554966b --- /dev/null +++ b/tools/preload/Policy.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2008 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. + */ + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * This is not instantiated - we just provide data for other classes to use + */ +public class Policy { + + /** + * This location (in the build system) of the preloaded-classes file. + */ + private static final String PRELOADED_CLASS_FILE = "frameworks/base/preloaded-classes"; + + /** + * The internal process name of the system process. Note, this also shows up as + * "system_process", e.g. in ddms. + */ + private static final String SYSTEM_SERVER_PROCESS_NAME = "system_server"; + + /** + * Names of non-application processes - these will not be checked for preloaded classes. + * + * TODO: Replace this hardcoded list with a walk up the parent chain looking for zygote. + */ + private static final Set<String> NOT_FROM_ZYGOTE = new HashSet<String>(Arrays.asList( + "zygote", + "dexopt", + "unknown", + SYSTEM_SERVER_PROCESS_NAME, + "com.android.development", + "app_process" // am & other shell commands + )); + + /** + * Long running services. These are restricted in their contribution to the preloader + * because their launch time is less critical. + */ + private static final Set<String> SERVICES = new HashSet<String>(Arrays.asList( + SYSTEM_SERVER_PROCESS_NAME, + "com.android.acore", + // Commented out to make sure DefaultTimeZones gets preloaded. + // "com.android.phone", + "com.google.process.content", + "android.process.media" + )); + + /** + * Classes which we shouldn't load from the Zygote. + */ + private static final Set<String> EXCLUDED_CLASSES = new HashSet<String>(Arrays.asList( + // Binders + "android.app.AlarmManager", + "android.app.SearchManager", + "android.os.FileObserver", + "com.android.server.PackageManagerService$AppDirObserver", + + // Threads + "android.os.AsyncTask", + "android.pim.ContactsAsyncHelper", + "java.lang.ProcessManager" + + )); + + /** + * No constructor - use static methods only + */ + private Policy() {} + + /** + * Returns the path/file name of the preloaded classes file that will be written + * by WritePreloadedClassFile. + */ + public static String getPreloadedClassFileName() { + return PRELOADED_CLASS_FILE; + } + + /** + * Reports if a given process name was created from zygote + */ + public static boolean isFromZygote(String processName) { + return !NOT_FROM_ZYGOTE.contains(processName); + } + + /** + * Reports if the given process name is a "long running" process or service + */ + public static boolean isService(String processName) { + return SERVICES.contains(processName); + } + + /** + * Reports if the given class should never be preloaded + */ + public static boolean isPreloadableClass(String className) { + return !EXCLUDED_CLASSES.contains(className); + } +} diff --git a/tools/preload/PrintCsv.java b/tools/preload/PrintCsv.java new file mode 100644 index 0000000..9f2a318 --- /dev/null +++ b/tools/preload/PrintCsv.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2008 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. + */ + +import java.io.IOException; +import java.io.FileInputStream; +import java.io.ObjectInputStream; +import java.io.BufferedInputStream; + +/** + * Prints raw information in CSV format. + */ +public class PrintCsv { + + public static void main(String[] args) + throws IOException, ClassNotFoundException { + if (args.length != 1) { + System.err.println("Usage: PrintCsv [compiled log file]"); + System.exit(0); + } + + Root root = Root.fromFile(args[0]); + + System.out.println("Name" + + ",Preloaded" + + ",Median Load Time (us)" + + ",Median Init Time (us)" + + ",Load Count" + + ",Init Count" + + ",Managed Heap (B)" + + ",Native Heap (B)" + + ",Managed Pages (kB)" + + ",Native Pages (kB)" + + ",Other Pages (kB)"); + + MemoryUsage baseline = root.baseline; + + for (LoadedClass loadedClass : root.loadedClasses.values()) { + if (!loadedClass.systemClass) { + continue; + } + + System.out.print(loadedClass.name); + System.out.print(','); + System.out.print(loadedClass.preloaded); + System.out.print(','); + System.out.print(loadedClass.medianLoadTimeMicros()); + System.out.print(','); + System.out.print(loadedClass.medianInitTimeMicros()); + System.out.print(','); + System.out.print(loadedClass.loads.size()); + System.out.print(','); + System.out.print(loadedClass.initializations.size()); + + if (loadedClass.memoryUsage.isAvailable()) { + MemoryUsage subtracted + = loadedClass.memoryUsage.subtract(baseline); + + System.out.print(','); + System.out.print(subtracted.javaHeapSize()); + System.out.print(','); + System.out.print(subtracted.nativeHeapSize); + System.out.print(','); + System.out.print(subtracted.javaPagesInK()); + System.out.print(','); + System.out.print(subtracted.nativePagesInK()); + System.out.print(','); + System.out.print(subtracted.otherPagesInK()); + + } else { + System.out.print(",n/a,n/a,n/a,n/a,n/a"); + } + + System.out.println(); + } + } +} diff --git a/tools/preload/PrintPsTree.java b/tools/preload/PrintPsTree.java new file mode 100644 index 0000000..22701fa --- /dev/null +++ b/tools/preload/PrintPsTree.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2008 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. + */ + +import java.io.IOException; +import java.io.FileInputStream; +import java.io.ObjectInputStream; +import java.io.BufferedInputStream; + +/** + * Prints raw information in CSV format. + */ +public class PrintPsTree { + + public static void main(String[] args) + throws IOException, ClassNotFoundException { + if (args.length != 1) { + System.err.println("Usage: PrintCsv [compiled log file]"); + System.exit(0); + } + + FileInputStream fin = new FileInputStream(args[0]); + ObjectInputStream oin = new ObjectInputStream( + new BufferedInputStream(fin)); + + Root root = (Root) oin.readObject(); + + for (Proc proc : root.processes.values()) { + if (proc.parent == null) { + proc.print(); + } + } + } +} diff --git a/tools/preload/Proc.java b/tools/preload/Proc.java new file mode 100644 index 0000000..22697f8 --- /dev/null +++ b/tools/preload/Proc.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2008 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. + */ + +import java.util.Set; +import java.util.HashSet; +import java.util.Arrays; +import java.util.List; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.Map; +import java.util.HashMap; +import java.util.Collections; +import java.util.TreeSet; +import java.io.Serializable; + +/** + * A Dalvik process. + */ +class Proc implements Serializable { + + private static final long serialVersionUID = 0; + + /** + * Default percentage of time to cut off of app class loading times. + */ + static final int PERCENTAGE_TO_PRELOAD = 75; + + /** + * Maximum number of classes to preload for a given process. + */ + static final int MAX_TO_PRELOAD = 100; + + /** Parent process. */ + final Proc parent; + + /** Process ID. */ + final int id; + + /** + * Name of this process. We may not have the correct name at first, i.e. + * some classes could have been loaded before the process name was set. + */ + String name; + + /** Child processes. */ + final List<Proc> children = new ArrayList<Proc>(); + + /** Maps thread ID to operation stack. */ + transient final Map<Integer, LinkedList<Operation>> stacks + = new HashMap<Integer, LinkedList<Operation>>(); + + /** Number of operations. */ + int operationCount; + + /** Sequential list of operations that happened in this process. */ + final List<Operation> operations = new ArrayList<Operation>(); + + /** List of past process names. */ + final List<String> nameHistory = new ArrayList<String>(); + + /** Constructs a new process. */ + Proc(Proc parent, int id) { + this.parent = parent; + this.id = id; + } + + /** Sets name of this process. */ + void setName(String name) { + if (!name.equals(this.name)) { + if (this.name != null) { + nameHistory.add(this.name); + } + this.name = name; + } + } + + /** + * Returns the percentage of time we should cut by preloading for this + * app. + */ + int percentageToPreload() { + return PERCENTAGE_TO_PRELOAD; + } + + /** + * Returns a list of classes which should be preloaded. + * + * @param takeAllClasses forces all classes to be taken (irrespective of ranking) + */ + List<LoadedClass> highestRankedClasses(boolean takeAllClasses) { + if (!isApplication()) { + return Collections.emptyList(); + } + + // Sort by rank. + Operation[] ranked = new Operation[operations.size()]; + ranked = operations.toArray(ranked); + Arrays.sort(ranked, new ClassRank()); + + // The percentage of time to save by preloading. + int timeToSave = totalTimeMicros() * percentageToPreload() / 100; + int timeSaved = 0; + + boolean service = Policy.isService(this.name); + + List<LoadedClass> highest = new ArrayList<LoadedClass>(); + for (Operation operation : ranked) { + + // These are actual ranking decisions, which can be overridden + if (!takeAllClasses) { + if (highest.size() >= MAX_TO_PRELOAD) { + System.out.println(name + " got " + + (timeSaved * 100 / timeToSave) + "% through"); + break; + } + + if (timeSaved >= timeToSave) { + break; + } + } + + // The remaining rules apply even to wired-down processes + if (!Policy.isPreloadableClass(operation.loadedClass.name)) { + continue; + } + + if (!operation.loadedClass.systemClass) { + continue; + } + + // Only load java.* class for services. + if (!service || operation.loadedClass.name.startsWith("java.")) { + highest.add(operation.loadedClass); + } + + // For services, still count the time even if it's not in java.* + timeSaved += operation.medianExclusiveTimeMicros(); + } + + return highest; + } + + /** + * Total time spent class loading and initializing. + */ + int totalTimeMicros() { + int totalTime = 0; + for (Operation operation : operations) { + totalTime += operation.medianExclusiveTimeMicros(); + } + return totalTime; + } + + /** + * Returns true if this process is an app. + * + * TODO: Replace the hardcoded list with a walk up the parent chain looking for zygote. + */ + public boolean isApplication() { + return Policy.isFromZygote(name); + } + + /** + * Starts an operation. + * + * @param threadId thread the operation started in + * @param loadedClass class operation happened to + * @param time the operation started + */ + void startOperation(int threadId, LoadedClass loadedClass, long time, + Operation.Type type) { + Operation o = new Operation( + this, loadedClass, time, operationCount++, type); + operations.add(o); + + LinkedList<Operation> stack = stacks.get(threadId); + if (stack == null) { + stack = new LinkedList<Operation>(); + stacks.put(threadId, stack); + } + + if (!stack.isEmpty()) { + stack.getLast().subops.add(o); + } + + stack.add(o); + } + + /** + * Ends an operation. + * + * @param threadId thread the operation ended in + * @param loadedClass class operation happened to + * @param time the operation ended + */ + Operation endOperation(int threadId, String className, + LoadedClass loadedClass, long time) { + LinkedList<Operation> stack = stacks.get(threadId); + + if (stack == null || stack.isEmpty()) { + didNotStart(className); + return null; + } + + Operation o = stack.getLast(); + if (loadedClass != o.loadedClass) { + didNotStart(className); + return null; + } + + stack.removeLast(); + + o.endTimeNanos = time; + return o; + } + + /** + * Prints an error indicating that we saw the end of an operation but not + * the start. A bug in the logging framework which results in dropped logs + * causes this. + */ + private static void didNotStart(String name) { + System.err.println("Warning: An operation ended on " + name + + " but it never started!"); + } + + /** + * Prints this process tree to stdout. + */ + void print() { + print(""); + } + + /** + * Prints a child proc to standard out. + */ + private void print(String prefix) { + System.out.println(prefix + "id=" + id + ", name=" + name); + for (Proc child : children) { + child.print(prefix + " "); + } + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/tools/preload/Record.java b/tools/preload/Record.java new file mode 100644 index 0000000..b2be4d4 --- /dev/null +++ b/tools/preload/Record.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2008 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. + */ + +/** + * One line from the loaded-classes file. + */ +class Record { + + enum Type { + /** Start of initialization. */ + START_LOAD, + + /** End of initialization. */ + END_LOAD, + + /** Start of initialization. */ + START_INIT, + + /** End of initialization. */ + END_INIT + } + + /** Parent process ID. */ + final int ppid; + + /** Process ID. */ + final int pid; + + /** Thread ID. */ + final int tid; + + /** Process name. */ + final String processName; + + /** Class loader pointer. */ + final int classLoader; + + /** Type of record. */ + final Type type; + + /** Name of loaded class. */ + final String className; + + /** Record time (ns). */ + final long time; + + /** Source file line# */ + int sourceLineNumber; + + /** + * Parses a line from the loaded-classes file. + */ + Record(String line, int lineNum) { + char typeChar = line.charAt(0); + switch (typeChar) { + case '>': type = Type.START_LOAD; break; + case '<': type = Type.END_LOAD; break; + case '+': type = Type.START_INIT; break; + case '-': type = Type.END_INIT; break; + default: throw new AssertionError("Bad line: " + line); + } + + sourceLineNumber = lineNum; + + line = line.substring(1); + String[] parts = line.split(":"); + + ppid = Integer.parseInt(parts[0]); + pid = Integer.parseInt(parts[1]); + tid = Integer.parseInt(parts[2]); + + processName = decode(parts[3]).intern(); + + classLoader = Integer.parseInt(parts[4]); + className = vmTypeToLanguage(decode(parts[5])).intern(); + + time = Long.parseLong(parts[6]); + } + + /** + * Decode any escaping that may have been written to the log line. + * + * Supports unicode-style escaping: \\uXXXX = character in hex + * + * @param rawField the field as it was written into the log + * @result the same field with any escaped characters replaced + */ + String decode(String rawField) { + String result = rawField; + int offset = result.indexOf("\\u"); + while (offset >= 0) { + String before = result.substring(0, offset); + String escaped = result.substring(offset+2, offset+6); + String after = result.substring(offset+6); + + result = String.format("%s%c%s", before, Integer.parseInt(escaped, 16), after); + + // find another but don't recurse + offset = result.indexOf("\\u", offset + 1); + } + return result; + } + + /** + * Converts a VM-style name to a language-style name. + */ + String vmTypeToLanguage(String typeName) { + // if the typename is (null), just return it as-is. This is probably in dexopt and + // will be discarded anyway. NOTE: This corresponds to the case in dalvik/vm/oo/Class.c + // where dvmLinkClass() returns false and we clean up and exit. + if ("(null)".equals(typeName)) { + return typeName; + } + + if (!typeName.startsWith("L") || !typeName.endsWith(";") ) { + throw new AssertionError("Bad name: " + typeName + " in line " + sourceLineNumber); + } + + typeName = typeName.substring(1, typeName.length() - 1); + return typeName.replace("/", "."); + } +} diff --git a/tools/preload/Root.java b/tools/preload/Root.java new file mode 100644 index 0000000..949f9b7 --- /dev/null +++ b/tools/preload/Root.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2008 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. + */ + +import java.io.Serializable; +import java.io.IOException; +import java.io.Writer; +import java.io.BufferedWriter; +import java.io.OutputStreamWriter; +import java.io.FileOutputStream; +import java.io.FileInputStream; +import java.io.ObjectInputStream; +import java.io.BufferedInputStream; +import java.io.ObjectOutputStream; +import java.io.BufferedOutputStream; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.TreeSet; +import java.util.Arrays; +import java.nio.charset.Charset; + +/** + * Root of our data model. + */ +public class Root implements Serializable { + + private static final long serialVersionUID = 0; + + /** pid -> Proc */ + final Map<Integer, Proc> processes = new HashMap<Integer, Proc>(); + + /** Class name -> LoadedClass */ + final Map<String, LoadedClass> loadedClasses + = new HashMap<String, LoadedClass>(); + + final MemoryUsage baseline = MemoryUsage.baseline(); + + /** + * Records class loads and initializations. + */ + void indexClassOperation(Record record) { + Proc process = processes.get(record.pid); + + // Ignore dexopt output. It loads applications classes through the + // system class loader and messes us up. + if (record.processName.equals("dexopt")) { + return; + } + + String name = record.className; + LoadedClass loadedClass = loadedClasses.get(name); + Operation o = null; + + switch (record.type) { + case START_LOAD: + case START_INIT: + if (loadedClass == null) { + loadedClass = new LoadedClass( + name, record.classLoader == 0); + if (loadedClass.systemClass) { + // Only measure memory for classes in the boot + // classpath. + loadedClass.measureMemoryUsage(); + } + loadedClasses.put(name, loadedClass); + } + break; + + case END_LOAD: + case END_INIT: + o = process.endOperation(record.tid, record.className, + loadedClass, record.time); + if (o == null) { + return; + } + } + + switch (record.type) { + case START_LOAD: + process.startOperation(record.tid, loadedClass, record.time, + Operation.Type.LOAD); + break; + + case START_INIT: + process.startOperation(record.tid, loadedClass, record.time, + Operation.Type.INIT); + break; + + case END_LOAD: + loadedClass.loads.add(o); + break; + + case END_INIT: + loadedClass.initializations.add(o); + break; + } + } + + /** + * Indexes information about the process from the given record. + */ + void indexProcess(Record record) { + Proc proc = processes.get(record.pid); + + if (proc == null) { + // Create a new process object. + Proc parent = processes.get(record.ppid); + proc = new Proc(parent, record.pid); + processes.put(proc.id, proc); + if (parent != null) { + parent.children.add(proc); + } + } + + proc.setName(record.processName); + } + + /** + * Writes this graph to a file. + */ + void toFile(String fileName) throws IOException { + FileOutputStream out = new FileOutputStream(fileName); + ObjectOutputStream oout = new ObjectOutputStream( + new BufferedOutputStream(out)); + + System.err.println("Writing object model..."); + + oout.writeObject(this); + + oout.close(); + + System.err.println("Done!"); + } + + /** + * Reads Root from a file. + */ + static Root fromFile(String fileName) + throws IOException, ClassNotFoundException { + FileInputStream fin = new FileInputStream(fileName); + ObjectInputStream oin = new ObjectInputStream( + new BufferedInputStream(fin)); + + Root root = (Root) oin.readObject(); + + oin.close(); + + return root; + } +} diff --git a/tools/preload/WritePreloadedClassFile.java b/tools/preload/WritePreloadedClassFile.java new file mode 100644 index 0000000..d87b1f0 --- /dev/null +++ b/tools/preload/WritePreloadedClassFile.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2008 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. + */ + +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * Writes /frameworks/base/preloaded-classes. Also updates LoadedClass.preloaded + * fields and writes over compiled log file. + */ +public class WritePreloadedClassFile { + + public static void main(String[] args) throws IOException, ClassNotFoundException { + + // Process command-line arguments first + List<String> wiredProcesses = new ArrayList<String>(); + String inputFileName = null; + int argOffset = 0; + try { + while ("--preload-all-process".equals(args[argOffset])) { + argOffset++; + wiredProcesses.add(args[argOffset++]); + } + + inputFileName = args[argOffset++]; + } catch (RuntimeException e) { + System.err.println("Usage: WritePreloadedClassFile " + + "[--preload-all-process process-name] " + + "[compiled log file]"); + System.exit(0); + } + + Root root = Root.fromFile(inputFileName); + + for (LoadedClass loadedClass : root.loadedClasses.values()) { + loadedClass.preloaded = false; + } + + Writer out = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(Policy.getPreloadedClassFileName()), + Charset.forName("US-ASCII"))); + + out.write("# Classes which are preloaded by com.android.internal.os.ZygoteInit.\n"); + out.write("# Automatically generated by /frameworks/base/tools/preload.\n"); + out.write("# percent=" + Proc.PERCENTAGE_TO_PRELOAD + ", weight=" + + ClassRank.SEQUENCE_WEIGHT + + ", bucket_size=" + ClassRank.BUCKET_SIZE + + "\n"); + for (String wiredProcess : wiredProcesses) { + out.write("# forcing classes loaded by: " + wiredProcess + "\n"); + } + + Set<LoadedClass> highestRanked = new TreeSet<LoadedClass>(); + for (Proc proc : root.processes.values()) { + // test to see if this is one of the wired-down ("take all classes") processes + boolean isWired = wiredProcesses.contains(proc.name); + + List<LoadedClass> highestForProc = proc.highestRankedClasses(isWired); + + System.out.println(proc.name + ": " + highestForProc.size()); + + for (LoadedClass loadedClass : highestForProc) { + loadedClass.preloaded = true; + } + highestRanked.addAll(highestForProc); + } + + for (LoadedClass loadedClass : highestRanked) { + out.write(loadedClass.name); + out.write('\n'); + } + + out.close(); + + System.out.println(highestRanked.size() + + " classes will be preloaded."); + + // Update data to reflect LoadedClass.preloaded changes. + root.toFile(inputFileName); + } +} diff --git a/tools/preload/loadclass/Android.mk b/tools/preload/loadclass/Android.mk new file mode 100644 index 0000000..435699d --- /dev/null +++ b/tools/preload/loadclass/Android.mk @@ -0,0 +1,8 @@ +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_MODULE := loadclass + +include $(BUILD_JAVA_LIBRARY) diff --git a/tools/preload/loadclass/LoadClass.java b/tools/preload/loadclass/LoadClass.java new file mode 100644 index 0000000..471cc84 --- /dev/null +++ b/tools/preload/loadclass/LoadClass.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2008 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. + */ + +import android.util.Log; +import android.os.Debug; + +/** + * Loads a class, runs the garbage collector, and prints showmap output. + * + * <p>Usage: dalvikvm LoadClass [class name] + */ +class LoadClass { + + public static void main(String[] args) { + System.loadLibrary("android_runtime"); + + if (registerNatives() < 0) { + throw new RuntimeException("Error registering natives."); + } + + Debug.startAllocCounting(); + + if (args.length > 0) { + try { + Class.forName(args[0]); + } catch (ClassNotFoundException e) { + Log.w("LoadClass", e); + return; + } + } + + System.gc(); + + int allocCount = Debug.getGlobalAllocCount(); + int allocSize = Debug.getGlobalAllocSize(); + int freedCount = Debug.getGlobalFreedCount(); + int freedSize = Debug.getGlobalFreedSize(); + long nativeHeapSize = Debug.getNativeHeapSize(); + + Debug.stopAllocCounting(); + + StringBuilder response = new StringBuilder("DECAFBAD"); + + int[] pages = new int[6]; + Debug.MemoryInfo memoryInfo = new Debug.MemoryInfo(); + Debug.getMemoryInfo(memoryInfo); + response.append(',').append(memoryInfo.nativeSharedDirty); + response.append(',').append(memoryInfo.dalvikSharedDirty); + response.append(',').append(memoryInfo.otherSharedDirty); + response.append(',').append(memoryInfo.nativePrivateDirty); + response.append(',').append(memoryInfo.dalvikPrivateDirty); + response.append(',').append(memoryInfo.otherPrivateDirty); + + response.append(',').append(allocCount); + response.append(',').append(allocSize); + response.append(',').append(freedCount); + response.append(',').append(freedSize); + response.append(',').append(nativeHeapSize); + + System.out.println(response.toString()); + } + + /** + * Registers native functions. See AndroidRuntime.cpp. + */ + static native int registerNatives(); +} diff --git a/tools/preload/preload.iml b/tools/preload/preload.iml new file mode 100644 index 0000000..d1fab57 --- /dev/null +++ b/tools/preload/preload.iml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module relativePaths="true" type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="false"> + <output url="file:///tmp/preload/" /> + <exclude-output /> + <output-test url="file:///tmp/preload/" /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$" isTestSource="false" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntryProperties /> + </component> +</module> + diff --git a/tools/preload/preload.ipr b/tools/preload/preload.ipr new file mode 100644 index 0000000..c5613ad --- /dev/null +++ b/tools/preload/preload.ipr @@ -0,0 +1,467 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project relativePaths="false" version="4"> + <component name="AntConfiguration"> + <defaultAnt bundledAnt="true" /> + </component> + <component name="BuildJarProjectSettings"> + <option name="BUILD_JARS_ON_MAKE" value="false" /> + </component> + <component name="ChangeBrowserSettings"> + <option name="MAIN_SPLITTER_PROPORTION" value="0.3" /> + <option name="MESSAGES_SPLITTER_PROPORTION" value="0.8" /> + <option name="USE_DATE_BEFORE_FILTER" value="false" /> + <option name="USE_DATE_AFTER_FILTER" value="false" /> + <option name="USE_CHANGE_BEFORE_FILTER" value="false" /> + <option name="USE_CHANGE_AFTER_FILTER" value="false" /> + <option name="DATE_BEFORE" value="" /> + <option name="DATE_AFTER" value="" /> + <option name="CHANGE_BEFORE" value="" /> + <option name="CHANGE_AFTER" value="" /> + <option name="USE_USER_FILTER" value="false" /> + <option name="USER" value="" /> + </component> + <component name="CodeStyleProjectProfileManger"> + <option name="PROJECT_PROFILE" /> + <option name="USE_PROJECT_LEVEL_SETTINGS" value="false" /> + </component> + <component name="CodeStyleSettingsManager"> + <option name="PER_PROJECT_SETTINGS" /> + <option name="USE_PER_PROJECT_SETTINGS" value="false" /> + </component> + <component name="CompilerConfiguration"> + <option name="DEFAULT_COMPILER" value="Javac" /> + <option name="DEPLOY_AFTER_MAKE" value="0" /> + <resourceExtensions> + <entry name=".+\.(properties|xml|html|dtd|tld)" /> + <entry name=".+\.(gif|png|jpeg|jpg)" /> + </resourceExtensions> + <wildcardResourcePatterns> + <entry name="?*.properties" /> + <entry name="?*.xml" /> + <entry name="?*.gif" /> + <entry name="?*.png" /> + <entry name="?*.jpeg" /> + <entry name="?*.jpg" /> + <entry name="?*.html" /> + <entry name="?*.dtd" /> + <entry name="?*.tld" /> + </wildcardResourcePatterns> + </component> + <component name="Cvs2Configuration"> + <option name="PRUNE_EMPTY_DIRECTORIES" value="true" /> + <option name="MERGING_MODE" value="0" /> + <option name="MERGE_WITH_BRANCH1_NAME" value="HEAD" /> + <option name="MERGE_WITH_BRANCH2_NAME" value="HEAD" /> + <option name="RESET_STICKY" value="false" /> + <option name="CREATE_NEW_DIRECTORIES" value="true" /> + <option name="DEFAULT_TEXT_FILE_SUBSTITUTION" value="kv" /> + <option name="PROCESS_UNKNOWN_FILES" value="false" /> + <option name="PROCESS_DELETED_FILES" value="false" /> + <option name="PROCESS_IGNORED_FILES" value="false" /> + <option name="RESERVED_EDIT" value="false" /> + <option name="CHECKOUT_DATE_OR_REVISION_SETTINGS"> + <value> + <option name="BRANCH" value="" /> + <option name="DATE" value="" /> + <option name="USE_BRANCH" value="false" /> + <option name="USE_DATE" value="false" /> + </value> + </option> + <option name="UPDATE_DATE_OR_REVISION_SETTINGS"> + <value> + <option name="BRANCH" value="" /> + <option name="DATE" value="" /> + <option name="USE_BRANCH" value="false" /> + <option name="USE_DATE" value="false" /> + </value> + </option> + <option name="SHOW_CHANGES_REVISION_SETTINGS"> + <value> + <option name="BRANCH" value="" /> + <option name="DATE" value="" /> + <option name="USE_BRANCH" value="false" /> + <option name="USE_DATE" value="false" /> + </value> + </option> + <option name="SHOW_OUTPUT" value="false" /> + <option name="ADD_WATCH_INDEX" value="0" /> + <option name="REMOVE_WATCH_INDEX" value="0" /> + <option name="UPDATE_KEYWORD_SUBSTITUTION" /> + <option name="MAKE_NEW_FILES_READONLY" value="false" /> + <option name="SHOW_CORRUPTED_PROJECT_FILES" value="0" /> + <option name="TAG_AFTER_PROJECT_COMMIT" value="false" /> + <option name="OVERRIDE_EXISTING_TAG_FOR_PROJECT" value="true" /> + <option name="TAG_AFTER_PROJECT_COMMIT_NAME" value="" /> + <option name="CLEAN_COPY" value="false" /> + </component> + <component name="DependenciesAnalyzeManager"> + <option name="myForwardDirection" value="false" /> + </component> + <component name="DependencyValidationManager"> + <option name="SKIP_IMPORT_STATEMENTS" value="false" /> + </component> + <component name="EclipseCompilerSettings"> + <option name="DEBUGGING_INFO" value="true" /> + <option name="GENERATE_NO_WARNINGS" value="true" /> + <option name="DEPRECATION" value="false" /> + <option name="ADDITIONAL_OPTIONS_STRING" value="" /> + <option name="MAXIMUM_HEAP_SIZE" value="128" /> + </component> + <component name="EclipseEmbeddedCompilerSettings"> + <option name="DEBUGGING_INFO" value="true" /> + <option name="GENERATE_NO_WARNINGS" value="true" /> + <option name="DEPRECATION" value="false" /> + <option name="ADDITIONAL_OPTIONS_STRING" value="" /> + <option name="MAXIMUM_HEAP_SIZE" value="128" /> + </component> + <component name="EntryPointsManager"> + <entry_points version="2.0" /> + </component> + <component name="ExportToHTMLSettings"> + <option name="PRINT_LINE_NUMBERS" value="false" /> + <option name="OPEN_IN_BROWSER" value="false" /> + <option name="OUTPUT_DIRECTORY" /> + </component> + <component name="IdProvider" IDEtalkID="D171F99B9178C1675593DC9A76A5CC7E" /> + <component name="InspectionProjectProfileManager"> + <option name="PROJECT_PROFILE" value="Project Default" /> + <option name="USE_PROJECT_LEVEL_SETTINGS" value="false" /> + <scopes /> + <profiles> + <profile version="1.0" is_locked="false"> + <option name="myName" value="Project Default" /> + <option name="myLocal" value="false" /> + <inspection_tool class="JavaDoc" level="WARNING" enabled="false"> + <option name="TOP_LEVEL_CLASS_OPTIONS"> + <value> + <option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" /> + <option name="REQUIRED_TAGS" value="" /> + </value> + </option> + <option name="INNER_CLASS_OPTIONS"> + <value> + <option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" /> + <option name="REQUIRED_TAGS" value="" /> + </value> + </option> + <option name="METHOD_OPTIONS"> + <value> + <option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" /> + <option name="REQUIRED_TAGS" value="@return@param@throws or @exception" /> + </value> + </option> + <option name="FIELD_OPTIONS"> + <value> + <option name="ACCESS_JAVADOC_REQUIRED_FOR" value="none" /> + <option name="REQUIRED_TAGS" value="" /> + </value> + </option> + <option name="IGNORE_DEPRECATED" value="false" /> + <option name="IGNORE_JAVADOC_PERIOD" value="true" /> + <option name="myAdditionalJavadocTags" value="" /> + </inspection_tool> + <inspection_tool class="OnDemandImport" level="WARNING" enabled="true" /> + <inspection_tool class="SamePackageImport" level="WARNING" enabled="true" /> + <inspection_tool class="JavaLangImport" level="WARNING" enabled="true" /> + <inspection_tool class="RedundantImport" level="WARNING" enabled="true" /> + <inspection_tool class="UnusedImport" level="WARNING" enabled="true" /> + </profile> + </profiles> + <list size="0" /> + </component> + <component name="JavacSettings"> + <option name="DEBUGGING_INFO" value="true" /> + <option name="GENERATE_NO_WARNINGS" value="false" /> + <option name="DEPRECATION" value="true" /> + <option name="ADDITIONAL_OPTIONS_STRING" value="" /> + <option name="MAXIMUM_HEAP_SIZE" value="128" /> + </component> + <component name="JavadocGenerationManager"> + <option name="OUTPUT_DIRECTORY" /> + <option name="OPTION_SCOPE" value="protected" /> + <option name="OPTION_HIERARCHY" value="true" /> + <option name="OPTION_NAVIGATOR" value="true" /> + <option name="OPTION_INDEX" value="true" /> + <option name="OPTION_SEPARATE_INDEX" value="true" /> + <option name="OPTION_DOCUMENT_TAG_USE" value="false" /> + <option name="OPTION_DOCUMENT_TAG_AUTHOR" value="false" /> + <option name="OPTION_DOCUMENT_TAG_VERSION" value="false" /> + <option name="OPTION_DOCUMENT_TAG_DEPRECATED" value="true" /> + <option name="OPTION_DEPRECATED_LIST" value="true" /> + <option name="OTHER_OPTIONS" value="" /> + <option name="HEAP_SIZE" /> + <option name="LOCALE" /> + <option name="OPEN_IN_BROWSER" value="true" /> + </component> + <component name="JikesSettings"> + <option name="JIKES_PATH" value="" /> + <option name="DEBUGGING_INFO" value="true" /> + <option name="DEPRECATION" value="true" /> + <option name="GENERATE_NO_WARNINGS" value="false" /> + <option name="IS_EMACS_ERRORS_MODE" value="true" /> + <option name="ADDITIONAL_OPTIONS_STRING" value="" /> + </component> + <component name="LogConsolePreferences"> + <option name="FILTER_ERRORS" value="false" /> + <option name="FILTER_WARNINGS" value="false" /> + <option name="FILTER_INFO" value="true" /> + <option name="CUSTOM_FILTER" /> + </component> + <component name="Palette2"> + <group name="Swing"> + <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" /> + </item> + <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" /> + </item> + <item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" /> + </item> + <item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true"> + <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" /> + </item> + <item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" /> + <initial-values> + <property name="text" value="Button" /> + </initial-values> + </item> + <item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" /> + <initial-values> + <property name="text" value="RadioButton" /> + </initial-values> + </item> + <item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" /> + <initial-values> + <property name="text" value="CheckBox" /> + </initial-values> + </item> + <item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" /> + <initial-values> + <property name="text" value="Label" /> + </initial-values> + </item> + <item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> + <preferred-size width="150" height="-1" /> + </default-constraints> + </item> + <item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> + <preferred-size width="150" height="-1" /> + </default-constraints> + </item> + <item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> + <preferred-size width="150" height="-1" /> + </default-constraints> + </item> + <item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" /> + </item> + <item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> + <preferred-size width="150" height="50" /> + </default-constraints> + </item> + <item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3"> + <preferred-size width="200" height="200" /> + </default-constraints> + </item> + <item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3"> + <preferred-size width="200" height="200" /> + </default-constraints> + </item> + <item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" /> + </item> + <item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" /> + </item> + <item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" /> + </item> + <item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" /> + </item> + <item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1"> + <preferred-size width="-1" height="20" /> + </default-constraints> + </item> + <item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false"> + <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" /> + </item> + <item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false"> + <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" /> + </item> + </group> + </component> + <component name="PerforceChangeBrowserSettings"> + <option name="USE_CLIENT_FILTER" value="true" /> + <option name="CLIENT" value="" /> + </component> + <component name="ProjectFileVersion" converted="true" /> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/preload.iml" filepath="$PROJECT_DIR$/preload.iml" /> + </modules> + </component> + <component name="ProjectRootManager" version="2" assert-keyword="true" jdk-15="true" project-jdk-name="1.5" project-jdk-type="JavaSDK"> + <output url="file:///tmp/preload" /> + </component> + <component name="RmicSettings"> + <option name="IS_EANABLED" value="false" /> + <option name="DEBUGGING_INFO" value="true" /> + <option name="GENERATE_NO_WARNINGS" value="false" /> + <option name="GENERATE_IIOP_STUBS" value="false" /> + <option name="ADDITIONAL_OPTIONS_STRING" value="" /> + </component> + <component name="StarteamConfiguration"> + <option name="SERVER" value="" /> + <option name="PORT" value="49201" /> + <option name="USER" value="" /> + <option name="PASSWORD" value="" /> + <option name="PROJECT" value="" /> + <option name="VIEW" value="" /> + <option name="ALTERNATIVE_WORKING_PATH" value="" /> + <option name="LOCK_ON_CHECKOUT" value="false" /> + <option name="UNLOCK_ON_CHECKIN" value="false" /> + </component> + <component name="Struts Assistant"> + <option name="showInputs" value="true" /> + <option name="resources"> + <value> + <option name="strutsPath" /> + <option name="strutsHelp" /> + </value> + </option> + <option name="selectedTaglibs" /> + <option name="selectedTaglibs" /> + <option name="myStrutsValidationEnabled" value="true" /> + <option name="myTilesValidationEnabled" value="true" /> + <option name="myValidatorValidationEnabled" value="true" /> + <option name="myReportErrorsAsWarnings" value="true" /> + </component> + <component name="SvnChangesBrowserSettings"> + <option name="USE_AUTHOR_FIELD" value="true" /> + <option name="AUTHOR" value="" /> + <option name="LOCATION" value="" /> + <option name="USE_PROJECT_SETTINGS" value="true" /> + <option name="USE_ALTERNATE_LOCATION" value="false" /> + </component> + <component name="SvnConfiguration"> + <option name="USER" value="" /> + <option name="PASSWORD" value="" /> + <option name="PROCESS_UNRESOLVED" value="false" /> + <option name="LAST_MERGED_REVISION" /> + <option name="UPDATE_RUN_STATUS" value="false" /> + <option name="UPDATE_RECURSIVELY" value="true" /> + <option name="MERGE_DRY_RUN" value="false" /> + </component> + <component name="VCS.FileViewConfiguration"> + <option name="SELECTED_STATUSES" value="DEFAULT" /> + <option name="SELECTED_COLUMNS" value="DEFAULT" /> + <option name="SHOW_FILTERS" value="true" /> + <option name="CUSTOMIZE_VIEW" value="true" /> + <option name="SHOW_FILE_HISTORY_AS_TREE" value="true" /> + </component> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Perforce" /> + </component> + <component name="VssConfiguration"> + <option name="CLIENT_PATH" value="" /> + <option name="SRCSAFEINI_PATH" value="" /> + <option name="USER_NAME" value="" /> + <option name="PWD" value="" /> + <option name="VSS_IS_INITIALIZED" value="false" /> + <CheckoutOptions> + <option name="COMMENT" value="" /> + <option name="DO_NOT_GET_LATEST_VERSION" value="false" /> + <option name="REPLACE_WRITABLE" value="false" /> + <option name="RECURSIVE" value="false" /> + </CheckoutOptions> + <CheckinOptions> + <option name="COMMENT" value="" /> + <option name="KEEP_CHECKED_OUT" value="false" /> + <option name="RECURSIVE" value="false" /> + </CheckinOptions> + <AddOptions> + <option name="STORE_ONLY_LATEST_VERSION" value="false" /> + <option name="CHECK_OUT_IMMEDIATELY" value="false" /> + <option name="FILE_TYPE" value="0" /> + </AddOptions> + <UndocheckoutOptions> + <option name="MAKE_WRITABLE" value="false" /> + <option name="REPLACE_LOCAL_COPY" value="0" /> + <option name="RECURSIVE" value="false" /> + </UndocheckoutOptions> + <GetOptions> + <option name="REPLACE_WRITABLE" value="0" /> + <option name="MAKE_WRITABLE" value="false" /> + <option name="ANSWER_NEGATIVELY" value="false" /> + <option name="ANSWER_POSITIVELY" value="false" /> + <option name="RECURSIVE" value="false" /> + <option name="VERSION" /> + </GetOptions> + <VssConfigurableExcludedFilesTag /> + </component> + <component name="antWorkspaceConfiguration"> + <option name="IS_AUTOSCROLL_TO_SOURCE" value="false" /> + <option name="FILTER_TARGETS" value="false" /> + </component> + <component name="com.intellij.ide.util.scopeChooser.ScopeChooserConfigurable" proportions="" version="1"> + <option name="myLastEditedConfigurable" /> + </component> + <component name="com.intellij.jsf.UserDefinedFacesConfigs"> + <option name="USER_DEFINED_CONFIGS"> + <value> + <list size="0" /> + </value> + </option> + </component> + <component name="com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectRootMasterDetailsConfigurable" proportions="" version="1"> + <option name="myPlainMode" value="false" /> + <option name="myLastEditedConfigurable" /> + </component> + <component name="com.intellij.profile.ui.ErrorOptionsConfigurable" proportions="" version="1"> + <option name="myLastEditedConfigurable" /> + </component> + <component name="uidesigner-configuration"> + <option name="INSTRUMENT_CLASSES" value="true" /> + <option name="COPY_FORMS_RUNTIME_TO_OUTPUT" value="true" /> + <option name="DEFAULT_LAYOUT_MANAGER" value="GridLayoutManager" /> + </component> +</project> + |