diff options
Diffstat (limited to 'tools')
291 files changed, 67426 insertions, 0 deletions
diff --git a/tools/aapt/AaptAssets.cpp b/tools/aapt/AaptAssets.cpp new file mode 100644 index 000000000000..3797b49cc32d --- /dev/null +++ b/tools/aapt/AaptAssets.cpp @@ -0,0 +1,2689 @@ +// +// Copyright 2006 The Android Open Source Project +// + +#include "AaptAssets.h" +#include "ResourceFilter.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* kValuesDir = "values"; +static const char* kMipmapDir = "mipmap"; +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; +} + +// The default to use if no other ignore pattern is defined. +const char * const gDefaultIgnoreAssets = + "!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"; +// The ignore pattern that can be passed via --ignore-assets in Main.cpp +const char * gUserIgnoreAssets = NULL; + +static bool isHidden(const char *root, const char *path) +{ + // Patterns syntax: + // - Delimiter is : + // - Entry can start with the flag ! to avoid printing a warning + // about the file being ignored. + // - Entry can have the flag "<dir>" to match only directories + // or <file> to match only files. Default is to match both. + // - Entry can be a simplified glob "<prefix>*" or "*<suffix>" + // where prefix/suffix must have at least 1 character (so that + // we don't match a '*' catch-all pattern.) + // - The special filenames "." and ".." are always ignored. + // - Otherwise the full string is matched. + // - match is not case-sensitive. + + if (strcmp(path, ".") == 0 || strcmp(path, "..") == 0) { + return true; + } + + const char *delim = ":"; + const char *p = gUserIgnoreAssets; + if (!p || !p[0]) { + p = getenv("ANDROID_AAPT_IGNORE"); + } + if (!p || !p[0]) { + p = gDefaultIgnoreAssets; + } + char *patterns = strdup(p); + + bool ignore = false; + bool chatty = true; + char *matchedPattern = NULL; + + String8 fullPath(root); + fullPath.appendPath(path); + FileType type = getFileType(fullPath); + + int plen = strlen(path); + + // Note: we don't have strtok_r under mingw. + for(char *token = strtok(patterns, delim); + !ignore && token != NULL; + token = strtok(NULL, delim)) { + chatty = token[0] != '!'; + if (!chatty) token++; // skip ! + if (strncasecmp(token, "<dir>" , 5) == 0) { + if (type != kFileTypeDirectory) continue; + token += 5; + } + if (strncasecmp(token, "<file>", 6) == 0) { + if (type != kFileTypeRegular) continue; + token += 6; + } + + matchedPattern = token; + int n = strlen(token); + + if (token[0] == '*') { + // Match *suffix + token++; + n--; + if (n <= plen) { + ignore = strncasecmp(token, path + plen - n, n) == 0; + } + } else if (n > 1 && token[n - 1] == '*') { + // Match prefix* + ignore = strncasecmp(token, path, n - 1) == 0; + } else { + ignore = strcasecmp(token, path) == 0; + } + } + + if (ignore && chatty) { + fprintf(stderr, " (skipping %s '%s' due to ANDROID_AAPT_IGNORE pattern '%s')\n", + type == kFileTypeDirectory ? "dir" : "file", + path, + matchedPattern ? matchedPattern : ""); + } + + free(patterns); + return ignore; +} + +// ========================================================================= +// ========================================================================= +// ========================================================================= + +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; + } + + // layout direction + if (getLayoutDirectionName(part.string(), &config)) { + *axis = AXIS_LAYOUTDIR; + *value = (config.screenLayout&ResTable_config::MASK_LAYOUTDIR); + return 0; + } + + // smallest screen dp width + if (getSmallestScreenWidthDpName(part.string(), &config)) { + *axis = AXIS_SMALLESTSCREENWIDTHDP; + *value = config.smallestScreenWidthDp; + return 0; + } + + // screen dp width + if (getScreenWidthDpName(part.string(), &config)) { + *axis = AXIS_SCREENWIDTHDP; + *value = config.screenWidthDp; + return 0; + } + + // screen dp height + if (getScreenHeightDpName(part.string(), &config)) { + *axis = AXIS_SCREENHEIGHTDP; + *value = config.screenHeightDp; + return 0; + } + + // screen layout size + if (getScreenLayoutSizeName(part.string(), &config)) { + *axis = AXIS_SCREENLAYOUTSIZE; + *value = (config.screenLayout&ResTable_config::MASK_SCREENSIZE); + return 0; + } + + // screen layout long + if (getScreenLayoutLongName(part.string(), &config)) { + *axis = AXIS_SCREENLAYOUTLONG; + *value = (config.screenLayout&ResTable_config::MASK_SCREENLONG); + return 0; + } + + // orientation + if (getOrientationName(part.string(), &config)) { + *axis = AXIS_ORIENTATION; + *value = config.orientation; + return 0; + } + + // ui mode type + if (getUiModeTypeName(part.string(), &config)) { + *axis = AXIS_UIMODETYPE; + *value = (config.uiMode&ResTable_config::MASK_UI_MODE_TYPE); + return 0; + } + + // ui mode night + if (getUiModeNightName(part.string(), &config)) { + *axis = AXIS_UIMODENIGHT; + *value = (config.uiMode&ResTable_config::MASK_UI_MODE_NIGHT); + 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 hidden + if (getNavHiddenName(part.string(), &config)) { + *axis = AXIS_NAVHIDDEN; + *value = config.inputFlags; + 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; +} + +uint32_t +AaptGroupEntry::getConfigValueForAxis(const ResTable_config& config, int axis) +{ + switch (axis) { + case AXIS_MCC: + return config.mcc; + case AXIS_MNC: + return config.mnc; + case AXIS_LANGUAGE: + return (((uint32_t)config.country[1]) << 24) | (((uint32_t)config.country[0]) << 16) + | (((uint32_t)config.language[1]) << 8) | (config.language[0]); + case AXIS_LAYOUTDIR: + return config.screenLayout&ResTable_config::MASK_LAYOUTDIR; + case AXIS_SCREENLAYOUTSIZE: + return config.screenLayout&ResTable_config::MASK_SCREENSIZE; + case AXIS_ORIENTATION: + return config.orientation; + case AXIS_UIMODETYPE: + return (config.uiMode&ResTable_config::MASK_UI_MODE_TYPE); + case AXIS_UIMODENIGHT: + return (config.uiMode&ResTable_config::MASK_UI_MODE_NIGHT); + case AXIS_DENSITY: + return config.density; + case AXIS_TOUCHSCREEN: + return config.touchscreen; + case AXIS_KEYSHIDDEN: + return config.inputFlags; + case AXIS_KEYBOARD: + return config.keyboard; + case AXIS_NAVIGATION: + return config.navigation; + case AXIS_SCREENSIZE: + return config.screenSize; + case AXIS_SMALLESTSCREENWIDTHDP: + return config.smallestScreenWidthDp; + case AXIS_SCREENWIDTHDP: + return config.screenWidthDp; + case AXIS_SCREENHEIGHTDP: + return config.screenHeightDp; + case AXIS_VERSION: + return config.version; + } + return 0; +} + +bool +AaptGroupEntry::configSameExcept(const ResTable_config& config, + const ResTable_config& otherConfig, int axis) +{ + for (int i=AXIS_START; i<=AXIS_END; i++) { + if (i == axis) { + continue; + } + if (getConfigValueForAxis(config, i) != getConfigValueForAxis(otherConfig, i)) { + return false; + } + } + return true; +} + +bool +AaptGroupEntry::initFromDirName(const char* dir, String8* resType) +{ + mParamsChanged = true; + + Vector<String8> parts; + + String8 mcc, mnc, loc, layoutsize, layoutlong, orient, den; + String8 touch, key, keysHidden, nav, navHidden, size, layoutDir, vers; + String8 uiModeType, uiModeNight, smallestwidthdp, widthdp, heightdp; + + 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()); + } + + if (getLayoutDirectionName(part.string())) { + layoutDir = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not layout direction: %s\n", part.string()); + } + + if (getSmallestScreenWidthDpName(part.string())) { + smallestwidthdp = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not smallest screen width dp: %s\n", part.string()); + } + + if (getScreenWidthDpName(part.string())) { + widthdp = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not screen width dp: %s\n", part.string()); + } + + if (getScreenHeightDpName(part.string())) { + heightdp = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not screen height dp: %s\n", part.string()); + } + + if (getScreenLayoutSizeName(part.string())) { + layoutsize = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not screen layout size: %s\n", part.string()); + } + + if (getScreenLayoutLongName(part.string())) { + layoutlong = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not screen layout long: %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()); + } + + // ui mode type + if (getUiModeTypeName(part.string())) { + uiModeType = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not ui mode type: %s\n", part.string()); + } + + // ui mode night + if (getUiModeNightName(part.string())) { + uiModeNight = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not ui mode night: %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()); + } + + // navigation hidden + if (getNavHiddenName(part.string())) { + navHidden = part; + + index++; + if (index == N) { + goto success; + } + part = parts[index]; + } else { + //printf("not navHidden: %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->screenLayoutSize = layoutsize; + this->screenLayoutLong = layoutlong; + this->smallestScreenWidthDp = smallestwidthdp; + this->screenWidthDp = widthdp; + this->screenHeightDp = heightdp; + this->orientation = orient; + this->uiModeType = uiModeType; + this->uiModeNight = uiModeNight; + this->density = den; + this->touchscreen = touch; + this->keysHidden = keysHidden; + this->keyboard = key; + this->navHidden = navHidden; + this->navigation = nav; + this->screenSize = size; + this->layoutDirection = layoutDir; + 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 += layoutDirection; + s += ","; + s += smallestScreenWidthDp; + s += ","; + s += screenWidthDp; + s += ","; + s += screenHeightDp; + s += ","; + s += screenLayoutSize; + s += ","; + s += screenLayoutLong; + s += ","; + s += this->orientation; + s += ","; + s += uiModeType; + s += ","; + s += uiModeNight; + s += ","; + s += density; + s += ","; + s += touchscreen; + s += ","; + s += keysHidden; + s += ","; + s += keyboard; + s += ","; + s += navHidden; + s += ","; + s += navigation; + s += ","; + s += screenSize; + s += ","; + s += version; + return s; +} + +String8 +AaptGroupEntry::toDirName(const String8& resType) const +{ + String8 s = resType; + if (this->mcc != "") { + if (s.length() > 0) { + s += "-"; + } + s += mcc; + } + if (this->mnc != "") { + if (s.length() > 0) { + s += "-"; + } + s += mnc; + } + if (this->locale != "") { + if (s.length() > 0) { + s += "-"; + } + s += locale; + } + if (this->layoutDirection != "") { + if (s.length() > 0) { + s += "-"; + } + s += layoutDirection; + } + if (this->smallestScreenWidthDp != "") { + if (s.length() > 0) { + s += "-"; + } + s += smallestScreenWidthDp; + } + if (this->screenWidthDp != "") { + if (s.length() > 0) { + s += "-"; + } + s += screenWidthDp; + } + if (this->screenHeightDp != "") { + if (s.length() > 0) { + s += "-"; + } + s += screenHeightDp; + } + if (this->screenLayoutSize != "") { + if (s.length() > 0) { + s += "-"; + } + s += screenLayoutSize; + } + if (this->screenLayoutLong != "") { + if (s.length() > 0) { + s += "-"; + } + s += screenLayoutLong; + } + if (this->orientation != "") { + if (s.length() > 0) { + s += "-"; + } + s += orientation; + } + if (this->uiModeType != "") { + if (s.length() > 0) { + s += "-"; + } + s += uiModeType; + } + if (this->uiModeNight != "") { + if (s.length() > 0) { + s += "-"; + } + s += uiModeNight; + } + if (this->density != "") { + if (s.length() > 0) { + s += "-"; + } + s += density; + } + if (this->touchscreen != "") { + if (s.length() > 0) { + s += "-"; + } + s += touchscreen; + } + if (this->keysHidden != "") { + if (s.length() > 0) { + s += "-"; + } + s += keysHidden; + } + if (this->keyboard != "") { + if (s.length() > 0) { + s += "-"; + } + s += keyboard; + } + if (this->navHidden != "") { + if (s.length() > 0) { + s += "-"; + } + s += navHidden; + } + if (this->navigation != "") { + if (s.length() > 0) { + s += "-"; + } + s += navigation; + } + if (this->screenSize != "") { + if (s.length() > 0) { + s += "-"; + } + s += screenSize; + } + if (this->version != "") { + if (s.length() > 0) { + 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; + + if (out) { + out->mnc = atoi(val); + if (out->mnc == 0) { + out->mnc = ACONFIGURATION_MNC_ZERO; + } + } + + return true; +} + +/* + * 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::getLayoutDirectionName(const char* name, ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_LAYOUTDIR) + | ResTable_config::LAYOUTDIR_ANY; + return true; + } else if (strcmp(name, "ldltr") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_LAYOUTDIR) + | ResTable_config::LAYOUTDIR_LTR; + return true; + } else if (strcmp(name, "ldrtl") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_LAYOUTDIR) + | ResTable_config::LAYOUTDIR_RTL; + return true; + } + + return false; +} + +bool AaptGroupEntry::getScreenLayoutSizeName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_ANY; + return true; + } else if (strcmp(name, "small") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_SMALL; + return true; + } else if (strcmp(name, "normal") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_NORMAL; + return true; + } else if (strcmp(name, "large") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_LARGE; + return true; + } else if (strcmp(name, "xlarge") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENSIZE) + | ResTable_config::SCREENSIZE_XLARGE; + return true; + } + + return false; +} + +bool AaptGroupEntry::getScreenLayoutLongName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENLONG) + | ResTable_config::SCREENLONG_ANY; + return true; + } else if (strcmp(name, "long") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENLONG) + | ResTable_config::SCREENLONG_YES; + return true; + } else if (strcmp(name, "notlong") == 0) { + if (out) out->screenLayout = + (out->screenLayout&~ResTable_config::MASK_SCREENLONG) + | ResTable_config::SCREENLONG_NO; + 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::getUiModeTypeName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_ANY; + return true; + } else if (strcmp(name, "desk") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_DESK; + return true; + } else if (strcmp(name, "car") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_CAR; + return true; + } else if (strcmp(name, "television") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_TELEVISION; + return true; + } else if (strcmp(name, "appliance") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_TYPE) + | ResTable_config::UI_MODE_TYPE_APPLIANCE; + return true; + } + + return false; +} + +bool AaptGroupEntry::getUiModeNightName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_NIGHT) + | ResTable_config::UI_MODE_NIGHT_ANY; + return true; + } else if (strcmp(name, "night") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_NIGHT) + | ResTable_config::UI_MODE_NIGHT_YES; + return true; + } else if (strcmp(name, "notnight") == 0) { + if (out) out->uiMode = + (out->uiMode&~ResTable_config::MASK_UI_MODE_NIGHT) + | ResTable_config::UI_MODE_NIGHT_NO; + return true; + } + + return false; +} + +bool AaptGroupEntry::getDensityName(const char* name, + ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) out->density = ResTable_config::DENSITY_DEFAULT; + return true; + } + + if (strcmp(name, "nodpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_NONE; + return true; + } + + if (strcmp(name, "ldpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_LOW; + return true; + } + + if (strcmp(name, "mdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_MEDIUM; + return true; + } + + if (strcmp(name, "tvdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_TV; + return true; + } + + if (strcmp(name, "hdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_HIGH; + return true; + } + + if (strcmp(name, "xhdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_XHIGH; + return true; + } + + if (strcmp(name, "xxhdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_XXHIGH; + return true; + } + + if (strcmp(name, "xxxhdpi") == 0) { + if (out) out->density = ResTable_config::DENSITY_XXXHIGH; + 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 = ResTable_config::MASK_KEYSHIDDEN; + value = ResTable_config::KEYSHIDDEN_ANY; + } else if (strcmp(name, "keysexposed") == 0) { + mask = ResTable_config::MASK_KEYSHIDDEN; + value = ResTable_config::KEYSHIDDEN_NO; + } else if (strcmp(name, "keyshidden") == 0) { + mask = ResTable_config::MASK_KEYSHIDDEN; + value = ResTable_config::KEYSHIDDEN_YES; + } else if (strcmp(name, "keyssoft") == 0) { + mask = ResTable_config::MASK_KEYSHIDDEN; + value = ResTable_config::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::getNavHiddenName(const char* name, + ResTable_config* out) +{ + uint8_t mask = 0; + uint8_t value = 0; + if (strcmp(name, kWildcardName) == 0) { + mask = ResTable_config::MASK_NAVHIDDEN; + value = ResTable_config::NAVHIDDEN_ANY; + } else if (strcmp(name, "navexposed") == 0) { + mask = ResTable_config::MASK_NAVHIDDEN; + value = ResTable_config::NAVHIDDEN_NO; + } else if (strcmp(name, "navhidden") == 0) { + mask = ResTable_config::MASK_NAVHIDDEN; + value = ResTable_config::NAVHIDDEN_YES; + } + + if (mask != 0) { + if (out) out->inputFlags = (out->inputFlags&~mask) | value; + 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::getSmallestScreenWidthDpName(const char* name, ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->smallestScreenWidthDp = out->SCREENWIDTH_ANY; + } + return true; + } + + if (*name != 's') return false; + name++; + if (*name != 'w') return false; + name++; + const char* x = name; + while (*x >= '0' && *x <= '9') x++; + if (x == name || x[0] != 'd' || x[1] != 'p' || x[2] != 0) return false; + String8 xName(name, x-name); + + if (out) { + out->smallestScreenWidthDp = (uint16_t)atoi(xName.string()); + } + + return true; +} + +bool AaptGroupEntry::getScreenWidthDpName(const char* name, ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->screenWidthDp = out->SCREENWIDTH_ANY; + } + return true; + } + + if (*name != 'w') return false; + name++; + const char* x = name; + while (*x >= '0' && *x <= '9') x++; + if (x == name || x[0] != 'd' || x[1] != 'p' || x[2] != 0) return false; + String8 xName(name, x-name); + + if (out) { + out->screenWidthDp = (uint16_t)atoi(xName.string()); + } + + return true; +} + +bool AaptGroupEntry::getScreenHeightDpName(const char* name, ResTable_config* out) +{ + if (strcmp(name, kWildcardName) == 0) { + if (out) { + out->screenHeightDp = out->SCREENWIDTH_ANY; + } + return true; + } + + if (*name != 'h') return false; + name++; + const char* x = name; + while (*x >= '0' && *x <= '9') x++; + if (x == name || x[0] != 'd' || x[1] != 'p' || x[2] != 0) return false; + String8 xName(name, x-name); + + if (out) { + out->screenHeightDp = (uint16_t)atoi(xName.string()); + } + + 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 = layoutDirection.compare(o.layoutDirection); + if (v == 0) v = vendor.compare(o.vendor); + if (v == 0) v = smallestScreenWidthDp.compare(o.smallestScreenWidthDp); + if (v == 0) v = screenWidthDp.compare(o.screenWidthDp); + if (v == 0) v = screenHeightDp.compare(o.screenHeightDp); + if (v == 0) v = screenLayoutSize.compare(o.screenLayoutSize); + if (v == 0) v = screenLayoutLong.compare(o.screenLayoutLong); + if (v == 0) v = orientation.compare(o.orientation); + if (v == 0) v = uiModeType.compare(o.uiModeType); + if (v == 0) v = uiModeNight.compare(o.uiModeNight); + 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 = navHidden.compare(o.navHidden); + 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; +} + +const ResTable_config& AaptGroupEntry::toParams() const +{ + if (!mParamsChanged) { + return mParams; + } + + mParamsChanged = false; + ResTable_config& params(mParams); + memset(¶ms, 0, sizeof(params)); + getMccName(mcc.string(), ¶ms); + getMncName(mnc.string(), ¶ms); + getLocaleName(locale.string(), ¶ms); + getLayoutDirectionName(layoutDirection.string(), ¶ms); + getSmallestScreenWidthDpName(smallestScreenWidthDp.string(), ¶ms); + getScreenWidthDpName(screenWidthDp.string(), ¶ms); + getScreenHeightDpName(screenHeightDp.string(), ¶ms); + getScreenLayoutSizeName(screenLayoutSize.string(), ¶ms); + getScreenLayoutLongName(screenLayoutLong.string(), ¶ms); + getOrientationName(orientation.string(), ¶ms); + getUiModeTypeName(uiModeType.string(), ¶ms); + getUiModeNightName(uiModeNight.string(), ¶ms); + getDensityName(density.string(), ¶ms); + getTouchscreenName(touchscreen.string(), ¶ms); + getKeysHiddenName(keysHidden.string(), ¶ms); + getKeyboardName(keyboard.string(), ¶ms); + getNavHiddenName(navHidden.string(), ¶ms); + getNavigationName(navigation.string(), ¶ms); + getScreenSizeName(screenSize.string(), ¶ms); + getVersionName(version.string(), ¶ms); + + // Fix up version number based on specified parameters. + int minSdk = 0; + if (params.smallestScreenWidthDp != ResTable_config::SCREENWIDTH_ANY + || params.screenWidthDp != ResTable_config::SCREENWIDTH_ANY + || params.screenHeightDp != ResTable_config::SCREENHEIGHT_ANY) { + minSdk = SDK_HONEYCOMB_MR2; + } else if ((params.uiMode&ResTable_config::MASK_UI_MODE_TYPE) + != ResTable_config::UI_MODE_TYPE_ANY + || (params.uiMode&ResTable_config::MASK_UI_MODE_NIGHT) + != ResTable_config::UI_MODE_NIGHT_ANY) { + minSdk = SDK_FROYO; + } else if ((params.screenLayout&ResTable_config::MASK_SCREENSIZE) + != ResTable_config::SCREENSIZE_ANY + || (params.screenLayout&ResTable_config::MASK_SCREENLONG) + != ResTable_config::SCREENLONG_ANY + || params.density != ResTable_config::DENSITY_DEFAULT) { + minSdk = SDK_DONUT; + } + + if (minSdk > params.sdkVersion) { + params.sdkVersion = minSdk; + } + + 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.toDirName(String8())); + 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; + } + +#if 0 + printf("Error adding file %s: group %s already exists in leaf=%s path=%s\n", + file->getSourceFile().string(), + file->getGroupEntry().toDirName(String8()).string(), + mLeaf.string(), mPath.string()); +#endif + + 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 String8& prefix) const +{ + printf("%s%s\n", prefix.string(), 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("%s Gen: (%s) %d bytes\n", prefix.string(), e.toDirName(String8()).string(), + (int)file->getSize()); + } else { + printf("%s Src: (%s) %s\n", prefix.string(), e.toDirName(String8()).string(), + file->getPrintableSource().string()); + } + //printf("%s File Group Entry: %s\n", prefix.string(), + // file->getGroupEntry().toDirName(String8()).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::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, + sp<FilePathStore>& fullResPaths) +{ + 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; + + String8 name(entry->d_name); + fileNames.add(name); + // Add fully qualified path for dependency purposes + // if we're collecting them + if (fullResPaths != NULL) { + fullResPaths->add(srcDir.appendPathCopy(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, fullResPaths); + 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 String8& prefix) const +{ + const size_t ND=getDirs().size(); + size_t i; + for (i=0; i<ND; i++) { + getDirs().valueAt(i)->print(prefix); + } + + const size_t NF=getFiles().size(); + for (i=0; i<NF; i++) { + getFiles().valueAt(i)->print(prefix); + } +} + +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; + +} + +// ========================================================================= +// ========================================================================= +// ========================================================================= + +status_t AaptSymbols::applyJavaSymbols(const sp<AaptSymbols>& javaSymbols) +{ + status_t err = NO_ERROR; + size_t N = javaSymbols->mSymbols.size(); + for (size_t i=0; i<N; i++) { + const String8& name = javaSymbols->mSymbols.keyAt(i); + const AaptSymbolEntry& entry = javaSymbols->mSymbols.valueAt(i); + ssize_t pos = mSymbols.indexOfKey(name); + if (pos < 0) { + entry.sourcePos.error("Symbol '%s' declared with <java-symbol> not defined\n", name.string()); + err = UNKNOWN_ERROR; + continue; + } + //printf("**** setting symbol #%d/%d %s to isJavaSymbol=%d\n", + // i, N, name.string(), entry.isJavaSymbol ? 1 : 0); + mSymbols.editValueAt(pos).isJavaSymbol = entry.isJavaSymbol; + } + + N = javaSymbols->mNestedSymbols.size(); + for (size_t i=0; i<N; i++) { + const String8& name = javaSymbols->mNestedSymbols.keyAt(i); + const sp<AaptSymbols>& symbols = javaSymbols->mNestedSymbols.valueAt(i); + ssize_t pos = mNestedSymbols.indexOfKey(name); + if (pos < 0) { + SourcePos pos; + pos.error("Java symbol dir %s not defined\n", name.string()); + err = UNKNOWN_ERROR; + continue; + } + //printf("**** applying java symbols in dir %s\n", name.string()); + status_t myerr = mNestedSymbols.valueAt(pos)->applyJavaSymbols(symbols); + if (myerr != NO_ERROR) { + err = myerr; + } + } + + return err; +} + +// ========================================================================= +// ========================================================================= +// ========================================================================= + +AaptAssets::AaptAssets() + : AaptDir(String8(), String8()), + mChanged(false), mHaveIncludedAssets(false), mRes(NULL) +{ +} + +const SortedVector<AaptGroupEntry>& AaptAssets::getGroupEntries() const { + if (mChanged) { + } + return mGroupEntries; +} + +status_t AaptAssets::addFile(const String8& name, const sp<AaptGroup>& file) +{ + mChanged = true; + return AaptDir::addFile(name, file); +} + +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(), mFullAssetPaths); + 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; + current->setFullResPaths(mFullResPaths); + } + 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(), mFullAssetPaths); + 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; + } + + count = filter(bundle); + 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, + sp<FilePathStore>& fullResPaths) +{ + ssize_t res = AaptDir::slurpFullTree(bundle, srcDir, kind, resType, fullResPaths); + 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; + } + + if (bundle->getMaxResVersion() != NULL && group.getVersionString().length() != 0) { + int maxResInt = atoi(bundle->getMaxResVersion()); + const char *verString = group.getVersionString().string(); + int dirVersionInt = atoi(verString + 1); // skip 'v' in version name + if (dirVersionInt > maxResInt) { + fprintf(stderr, "max res %d, skipping %s\n", maxResInt, entry->d_name); + continue; + } + } + + FileType type = getFileType(subdirName.string()); + + if (type == kFileTypeDirectory) { + sp<AaptDir> dir = makeDir(resType); + ssize_t res = dir->slurpFullTree(bundle, subdirName, group, + resType, mFullResPaths); + if (res < 0) { + count = res; + goto bail; + } + if (res > 0) { + mGroupEntries.add(group); + count += res; + } + + // Only add this directory if we don't already have a resource dir + // for the current type. This ensures that we only add the dir once + // for all configs. + sp<AaptDir> rdir = resDir(resType); + if (rdir == NULL) { + mResDirs.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; +} + +status_t AaptAssets::filter(Bundle* bundle) +{ + ResourceFilter reqFilter; + status_t err = reqFilter.parse(bundle->getConfigurations()); + if (err != NO_ERROR) { + return err; + } + + ResourceFilter prefFilter; + err = prefFilter.parse(bundle->getPreferredConfigurations()); + if (err != NO_ERROR) { + return err; + } + + if (reqFilter.isEmpty() && prefFilter.isEmpty()) { + return NO_ERROR; + } + + if (bundle->getVerbose()) { + if (!reqFilter.isEmpty()) { + printf("Applying required filter: %s\n", + bundle->getConfigurations()); + } + if (!prefFilter.isEmpty()) { + printf("Applying preferred filter: %s\n", + bundle->getPreferredConfigurations()); + } + } + + const Vector<sp<AaptDir> >& resdirs = mResDirs; + const size_t ND = resdirs.size(); + for (size_t i=0; i<ND; i++) { + const sp<AaptDir>& dir = resdirs.itemAt(i); + if (dir->getLeaf() == kValuesDir) { + // The "value" dir is special since a single file defines + // multiple resources, so we can not do filtering on the + // files themselves. + continue; + } + if (dir->getLeaf() == kMipmapDir) { + // We also skip the "mipmap" directory, since the point of this + // is to include all densities without stripping. If you put + // other configurations in here as well they won't be stripped + // either... So don't do that. Seriously. What is wrong with you? + continue; + } + + const size_t NG = dir->getFiles().size(); + for (size_t j=0; j<NG; j++) { + sp<AaptGroup> grp = dir->getFiles().valueAt(j); + + // First remove any configurations we know we don't need. + for (size_t k=0; k<grp->getFiles().size(); k++) { + sp<AaptFile> file = grp->getFiles().valueAt(k); + if (k == 0 && grp->getFiles().size() == 1) { + // If this is the only file left, we need to keep it. + // Otherwise the resource IDs we are using will be inconsistent + // with what we get when not stripping. Sucky, but at least + // for now we can rely on the back-end doing another filtering + // pass to take this out and leave us with this resource name + // containing no entries. + continue; + } + if (file->getPath().getPathExtension() == ".xml") { + // We can't remove .xml files at this point, because when + // we parse them they may add identifier resources, so + // removing them can cause our resource identifiers to + // become inconsistent. + continue; + } + const ResTable_config& config(file->getGroupEntry().toParams()); + if (!reqFilter.match(config)) { + if (bundle->getVerbose()) { + printf("Pruning unneeded resource: %s\n", + file->getPrintableSource().string()); + } + grp->removeFile(k); + k--; + } + } + + // Quick check: no preferred filters, nothing more to do. + if (prefFilter.isEmpty()) { + continue; + } + + // Now deal with preferred configurations. + for (int axis=AXIS_START; axis<=AXIS_END; axis++) { + for (size_t k=0; k<grp->getFiles().size(); k++) { + sp<AaptFile> file = grp->getFiles().valueAt(k); + if (k == 0 && grp->getFiles().size() == 1) { + // If this is the only file left, we need to keep it. + // Otherwise the resource IDs we are using will be inconsistent + // with what we get when not stripping. Sucky, but at least + // for now we can rely on the back-end doing another filtering + // pass to take this out and leave us with this resource name + // containing no entries. + continue; + } + if (file->getPath().getPathExtension() == ".xml") { + // We can't remove .xml files at this point, because when + // we parse them they may add identifier resources, so + // removing them can cause our resource identifiers to + // become inconsistent. + continue; + } + const ResTable_config& config(file->getGroupEntry().toParams()); + if (!prefFilter.match(axis, config)) { + // This is a resource we would prefer not to have. Check + // to see if have a similar variation that we would like + // to have and, if so, we can drop it. + for (size_t m=0; m<grp->getFiles().size(); m++) { + if (m == k) continue; + sp<AaptFile> mfile = grp->getFiles().valueAt(m); + const ResTable_config& mconfig(mfile->getGroupEntry().toParams()); + if (AaptGroupEntry::configSameExcept(config, mconfig, axis)) { + if (prefFilter.match(axis, mconfig)) { + if (bundle->getVerbose()) { + printf("Pruning unneeded resource: %s\n", + file->getPrintableSource().string()); + } + grp->removeFile(k); + k--; + break; + } + } + } + } + } + } + } + } + + return NO_ERROR; +} + +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; +} + +sp<AaptSymbols> AaptAssets::getJavaSymbolsFor(const String8& name) +{ + sp<AaptSymbols> sym = mJavaSymbols.valueFor(name); + if (sym == NULL) { + sym = new AaptSymbols(); + mJavaSymbols.add(name, sym); + } + return sym; +} + +status_t AaptAssets::applyJavaSymbols() +{ + size_t N = mJavaSymbols.size(); + for (size_t i=0; i<N; i++) { + const String8& name = mJavaSymbols.keyAt(i); + const sp<AaptSymbols>& symbols = mJavaSymbols.valueAt(i); + ssize_t pos = mSymbols.indexOfKey(name); + if (pos < 0) { + SourcePos pos; + pos.error("Java symbol dir %s not defined\n", name.string()); + return UNKNOWN_ERROR; + } + //printf("**** applying java symbols in dir %s\n", name.string()); + status_t err = mSymbols.valueAt(pos)->applyJavaSymbols(symbols); + if (err != NO_ERROR) { + return err; + } + } + + return NO_ERROR; +} + +bool AaptAssets::isJavaSymbol(const AaptSymbolEntry& sym, bool includePrivate) const { + //printf("isJavaSymbol %s: public=%d, includePrivate=%d, isJavaSymbol=%d\n", + // sym.name.string(), sym.isPublic ? 1 : 0, includePrivate ? 1 : 0, + // sym.isJavaSymbol ? 1 : 0); + if (!mHavePrivateSymbols) return true; + if (sym.isPublic) return true; + if (includePrivate && sym.isJavaSymbol) return true; + return false; +} + +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 String8& prefix) const +{ + String8 innerPrefix(prefix); + innerPrefix.append(" "); + String8 innerInnerPrefix(innerPrefix); + innerInnerPrefix.append(" "); + printf("%sConfigurations:\n", prefix.string()); + const size_t N=mGroupEntries.size(); + for (size_t i=0; i<N; i++) { + String8 cname = mGroupEntries.itemAt(i).toDirName(String8()); + printf("%s %s\n", prefix.string(), + cname != "" ? cname.string() : "(default)"); + } + + printf("\n%sFiles:\n", prefix.string()); + AaptDir::print(innerPrefix); + + printf("\n%sResource Dirs:\n", prefix.string()); + const Vector<sp<AaptDir> >& resdirs = mResDirs; + const size_t NR = resdirs.size(); + for (size_t i=0; i<NR; i++) { + const sp<AaptDir>& d = resdirs.itemAt(i); + printf("%s Type %s\n", prefix.string(), d->getLeaf().string()); + d->print(innerInnerPrefix); + } +} + +sp<AaptDir> AaptAssets::resDir(const String8& name) const +{ + const Vector<sp<AaptDir> >& resdirs = mResDirs; + const size_t N = resdirs.size(); + for (size_t i=0; i<N; i++) { + const sp<AaptDir>& d = resdirs.itemAt(i); + if (d->getLeaf() == name) { + return d; + } + } + return NULL; +} + +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 000000000000..5cfa91350063 --- /dev/null +++ b/tools/aapt/AaptAssets.h @@ -0,0 +1,633 @@ +// +// 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 <androidfw/AssetManager.h> +#include <androidfw/ResourceTypes.h> +#include <utils/KeyedVector.h> +#include <utils/RefBase.h> +#include <utils/SortedVector.h> +#include <utils/String8.h> +#include <utils/String8.h> +#include <utils/Vector.h> +#include "ZipFile.h" + +#include "Bundle.h" +#include "SourcePos.h" + +using namespace android; + + +extern const char * const gDefaultIgnoreAssets; +extern const char * gUserIgnoreAssets; + +bool valid_symbol_name(const String8& str); + +class AaptAssets; + +enum { + AXIS_NONE = 0, + AXIS_MCC = 1, + AXIS_MNC, + AXIS_LANGUAGE, + AXIS_REGION, + AXIS_SCREENLAYOUTSIZE, + AXIS_SCREENLAYOUTLONG, + AXIS_ORIENTATION, + AXIS_UIMODETYPE, + AXIS_UIMODENIGHT, + AXIS_DENSITY, + AXIS_TOUCHSCREEN, + AXIS_KEYSHIDDEN, + AXIS_KEYBOARD, + AXIS_NAVHIDDEN, + AXIS_NAVIGATION, + AXIS_SCREENSIZE, + AXIS_SMALLESTSCREENWIDTHDP, + AXIS_SCREENWIDTHDP, + AXIS_SCREENHEIGHTDP, + AXIS_LAYOUTDIR, + AXIS_VERSION, + + AXIS_START = AXIS_MCC, + AXIS_END = 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() : mParamsChanged(true) { } + AaptGroupEntry(const String8& _locale, const String8& _vendor) + : locale(_locale), vendor(_vendor), mParamsChanged(true) { } + + bool initFromDirName(const char* dir, String8* resType); + + static status_t parseNamePart(const String8& part, int* axis, uint32_t* value); + + static uint32_t getConfigValueForAxis(const ResTable_config& config, int axis); + + static bool configSameExcept(const ResTable_config& config, + const ResTable_config& otherConfig, int axis); + + 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 getScreenLayoutSizeName(const char* name, ResTable_config* out = NULL); + static bool getScreenLayoutLongName(const char* name, ResTable_config* out = NULL); + static bool getOrientationName(const char* name, ResTable_config* out = NULL); + static bool getUiModeTypeName(const char* name, ResTable_config* out = NULL); + static bool getUiModeNightName(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 getNavHiddenName(const char* name, ResTable_config* out = NULL); + static bool getScreenSizeName(const char* name, ResTable_config* out = NULL); + static bool getSmallestScreenWidthDpName(const char* name, ResTable_config* out = NULL); + static bool getScreenWidthDpName(const char* name, ResTable_config* out = NULL); + static bool getScreenHeightDpName(const char* name, ResTable_config* out = NULL); + static bool getLayoutDirectionName(const char* name, ResTable_config* out = NULL); + static bool getVersionName(const char* name, ResTable_config* out = NULL); + + int compare(const AaptGroupEntry& o) const; + + 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; + + const String8& getVersionString() const { return version; } + +private: + String8 mcc; + String8 mnc; + String8 locale; + String8 vendor; + String8 smallestScreenWidthDp; + String8 screenWidthDp; + String8 screenHeightDp; + String8 screenLayoutSize; + String8 screenLayoutLong; + String8 orientation; + String8 uiModeType; + String8 uiModeNight; + String8 density; + String8 touchscreen; + String8 keysHidden; + String8 keyboard; + String8 navHidden; + String8 navigation; + String8 screenSize; + String8 layoutDirection; + String8 version; + + mutable bool mParamsChanged; + mutable ResTable_config mParams; +}; + +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; +class FilePathStore; + +/** + * 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() { + free(mData); + } + + 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& prefix) const; + + String8 getPrintableSource() const; + +private: + String8 mLeaf; + String8 mPath; + + DefaultKeyedVector<AaptGroupEntry, sp<AaptFile> > mFiles; +}; + +/** + * A single directory of assets, which can contain 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; } + + virtual status_t addFile(const String8& name, const sp<AaptGroup>& file); + + void removeFile(const String8& name); + void removeDir(const String8& name); + + /* + * 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& prefix) const; + + String8 getPrintableSource() const; + +private: + friend class AaptAssets; + + status_t addDir(const String8& name, const sp<AaptDir>& dir); + sp<AaptDir> makeDir(const String8& name); + 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, + sp<FilePathStore>& fullResPaths); + + 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), isJavaSymbol(false), typeCode(TYPE_UNKNOWN) + { + } + AaptSymbolEntry(const String8& _name) + : name(_name), isPublic(false), isJavaSymbol(false), typeCode(TYPE_UNKNOWN) + { + } + AaptSymbolEntry(const AaptSymbolEntry& o) + : name(o.name), sourcePos(o.sourcePos), isPublic(o.isPublic) + , isJavaSymbol(o.isJavaSymbol), 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; + isJavaSymbol = o.isJavaSymbol; + comment = o.comment; + typeComment = o.typeComment; + typeCode = o.typeCode; + int32Val = o.int32Val; + stringVal = o.stringVal; + return *this; + } + + const String8 name; + + SourcePos sourcePos; + bool isPublic; + bool isJavaSymbol; + + 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; + } + + status_t makeSymbolJavaSymbol(const String8& name, const SourcePos& pos) { + if (!check_valid_symbol_name(name, pos, "symbol")) { + return BAD_VALUE; + } + AaptSymbolEntry& sym = edit_symbol(name, &pos); + sym.isJavaSymbol = 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; + } + + status_t applyJavaSymbols(const sp<AaptSymbols>& javaSymbols); + + 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 : public RefBase, + public KeyedVector<String8,sp<AaptGroup> > +{ +public: + ResourceTypeSet(); +}; + +// Storage for lists of fully qualified paths for +// resources encountered during slurping. +class FilePathStore : public RefBase, + public Vector<String8> +{ +public: + FilePathStore(); +}; + +/** + * Asset hierarchy being operated on. + */ +class AaptAssets : public AaptDir +{ +public: + AaptAssets(); + virtual ~AaptAssets() { delete mRes; } + + const String8& getPackage() const { return mPackage; } + void setPackage(const String8& package) { + mPackage = package; + mSymbolsPrivatePackage = package; + mHavePrivateSymbols = false; + } + + const SortedVector<AaptGroupEntry>& getGroupEntries() const; + + virtual status_t addFile(const String8& name, const sp<AaptGroup>& file); + + 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); + + void addGroupEntry(const AaptGroupEntry& entry) { mGroupEntries.add(entry); } + + ssize_t slurpFromArgs(Bundle* bundle); + + sp<AaptSymbols> getSymbolsFor(const String8& name); + + sp<AaptSymbols> getJavaSymbolsFor(const String8& name); + + status_t applyJavaSymbols(); + + const DefaultKeyedVector<String8, sp<AaptSymbols> >& getSymbols() const { return mSymbols; } + + String8 getSymbolsPrivatePackage() const { return mSymbolsPrivatePackage; } + void setSymbolsPrivatePackage(const String8& pkg) { + mSymbolsPrivatePackage = pkg; + mHavePrivateSymbols = mSymbolsPrivatePackage != mPackage; + } + + bool havePrivateSymbols() const { return mHavePrivateSymbols; } + + bool isJavaSymbol(const AaptSymbolEntry& sym, bool includePrivate) const; + + status_t buildIncludedResources(Bundle* bundle); + status_t addIncludedResources(const sp<AaptFile>& file); + const ResTable& getIncludedResources() const; + + void print(const String8& prefix) const; + + inline const Vector<sp<AaptDir> >& resDirs() const { return mResDirs; } + sp<AaptDir> resDir(const String8& name) const; + + 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) { delete mRes; mRes = res; } + + inline sp<FilePathStore>& getFullResPaths() { return mFullResPaths; } + inline void + setFullResPaths(sp<FilePathStore>& res) { mFullResPaths = res; } + + inline sp<FilePathStore>& getFullAssetPaths() { return mFullAssetPaths; } + inline void + setFullAssetPaths(sp<FilePathStore>& res) { mFullAssetPaths = res; } + +private: + virtual ssize_t slurpFullTree(Bundle* bundle, + const String8& srcDir, + const AaptGroupEntry& kind, + const String8& resType, + sp<FilePathStore>& fullResPaths); + + ssize_t slurpResourceTree(Bundle* bundle, const String8& srcDir); + ssize_t slurpResourceZip(Bundle* bundle, const char* filename); + + status_t filter(Bundle* bundle); + + String8 mPackage; + SortedVector<AaptGroupEntry> mGroupEntries; + DefaultKeyedVector<String8, sp<AaptSymbols> > mSymbols; + DefaultKeyedVector<String8, sp<AaptSymbols> > mJavaSymbols; + String8 mSymbolsPrivatePackage; + bool mHavePrivateSymbols; + + Vector<sp<AaptDir> > mResDirs; + + bool mChanged; + + bool mHaveIncludedAssets; + AssetManager mIncludedAssets; + + sp<AaptAssets> mOverlay; + KeyedVector<String8, sp<ResourceTypeSet> >* mRes; + + sp<FilePathStore> mFullResPaths; + sp<FilePathStore> mFullAssetPaths; +}; + +#endif // __AAPT_ASSETS_H + diff --git a/tools/aapt/Android.mk b/tools/aapt/Android.mk new file mode 100644 index 000000000000..452c60a6853e --- /dev/null +++ b/tools/aapt/Android.mk @@ -0,0 +1,103 @@ +# +# Copyright 2006 The Android Open Source Project +# +# Android Asset Packaging Tool +# + +# This tool is prebuilt if we're doing an app-only build. +ifeq ($(TARGET_BUILD_APPS),) + + +aapt_src_files := \ + AaptAssets.cpp \ + Command.cpp \ + CrunchCache.cpp \ + FileFinder.cpp \ + Main.cpp \ + Package.cpp \ + StringPool.cpp \ + XMLNode.cpp \ + ResourceFilter.cpp \ + ResourceIdCache.cpp \ + ResourceTable.cpp \ + Images.cpp \ + Resource.cpp \ + pseudolocalize.cpp \ + SourcePos.cpp \ + WorkQueue.cpp \ + ZipEntry.cpp \ + ZipFile.cpp \ + qsort_r_compat.c + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(aapt_src_files) + +LOCAL_CFLAGS += -Wno-format-y2k +ifeq (darwin,$(HOST_OS)) +LOCAL_CFLAGS += -D_DARWIN_UNLIMITED_STREAMS +endif + +LOCAL_CFLAGS += -DSTATIC_ANDROIDFW_FOR_TOOLS + +LOCAL_C_INCLUDES += external/libpng +LOCAL_C_INCLUDES += external/zlib + +LOCAL_STATIC_LIBRARIES := \ + libandroidfw \ + libutils \ + libcutils \ + libexpat \ + libpng \ + liblog + +ifeq ($(HOST_OS),linux) +LOCAL_LDLIBS += -lrt -ldl -lpthread +endif + +# Statically link libz for MinGW (Win SDK under Linux), +# and dynamically link for all others. +ifneq ($(strip $(USE_MINGW)),) + LOCAL_STATIC_LIBRARIES += libz +else + LOCAL_LDLIBS += -lz +endif + +LOCAL_MODULE := aapt + +include $(BUILD_HOST_EXECUTABLE) + +# aapt for running on the device +# ========================================================= +ifneq ($(SDK_ONLY),true) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(aapt_src_files) + +LOCAL_MODULE := aapt + +LOCAL_C_INCLUDES += bionic +LOCAL_C_INCLUDES += bionic/libstdc++/include +LOCAL_C_INCLUDES += external/stlport/stlport +LOCAL_C_INCLUDES += external/libpng +LOCAL_C_INCLUDES += external/zlib + +LOCAL_CFLAGS += -Wno-non-virtual-dtor + +LOCAL_SHARED_LIBRARIES := \ + libandroidfw \ + libutils \ + libcutils \ + libpng \ + liblog \ + libz + +LOCAL_STATIC_LIBRARIES := \ + libstlport_static \ + libexpat_static + +include $(BUILD_EXECUTABLE) +endif + +endif # TARGET_BUILD_APPS diff --git a/tools/aapt/Bundle.h b/tools/aapt/Bundle.h new file mode 100644 index 000000000000..b67ca09e5acf --- /dev/null +++ b/tools/aapt/Bundle.h @@ -0,0 +1,309 @@ +// +// 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/Log.h> +#include <utils/threads.h> +#include <utils/List.h> +#include <utils/Errors.h> +#include <utils/String8.h> +#include <utils/Vector.h> + +enum { + SDK_CUPCAKE = 3, + SDK_DONUT = 4, + SDK_ECLAIR = 5, + SDK_ECLAIR_0_1 = 6, + SDK_MR1 = 7, + SDK_FROYO = 8, + SDK_HONEYCOMB_MR2 = 13, + SDK_ICE_CREAM_SANDWICH = 14, + SDK_ICE_CREAM_SANDWICH_MR1 = 15, +}; + +/* + * Things we can do. + */ +typedef enum Command { + kCommandUnknown = 0, + kCommandVersion, + kCommandList, + kCommandDump, + kCommandAdd, + kCommandRemove, + kCommandPackage, + kCommandCrunch, + kCommandSingleCrunch, +} 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), + mWantUTF16(false), mValues(false), mIncludeMetaData(false), + mCompressionMethod(0), mJunkPath(false), mOutputAPKFile(NULL), + mManifestPackageNameOverride(NULL), mInstrumentationPackageNameOverride(NULL), + mAutoAddOverlay(false), mGenDependencies(false), + mAssetSourceDir(NULL), + mCrunchedOutputDir(NULL), mProguardFile(NULL), + mAndroidManifestFile(NULL), mPublicOutputFile(NULL), + mRClassDir(NULL), mResourceIntermediatesDir(NULL), mManifestMinSdkVersion(NULL), + mMinSdkVersion(NULL), mTargetSdkVersion(NULL), mMaxSdkVersion(NULL), + mVersionCode(NULL), mVersionName(NULL), mCustomPackage(NULL), mExtraPackages(NULL), + mMaxResVersion(NULL), mDebugMode(false), mNonConstantId(false), mProduct(NULL), + mUseCrunchCache(false), mErrorOnFailedInsert(false), mOutputTextSymbols(NULL), + mSingleCrunchInputFile(NULL), mSingleCrunchOutputFile(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() const { 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; } + void setWantUTF16(bool val) { mWantUTF16 = val; } + bool getValues(void) const { return mValues; } + void setValues(bool val) { mValues = val; } + bool getIncludeMetaData(void) const { return mIncludeMetaData; } + void setIncludeMetaData(bool val) { mIncludeMetaData = val; } + int getCompressionMethod(void) const { return mCompressionMethod; } + void setCompressionMethod(int val) { mCompressionMethod = val; } + bool getJunkPath(void) const { return mJunkPath; } + void setJunkPath(bool val) { mJunkPath = val; } + const char* getOutputAPKFile() const { return mOutputAPKFile; } + void setOutputAPKFile(const char* val) { mOutputAPKFile = val; } + const char* getManifestPackageNameOverride() const { return mManifestPackageNameOverride; } + void setManifestPackageNameOverride(const char * val) { mManifestPackageNameOverride = val; } + const char* getInstrumentationPackageNameOverride() const { return mInstrumentationPackageNameOverride; } + void setInstrumentationPackageNameOverride(const char * val) { mInstrumentationPackageNameOverride = val; } + bool getAutoAddOverlay() { return mAutoAddOverlay; } + void setAutoAddOverlay(bool val) { mAutoAddOverlay = val; } + bool getGenDependencies() { return mGenDependencies; } + void setGenDependencies(bool val) { mGenDependencies = val; } + bool getErrorOnFailedInsert() { return mErrorOnFailedInsert; } + void setErrorOnFailedInsert(bool val) { mErrorOnFailedInsert = val; } + + bool getUTF16StringsOption() { + return mWantUTF16 || !isMinSdkAtLeast(SDK_FROYO); + } + + /* + * Input options. + */ + const char* getAssetSourceDir() const { return mAssetSourceDir; } + void setAssetSourceDir(const char* dir) { mAssetSourceDir = dir; } + const char* getCrunchedOutputDir() const { return mCrunchedOutputDir; } + void setCrunchedOutputDir(const char* dir) { mCrunchedOutputDir = dir; } + const char* getProguardFile() const { return mProguardFile; } + void setProguardFile(const char* file) { mProguardFile = file; } + 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* getPreferredConfigurations() const { return mPreferredConfigurations.size() > 0 ? mPreferredConfigurations.string() : NULL; } + void addPreferredConfigurations(const char* val) { if (mPreferredConfigurations.size() > 0) { mPreferredConfigurations.append(","); mPreferredConfigurations.append(val); } else { mPreferredConfigurations = 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); } + + const char* getManifestMinSdkVersion() const { return mManifestMinSdkVersion; } + void setManifestMinSdkVersion(const char* val) { mManifestMinSdkVersion = val; } + const char* getMinSdkVersion() const { return mMinSdkVersion; } + void setMinSdkVersion(const char* val) { mMinSdkVersion = val; } + const char* getTargetSdkVersion() const { return mTargetSdkVersion; } + void setTargetSdkVersion(const char* val) { mTargetSdkVersion = val; } + const char* getMaxSdkVersion() const { return mMaxSdkVersion; } + void setMaxSdkVersion(const char* val) { mMaxSdkVersion = val; } + const char* getVersionCode() const { return mVersionCode; } + void setVersionCode(const char* val) { mVersionCode = val; } + const char* getVersionName() const { return mVersionName; } + void setVersionName(const char* val) { mVersionName = val; } + const char* getCustomPackage() const { return mCustomPackage; } + void setCustomPackage(const char* val) { mCustomPackage = val; } + const char* getExtraPackages() const { return mExtraPackages; } + void setExtraPackages(const char* val) { mExtraPackages = val; } + const char* getMaxResVersion() const { return mMaxResVersion; } + void setMaxResVersion(const char * val) { mMaxResVersion = val; } + bool getDebugMode() const { return mDebugMode; } + void setDebugMode(bool val) { mDebugMode = val; } + bool getNonConstantId() const { return mNonConstantId; } + void setNonConstantId(bool val) { mNonConstantId = val; } + const char* getProduct() const { return mProduct; } + void setProduct(const char * val) { mProduct = val; } + void setUseCrunchCache(bool val) { mUseCrunchCache = val; } + bool getUseCrunchCache() const { return mUseCrunchCache; } + const char* getOutputTextSymbols() const { return mOutputTextSymbols; } + void setOutputTextSymbols(const char* val) { mOutputTextSymbols = val; } + const char* getSingleCrunchInputFile() const { return mSingleCrunchInputFile; } + void setSingleCrunchInputFile(const char* val) { mSingleCrunchInputFile = val; } + const char* getSingleCrunchOutputFile() const { return mSingleCrunchOutputFile; } + void setSingleCrunchOutputFile(const char* val) { mSingleCrunchOutputFile = val; } + + /* + * 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 + + /* Certain features may only be available on a specific SDK level or + * above. SDK levels that have a non-numeric identifier are assumed + * to be newer than any SDK level that has a number designated. + */ + bool isMinSdkAtLeast(int desired) { + /* If the application specifies a minSdkVersion in the manifest + * then use that. Otherwise, check what the user specified on + * the command line. If neither, it's not available since + * the minimum SDK version is assumed to be 1. + */ + const char *minVer; + if (mManifestMinSdkVersion != NULL) { + minVer = mManifestMinSdkVersion; + } else if (mMinSdkVersion != NULL) { + minVer = mMinSdkVersion; + } else { + return false; + } + + char *end; + int minSdkNum = (int)strtol(minVer, &end, 0); + if (*end == '\0') { + if (minSdkNum < desired) { + return false; + } + } + return true; + } + +private: + /* commands & modifiers */ + Command mCmd; + bool mVerbose; + bool mAndroidList; + bool mForce; + int mGrayscaleTolerance; + bool mMakePackageDirs; + bool mUpdate; + bool mExtending; + bool mRequireLocalization; + bool mPseudolocalize; + bool mWantUTF16; + bool mValues; + bool mIncludeMetaData; + int mCompressionMethod; + bool mJunkPath; + const char* mOutputAPKFile; + const char* mManifestPackageNameOverride; + const char* mInstrumentationPackageNameOverride; + bool mAutoAddOverlay; + bool mGenDependencies; + const char* mAssetSourceDir; + const char* mCrunchedOutputDir; + const char* mProguardFile; + const char* mAndroidManifestFile; + const char* mPublicOutputFile; + const char* mRClassDir; + const char* mResourceIntermediatesDir; + android::String8 mConfigurations; + android::String8 mPreferredConfigurations; + android::Vector<const char*> mPackageIncludes; + android::Vector<const char*> mJarFiles; + android::Vector<const char*> mNoCompressExtensions; + android::Vector<const char*> mResourceSourceDirs; + + const char* mManifestMinSdkVersion; + const char* mMinSdkVersion; + const char* mTargetSdkVersion; + const char* mMaxSdkVersion; + const char* mVersionCode; + const char* mVersionName; + const char* mCustomPackage; + const char* mExtraPackages; + const char* mMaxResVersion; + bool mDebugMode; + bool mNonConstantId; + const char* mProduct; + bool mUseCrunchCache; + bool mErrorOnFailedInsert; + const char* mOutputTextSymbols; + const char* mSingleCrunchInputFile; + const char* mSingleCrunchOutputFile; + + /* file specification */ + int mArgc; + char* const* mArgv; + +#if 0 + /* misc stuff */ + int mPackageCount; +#endif + +}; + +#endif // __BUNDLE_H diff --git a/tools/aapt/CacheUpdater.h b/tools/aapt/CacheUpdater.h new file mode 100644 index 000000000000..0e65589529e5 --- /dev/null +++ b/tools/aapt/CacheUpdater.h @@ -0,0 +1,107 @@ +// +// Copyright 2011 The Android Open Source Project +// +// Abstraction of calls to system to make directories and delete files and +// wrapper to image processing. + +#ifndef CACHE_UPDATER_H +#define CACHE_UPDATER_H + +#include <utils/String8.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <stdio.h> +#include "Images.h" + +using namespace android; + +/** CacheUpdater + * This is a pure virtual class that declares abstractions of functions useful + * for managing a cache files. This manager is set up to be used in a + * mirror cache where the source tree is duplicated and filled with processed + * images. This class is abstracted to allow for dependency injection during + * unit testing. + * Usage: + * To update/add a file to the cache, call processImage + * To remove a file from the cache, call deleteFile + */ +class CacheUpdater { +public: + // Make sure all the directories along this path exist + virtual void ensureDirectoriesExist(String8 path) = 0; + + // Delete a file + virtual void deleteFile(String8 path) = 0; + + // Process an image from source out to dest + virtual void processImage(String8 source, String8 dest) = 0; +private: +}; + +/** SystemCacheUpdater + * This is an implementation of the above virtual cache updater specification. + * This implementations hits the filesystem to manage a cache and calls out to + * the PNG crunching in images.h to process images out to its cache components. + */ +class SystemCacheUpdater : public CacheUpdater { +public: + // Constructor to set bundle to pass to preProcessImage + SystemCacheUpdater (Bundle* b) + : bundle(b) { }; + + // Make sure all the directories along this path exist + virtual void ensureDirectoriesExist(String8 path) + { + // Check to see if we're dealing with a fully qualified path + String8 existsPath; + String8 toCreate; + String8 remains; + struct stat s; + + // Check optomistically to see if all directories exist. + // If something in the path doesn't exist, then walk the path backwards + // and find the place to start creating directories forward. + if (stat(path.string(),&s) == -1) { + // Walk backwards to find place to start creating directories + existsPath = path; + do { + // As we remove the end of existsPath add it to + // the string of paths to create. + toCreate = existsPath.getPathLeaf().appendPath(toCreate); + existsPath = existsPath.getPathDir(); + } while (stat(existsPath.string(),&s) == -1); + + // Walk forwards and build directories as we go + do { + // Advance to the next segment of the path + existsPath.appendPath(toCreate.walkPath(&remains)); + toCreate = remains; +#ifdef HAVE_MS_C_RUNTIME + _mkdir(existsPath.string()); +#else + mkdir(existsPath.string(), S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP); +#endif + } while (remains.length() > 0); + } //if + }; + + // Delete a file + virtual void deleteFile(String8 path) + { + if (remove(path.string()) != 0) + fprintf(stderr,"ERROR DELETING %s\n",path.string()); + }; + + // Process an image from source out to dest + virtual void processImage(String8 source, String8 dest) + { + // Make sure we're trying to write to a directory that is extant + ensureDirectoriesExist(dest.getPathDir()); + + preProcessImageToCache(bundle, source, dest); + }; +private: + Bundle* bundle; +}; + +#endif // CACHE_UPDATER_H
\ No newline at end of file diff --git a/tools/aapt/Command.cpp b/tools/aapt/Command.cpp new file mode 100644 index 000000000000..7fa8c9d89b3c --- /dev/null +++ b/tools/aapt/Command.cpp @@ -0,0 +1,2121 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Android Asset Packaging Tool main entry point. +// +#include "Main.h" +#include "Bundle.h" +#include "ResourceFilter.h" +#include "ResourceTable.h" +#include "Images.h" +#include "XMLNode.h" + +#include <utils/Log.h> +#include <utils/threads.h> +#include <utils/List.h> +#include <utils/Errors.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 Offset 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%% %8zd %s %08lx %s\n", + (long) entry->getUncompressedLen(), + compressionName(entry->getCompressionMethod()), + (long) entry->getCompressedLen(), + calcPercent(entry->getUncompressedLen(), + entry->getCompressedLen()), + (size_t) entry->getLFHOffset(), + 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 { +#ifndef HAVE_ANDROID_OS + printf("\nResource table:\n"); + res.print(false); +#endif + } + + 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; +} + +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, int32_t defValue = -1) +{ + ssize_t idx = indexOfAttribute(tree, attrRes); + if (idx < 0) { + return defValue; + } + Res_value value; + if (tree.getAttributeValue(idx, &value) != NO_ERROR) { + if (value.dataType < Res_value::TYPE_FIRST_INT + || value.dataType > Res_value::TYPE_LAST_INT) { + if (outError != NULL) { + *outError = "attribute is not an integer value"; + } + return defValue; + } + } + return value.data; +} + +static int32_t getResolvedIntegerAttribute(const ResTable* resTable, const ResXMLTree& tree, + uint32_t attrRes, String8* outError, int32_t defValue = -1) +{ + ssize_t idx = indexOfAttribute(tree, attrRes); + if (idx < 0) { + return defValue; + } + Res_value value; + if (tree.getAttributeValue(idx, &value) != NO_ERROR) { + if (value.dataType == Res_value::TYPE_REFERENCE) { + resTable->resolveReference(&value, 0); + } + if (value.dataType < Res_value::TYPE_FIRST_INT + || value.dataType > Res_value::TYPE_LAST_INT) { + if (outError != NULL) { + *outError = "attribute is not an integer value"; + } + return defValue; + } + } + 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(); +} + +static void getResolvedResourceAttribute(Res_value* value, const ResTable* resTable, + const ResXMLTree& tree, uint32_t attrRes, String8* outError) +{ + ssize_t idx = indexOfAttribute(tree, attrRes); + if (idx < 0) { + if (outError != NULL) { + *outError = "attribute could not be found"; + } + return; + } + if (tree.getAttributeValue(idx, value) != NO_ERROR) { + if (value->dataType == Res_value::TYPE_REFERENCE) { + resTable->resolveReference(value, 0); + } + // The attribute was found and was resolved if need be. + return; + } + if (outError != NULL) { + *outError = "error getting resolved resource attribute"; + } +} + +// These are attribute resource constants for the platform, as found +// in android.R.attr +enum { + LABEL_ATTR = 0x01010001, + ICON_ATTR = 0x01010002, + NAME_ATTR = 0x01010003, + DEBUGGABLE_ATTR = 0x0101000f, + VALUE_ATTR = 0x01010024, + VERSION_CODE_ATTR = 0x0101021b, + VERSION_NAME_ATTR = 0x0101021c, + SCREEN_ORIENTATION_ATTR = 0x0101001e, + MIN_SDK_VERSION_ATTR = 0x0101020c, + MAX_SDK_VERSION_ATTR = 0x01010271, + REQ_TOUCH_SCREEN_ATTR = 0x01010227, + REQ_KEYBOARD_TYPE_ATTR = 0x01010228, + REQ_HARD_KEYBOARD_ATTR = 0x01010229, + REQ_NAVIGATION_ATTR = 0x0101022a, + REQ_FIVE_WAY_NAV_ATTR = 0x01010232, + TARGET_SDK_VERSION_ATTR = 0x01010270, + TEST_ONLY_ATTR = 0x01010272, + ANY_DENSITY_ATTR = 0x0101026c, + GL_ES_VERSION_ATTR = 0x01010281, + SMALL_SCREEN_ATTR = 0x01010284, + NORMAL_SCREEN_ATTR = 0x01010285, + LARGE_SCREEN_ATTR = 0x01010286, + XLARGE_SCREEN_ATTR = 0x010102bf, + REQUIRED_ATTR = 0x0101028e, + SCREEN_SIZE_ATTR = 0x010102ca, + SCREEN_DENSITY_ATTR = 0x010102cb, + REQUIRES_SMALLEST_WIDTH_DP_ATTR = 0x01010364, + COMPATIBLE_WIDTH_LIMIT_DP_ATTR = 0x01010365, + LARGEST_WIDTH_LIMIT_DP_ATTR = 0x01010366, + PUBLIC_KEY_ATTR = 0x010103a6, +}; + +const char *getComponentName(String8 &pkgName, String8 &componentName) { + ssize_t idx = componentName.find("."); + String8 retStr(pkgName); + if (idx == 0) { + retStr += componentName; + } else if (idx < 0) { + retStr += "."; + retStr += componentName; + } else { + return componentName.string(); + } + return retStr.string(); +} + +static void printCompatibleScreens(ResXMLTree& tree) { + size_t len; + ResXMLTree::event_code_t code; + int depth = 0; + bool first = true; + printf("compatible-screens:"); + while ((code=tree.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::END_TAG) { + depth--; + if (depth < 0) { + break; + } + continue; + } + if (code != ResXMLTree::START_TAG) { + continue; + } + depth++; + String8 tag(tree.getElementName(&len)); + if (tag == "screen") { + int32_t screenSize = getIntegerAttribute(tree, + SCREEN_SIZE_ATTR, NULL, -1); + int32_t screenDensity = getIntegerAttribute(tree, + SCREEN_DENSITY_ATTR, NULL, -1); + if (screenSize > 0 && screenDensity > 0) { + if (!first) { + printf(","); + } + first = false; + printf("'%d/%d'", screenSize, screenDensity); + } + } + } + printf("\n"); +} + +/* + * Handle the "dump" command, to extract select data from an archive. + */ +extern char CONSOLE_DATA[2925]; // see EOF +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; + void* assetsCookie; + if (!assets.addAssetPath(String8(filename), &assetsCookie)) { + fprintf(stderr, "ERROR: dump failed because assets could not be loaded\n"); + return 1; + } + + // Make a dummy config for retrieving resources... we need to supply + // non-default values for some configs so that we can retrieve resources + // in the app that don't have a default. The most important of these is + // the API version because key resources like icons will have an implicit + // version if they are using newer config types like density. + ResTable_config config; + config.language[0] = 'e'; + config.language[1] = 'n'; + config.country[0] = 'U'; + config.country[1] = 'S'; + config.orientation = ResTable_config::ORIENTATION_PORT; + config.density = ResTable_config::DENSITY_MEDIUM; + config.sdkVersion = 10000; // Very high. + config.screenWidthDp = 320; + config.screenHeightDp = 480; + config.smallestScreenWidthDp = 320; + assets.setConfiguration(config); + + 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) { +#ifndef HAVE_ANDROID_OS + res.print(bundle->getValues()); +#endif + + } else if (strcmp("strings", option) == 0) { + const ResStringPool* pool = res.getTableStringBlock(0); + printStringPool(pool); + + } 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 %s 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); + tree.uninit(); + 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 %s 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()); + int req = getIntegerAttribute(tree, REQUIRED_ATTR, NULL, 1); + if (!req) { + printf("optional-permission: %s\n", name.string()); + } + } + } + } else if (strcmp("badging", option) == 0) { + Vector<String8> locales; + res.getLocales(&locales); + + Vector<ResTable_config> configs; + res.getConfigurations(&configs); + SortedVector<int> densities; + const size_t NC = configs.size(); + for (size_t i=0; i<NC; i++) { + int dens = configs[i].density; + if (dens == 0) { + dens = 160; + } + densities.add(dens); + } + + size_t len; + ResXMLTree::event_code_t code; + int depth = 0; + String8 error; + bool withinActivity = false; + bool isMainActivity = false; + bool isLauncherActivity = false; + bool isSearchable = false; + bool withinApplication = false; + bool withinReceiver = false; + bool withinService = false; + bool withinIntentFilter = false; + bool hasMainActivity = false; + bool hasOtherActivities = false; + bool hasOtherReceivers = false; + bool hasOtherServices = false; + bool hasWallpaperService = false; + bool hasImeService = false; + bool hasWidgetReceivers = false; + bool hasIntentFilter = false; + bool actMainActivity = false; + bool actWidgetReceivers = false; + bool actImeService = false; + bool actWallpaperService = false; + + // These two implement the implicit permissions that are granted + // to pre-1.6 applications. + bool hasWriteExternalStoragePermission = false; + bool hasReadPhoneStatePermission = false; + + // If an app requests write storage, they will also get read storage. + bool hasReadExternalStoragePermission = false; + + // Implement transition to read and write call log. + bool hasReadContactsPermission = false; + bool hasWriteContactsPermission = false; + bool hasReadCallLogPermission = false; + bool hasWriteCallLogPermission = false; + + // This next group of variables is used to implement a group of + // backward-compatibility heuristics necessitated by the addition of + // some new uses-feature constants in 2.1 and 2.2. In most cases, the + // heuristic is "if an app requests a permission but doesn't explicitly + // request the corresponding <uses-feature>, presume it's there anyway". + bool specCameraFeature = false; // camera-related + bool specCameraAutofocusFeature = false; + bool reqCameraAutofocusFeature = false; + bool reqCameraFlashFeature = false; + bool hasCameraPermission = false; + bool specLocationFeature = false; // location-related + bool specNetworkLocFeature = false; + bool reqNetworkLocFeature = false; + bool specGpsFeature = false; + bool reqGpsFeature = false; + bool hasMockLocPermission = false; + bool hasCoarseLocPermission = false; + bool hasGpsPermission = false; + bool hasGeneralLocPermission = false; + bool specBluetoothFeature = false; // Bluetooth API-related + bool hasBluetoothPermission = false; + bool specMicrophoneFeature = false; // microphone-related + bool hasRecordAudioPermission = false; + bool specWiFiFeature = false; + bool hasWiFiPermission = false; + bool specTelephonyFeature = false; // telephony-related + bool reqTelephonySubFeature = false; + bool hasTelephonyPermission = false; + bool specTouchscreenFeature = false; // touchscreen-related + bool specMultitouchFeature = false; + bool reqDistinctMultitouchFeature = false; + bool specScreenPortraitFeature = false; + bool specScreenLandscapeFeature = false; + bool reqScreenPortraitFeature = false; + bool reqScreenLandscapeFeature = false; + // 2.2 also added some other features that apps can request, but that + // have no corresponding permission, so we cannot implement any + // back-compatibility heuristic for them. The below are thus unnecessary + // (but are retained here for documentary purposes.) + //bool specCompassFeature = false; + //bool specAccelerometerFeature = false; + //bool specProximityFeature = false; + //bool specAmbientLightFeature = false; + //bool specLiveWallpaperFeature = false; + + int targetSdk = 0; + int smallScreen = 1; + int normalScreen = 1; + int largeScreen = 1; + int xlargeScreen = 1; + int anyDensity = 1; + int requiresSmallestWidthDp = 0; + int compatibleWidthLimitDp = 0; + int largestWidthLimitDp = 0; + String8 pkg; + String8 activityName; + String8 activityLabel; + String8 activityIcon; + String8 receiverName; + String8 serviceName; + while ((code=tree.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::END_TAG) { + depth--; + if (depth < 2) { + withinApplication = false; + } else if (depth < 3) { + if (withinActivity && isMainActivity && isLauncherActivity) { + const char *aName = getComponentName(pkg, activityName); + printf("launchable-activity:"); + if (aName != NULL) { + printf(" name='%s' ", aName); + } + printf(" label='%s' icon='%s'\n", + activityLabel.string(), + activityIcon.string()); + } + if (!hasIntentFilter) { + hasOtherActivities |= withinActivity; + hasOtherReceivers |= withinReceiver; + hasOtherServices |= withinService; + } + withinActivity = false; + withinService = false; + withinReceiver = false; + hasIntentFilter = false; + isMainActivity = isLauncherActivity = false; + } else if (depth < 4) { + if (withinIntentFilter) { + if (withinActivity) { + hasMainActivity |= actMainActivity; + hasOtherActivities |= !actMainActivity; + } else if (withinReceiver) { + hasWidgetReceivers |= actWidgetReceivers; + hasOtherReceivers |= !actWidgetReceivers; + } else if (withinService) { + hasImeService |= actImeService; + hasWallpaperService |= actWallpaperService; + hasOtherServices |= (!actImeService && !actWallpaperService); + } + } + withinIntentFilter = false; + } + continue; + } + if (code != ResXMLTree::START_TAG) { + continue; + } + depth++; + String8 tag(tree.getElementName(&len)); + //printf("Depth %d, %s\n", depth, tag.string()); + if (depth == 1) { + if (tag != "manifest") { + fprintf(stderr, "ERROR: manifest does not start with <manifest> tag\n"); + goto bail; + } + 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 = getResolvedAttribute(&res, 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) { + withinApplication = false; + if (tag == "application") { + withinApplication = true; + + String8 label; + const size_t NL = locales.size(); + for (size_t i=0; i<NL; i++) { + const char* localeStr = locales[i].string(); + assets.setLocale(localeStr != NULL ? localeStr : ""); + String8 llabel = getResolvedAttribute(&res, tree, LABEL_ATTR, &error); + if (llabel != "") { + if (localeStr == NULL || strlen(localeStr) == 0) { + label = llabel; + printf("application-label:'%s'\n", llabel.string()); + } else { + if (label == "") { + label = llabel; + } + printf("application-label-%s:'%s'\n", localeStr, + llabel.string()); + } + } + } + + ResTable_config tmpConfig = config; + const size_t ND = densities.size(); + for (size_t i=0; i<ND; i++) { + tmpConfig.density = densities[i]; + assets.setConfiguration(tmpConfig); + String8 icon = getResolvedAttribute(&res, tree, ICON_ATTR, &error); + if (icon != "") { + printf("application-icon-%d:'%s'\n", densities[i], icon.string()); + } + } + assets.setConfiguration(config); + + String8 icon = getResolvedAttribute(&res, tree, ICON_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:icon' attribute: %s\n", error.string()); + goto bail; + } + int32_t testOnly = getIntegerAttribute(tree, TEST_ONLY_ATTR, &error, 0); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:testOnly' attribute: %s\n", error.string()); + goto bail; + } + printf("application: label='%s' ", label.string()); + printf("icon='%s'\n", icon.string()); + if (testOnly != 0) { + printf("testOnly='%d'\n", testOnly); + } + + int32_t debuggable = getResolvedIntegerAttribute(&res, tree, DEBUGGABLE_ATTR, &error, 0); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:debuggable' attribute: %s\n", error.string()); + goto bail; + } + if (debuggable != 0) { + printf("application-debuggable\n"); + } + } else if (tag == "uses-sdk") { + int32_t code = getIntegerAttribute(tree, MIN_SDK_VERSION_ATTR, &error); + if (error != "") { + error = ""; + String8 name = getResolvedAttribute(&res, tree, MIN_SDK_VERSION_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:minSdkVersion' attribute: %s\n", + error.string()); + goto bail; + } + if (name == "Donut") targetSdk = 4; + printf("sdkVersion:'%s'\n", name.string()); + } else if (code != -1) { + targetSdk = code; + printf("sdkVersion:'%d'\n", code); + } + code = getIntegerAttribute(tree, MAX_SDK_VERSION_ATTR, NULL, -1); + if (code != -1) { + printf("maxSdkVersion:'%d'\n", code); + } + code = getIntegerAttribute(tree, TARGET_SDK_VERSION_ATTR, &error); + if (error != "") { + error = ""; + String8 name = getResolvedAttribute(&res, tree, TARGET_SDK_VERSION_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:targetSdkVersion' attribute: %s\n", + error.string()); + goto bail; + } + if (name == "Donut" && targetSdk < 4) targetSdk = 4; + printf("targetSdkVersion:'%s'\n", name.string()); + } else if (code != -1) { + if (targetSdk < code) { + targetSdk = code; + } + printf("targetSdkVersion:'%d'\n", code); + } + } else if (tag == "uses-configuration") { + int32_t reqTouchScreen = getIntegerAttribute(tree, + REQ_TOUCH_SCREEN_ATTR, NULL, 0); + int32_t reqKeyboardType = getIntegerAttribute(tree, + REQ_KEYBOARD_TYPE_ATTR, NULL, 0); + int32_t reqHardKeyboard = getIntegerAttribute(tree, + REQ_HARD_KEYBOARD_ATTR, NULL, 0); + int32_t reqNavigation = getIntegerAttribute(tree, + REQ_NAVIGATION_ATTR, NULL, 0); + int32_t reqFiveWayNav = getIntegerAttribute(tree, + REQ_FIVE_WAY_NAV_ATTR, NULL, 0); + printf("uses-configuration:"); + if (reqTouchScreen != 0) { + printf(" reqTouchScreen='%d'", reqTouchScreen); + } + if (reqKeyboardType != 0) { + printf(" reqKeyboardType='%d'", reqKeyboardType); + } + if (reqHardKeyboard != 0) { + printf(" reqHardKeyboard='%d'", reqHardKeyboard); + } + if (reqNavigation != 0) { + printf(" reqNavigation='%d'", reqNavigation); + } + if (reqFiveWayNav != 0) { + printf(" reqFiveWayNav='%d'", reqFiveWayNav); + } + printf("\n"); + } else if (tag == "supports-screens") { + smallScreen = getIntegerAttribute(tree, + SMALL_SCREEN_ATTR, NULL, 1); + normalScreen = getIntegerAttribute(tree, + NORMAL_SCREEN_ATTR, NULL, 1); + largeScreen = getIntegerAttribute(tree, + LARGE_SCREEN_ATTR, NULL, 1); + xlargeScreen = getIntegerAttribute(tree, + XLARGE_SCREEN_ATTR, NULL, 1); + anyDensity = getIntegerAttribute(tree, + ANY_DENSITY_ATTR, NULL, 1); + requiresSmallestWidthDp = getIntegerAttribute(tree, + REQUIRES_SMALLEST_WIDTH_DP_ATTR, NULL, 0); + compatibleWidthLimitDp = getIntegerAttribute(tree, + COMPATIBLE_WIDTH_LIMIT_DP_ATTR, NULL, 0); + largestWidthLimitDp = getIntegerAttribute(tree, + LARGEST_WIDTH_LIMIT_DP_ATTR, NULL, 0); + } else if (tag == "uses-feature") { + String8 name = getAttribute(tree, NAME_ATTR, &error); + + if (name != "" && error == "") { + int req = getIntegerAttribute(tree, + REQUIRED_ATTR, NULL, 1); + + if (name == "android.hardware.camera") { + specCameraFeature = true; + } else if (name == "android.hardware.camera.autofocus") { + // these have no corresponding permission to check for, + // but should imply the foundational camera permission + reqCameraAutofocusFeature = reqCameraAutofocusFeature || req; + specCameraAutofocusFeature = true; + } else if (req && (name == "android.hardware.camera.flash")) { + // these have no corresponding permission to check for, + // but should imply the foundational camera permission + reqCameraFlashFeature = true; + } else if (name == "android.hardware.location") { + specLocationFeature = true; + } else if (name == "android.hardware.location.network") { + specNetworkLocFeature = true; + reqNetworkLocFeature = reqNetworkLocFeature || req; + } else if (name == "android.hardware.location.gps") { + specGpsFeature = true; + reqGpsFeature = reqGpsFeature || req; + } else if (name == "android.hardware.bluetooth") { + specBluetoothFeature = true; + } else if (name == "android.hardware.touchscreen") { + specTouchscreenFeature = true; + } else if (name == "android.hardware.touchscreen.multitouch") { + specMultitouchFeature = true; + } else if (name == "android.hardware.touchscreen.multitouch.distinct") { + reqDistinctMultitouchFeature = reqDistinctMultitouchFeature || req; + } else if (name == "android.hardware.microphone") { + specMicrophoneFeature = true; + } else if (name == "android.hardware.wifi") { + specWiFiFeature = true; + } else if (name == "android.hardware.telephony") { + specTelephonyFeature = true; + } else if (req && (name == "android.hardware.telephony.gsm" || + name == "android.hardware.telephony.cdma")) { + // these have no corresponding permission to check for, + // but should imply the foundational telephony permission + reqTelephonySubFeature = true; + } else if (name == "android.hardware.screen.portrait") { + specScreenPortraitFeature = true; + } else if (name == "android.hardware.screen.landscape") { + specScreenLandscapeFeature = true; + } + printf("uses-feature%s:'%s'\n", + req ? "" : "-not-required", name.string()); + } else { + int vers = getIntegerAttribute(tree, + GL_ES_VERSION_ATTR, &error); + if (error == "") { + printf("uses-gl-es:'0x%x'\n", vers); + } + } + } else if (tag == "uses-permission") { + String8 name = getAttribute(tree, NAME_ATTR, &error); + if (name != "" && error == "") { + if (name == "android.permission.CAMERA") { + hasCameraPermission = true; + } else if (name == "android.permission.ACCESS_FINE_LOCATION") { + hasGpsPermission = true; + } else if (name == "android.permission.ACCESS_MOCK_LOCATION") { + hasMockLocPermission = true; + } else if (name == "android.permission.ACCESS_COARSE_LOCATION") { + hasCoarseLocPermission = true; + } else if (name == "android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" || + name == "android.permission.INSTALL_LOCATION_PROVIDER") { + hasGeneralLocPermission = true; + } else if (name == "android.permission.BLUETOOTH" || + name == "android.permission.BLUETOOTH_ADMIN") { + hasBluetoothPermission = true; + } else if (name == "android.permission.RECORD_AUDIO") { + hasRecordAudioPermission = true; + } else if (name == "android.permission.ACCESS_WIFI_STATE" || + name == "android.permission.CHANGE_WIFI_STATE" || + name == "android.permission.CHANGE_WIFI_MULTICAST_STATE") { + hasWiFiPermission = true; + } else if (name == "android.permission.CALL_PHONE" || + name == "android.permission.CALL_PRIVILEGED" || + name == "android.permission.MODIFY_PHONE_STATE" || + name == "android.permission.PROCESS_OUTGOING_CALLS" || + name == "android.permission.READ_SMS" || + name == "android.permission.RECEIVE_SMS" || + name == "android.permission.RECEIVE_MMS" || + name == "android.permission.RECEIVE_WAP_PUSH" || + name == "android.permission.SEND_SMS" || + name == "android.permission.WRITE_APN_SETTINGS" || + name == "android.permission.WRITE_SMS") { + hasTelephonyPermission = true; + } else if (name == "android.permission.WRITE_EXTERNAL_STORAGE") { + hasWriteExternalStoragePermission = true; + } else if (name == "android.permission.READ_EXTERNAL_STORAGE") { + hasReadExternalStoragePermission = true; + } else if (name == "android.permission.READ_PHONE_STATE") { + hasReadPhoneStatePermission = true; + } else if (name == "android.permission.READ_CONTACTS") { + hasReadContactsPermission = true; + } else if (name == "android.permission.WRITE_CONTACTS") { + hasWriteContactsPermission = true; + } else if (name == "android.permission.READ_CALL_LOG") { + hasReadCallLogPermission = true; + } else if (name == "android.permission.WRITE_CALL_LOG") { + hasWriteCallLogPermission = true; + } + printf("uses-permission:'%s'\n", name.string()); + int req = getIntegerAttribute(tree, REQUIRED_ATTR, NULL, 1); + if (!req) { + printf("optional-permission:'%s'\n", name.string()); + } + } else { + fprintf(stderr, "ERROR getting 'android:name' attribute: %s\n", + error.string()); + goto bail; + } + } else if (tag == "uses-package") { + String8 name = getAttribute(tree, NAME_ATTR, &error); + if (name != "" && error == "") { + printf("uses-package:'%s'\n", name.string()); + } else { + fprintf(stderr, "ERROR getting 'android:name' attribute: %s\n", + error.string()); + goto bail; + } + } else if (tag == "original-package") { + String8 name = getAttribute(tree, NAME_ATTR, &error); + if (name != "" && error == "") { + printf("original-package:'%s'\n", name.string()); + } else { + fprintf(stderr, "ERROR getting 'android:name' attribute: %s\n", + error.string()); + goto bail; + } + } else if (tag == "supports-gl-texture") { + String8 name = getAttribute(tree, NAME_ATTR, &error); + if (name != "" && error == "") { + printf("supports-gl-texture:'%s'\n", name.string()); + } else { + fprintf(stderr, "ERROR getting 'android:name' attribute: %s\n", + error.string()); + goto bail; + } + } else if (tag == "compatible-screens") { + printCompatibleScreens(tree); + depth--; + } else if (tag == "package-verifier") { + String8 name = getAttribute(tree, NAME_ATTR, &error); + if (name != "" && error == "") { + String8 publicKey = getAttribute(tree, PUBLIC_KEY_ATTR, &error); + if (publicKey != "" && error == "") { + printf("package-verifier: name='%s' publicKey='%s'\n", + name.string(), publicKey.string()); + } + } + } + } else if (depth == 3 && withinApplication) { + withinActivity = false; + withinReceiver = false; + withinService = false; + hasIntentFilter = false; + if(tag == "activity") { + withinActivity = true; + 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; + } + + int32_t orien = getResolvedIntegerAttribute(&res, tree, + SCREEN_ORIENTATION_ATTR, &error); + if (error == "") { + if (orien == 0 || orien == 6 || orien == 8) { + // Requests landscape, sensorLandscape, or reverseLandscape. + reqScreenLandscapeFeature = true; + } else if (orien == 1 || orien == 7 || orien == 9) { + // Requests portrait, sensorPortrait, or reversePortrait. + reqScreenPortraitFeature = true; + } + } + } else if (tag == "uses-library") { + String8 libraryName = getAttribute(tree, NAME_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:name' attribute for uses-library: %s\n", error.string()); + goto bail; + } + int req = getIntegerAttribute(tree, + REQUIRED_ATTR, NULL, 1); + printf("uses-library%s:'%s'\n", + req ? "" : "-not-required", libraryName.string()); + } else if (tag == "receiver") { + withinReceiver = true; + receiverName = getAttribute(tree, NAME_ATTR, &error); + + if (error != "") { + fprintf(stderr, "ERROR getting 'android:name' attribute for " + "receiver:%s\n", error.string()); + goto bail; + } + } else if (tag == "service") { + withinService = true; + serviceName = getAttribute(tree, NAME_ATTR, &error); + + if (error != "") { + fprintf(stderr, "ERROR getting 'android:name' attribute for " + "service:%s\n", error.string()); + goto bail; + } + } else if (bundle->getIncludeMetaData() && tag == "meta-data") { + String8 metaDataName = getAttribute(tree, NAME_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:name' attribute for " + "meta-data:%s\n", error.string()); + goto bail; + } + printf("meta-data: name='%s' ", metaDataName.string()); + Res_value value; + getResolvedResourceAttribute(&value, &res, tree, VALUE_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:value' attribute for " + "meta-data:%s\n", error.string()); + goto bail; + } + if (value.dataType == Res_value::TYPE_STRING) { + String8 metaDataValue = getAttribute(tree, value.data, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:value' attribute for " + "meta-data: %s\n", error.string()); + goto bail; + } + printf("value='%s'\n", metaDataValue.string()); + } else if (Res_value::TYPE_FIRST_INT <= value.dataType && + value.dataType <= Res_value::TYPE_LAST_INT) { + printf("value='%d'\n", value.data); + } else { + printf("value=(type 0x%x)0x%x", (int)value.dataType, (int)value.data); + } + } + } else if ((depth == 4) && (tag == "intent-filter")) { + hasIntentFilter = true; + withinIntentFilter = true; + actMainActivity = actWidgetReceivers = actImeService = actWallpaperService = + false; + } else if ((depth == 5) && withinIntentFilter) { + String8 action; + if (tag == "action") { + action = getAttribute(tree, NAME_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:name' attribute: %s\n", + error.string()); + goto bail; + } + if (withinActivity) { + if (action == "android.intent.action.MAIN") { + isMainActivity = true; + actMainActivity = true; + } + } else if (withinReceiver) { + if (action == "android.appwidget.action.APPWIDGET_UPDATE") { + actWidgetReceivers = true; + } + } else if (withinService) { + if (action == "android.view.InputMethod") { + actImeService = true; + } else if (action == "android.service.wallpaper.WallpaperService") { + actWallpaperService = true; + } + } + if (action == "android.intent.action.SEARCH") { + isSearchable = true; + } + } + + 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 (withinActivity) { + if (category == "android.intent.category.LAUNCHER") { + isLauncherActivity = true; + } + } + } + } + } + + // Pre-1.6 implicitly granted permission compatibility logic + if (targetSdk < 4) { + if (!hasWriteExternalStoragePermission) { + printf("uses-permission:'android.permission.WRITE_EXTERNAL_STORAGE'\n"); + printf("uses-implied-permission:'android.permission.WRITE_EXTERNAL_STORAGE'," \ + "'targetSdkVersion < 4'\n"); + hasWriteExternalStoragePermission = true; + } + if (!hasReadPhoneStatePermission) { + printf("uses-permission:'android.permission.READ_PHONE_STATE'\n"); + printf("uses-implied-permission:'android.permission.READ_PHONE_STATE'," \ + "'targetSdkVersion < 4'\n"); + } + } + + // If the application has requested WRITE_EXTERNAL_STORAGE, we will + // force them to always take READ_EXTERNAL_STORAGE as well. We always + // do this (regardless of target API version) because we can't have + // an app with write permission but not read permission. + if (!hasReadExternalStoragePermission && hasWriteExternalStoragePermission) { + printf("uses-permission:'android.permission.READ_EXTERNAL_STORAGE'\n"); + printf("uses-implied-permission:'android.permission.READ_EXTERNAL_STORAGE'," \ + "'requested WRITE_EXTERNAL_STORAGE'\n"); + } + + // Pre-JellyBean call log permission compatibility. + if (targetSdk < 16) { + if (!hasReadCallLogPermission && hasReadContactsPermission) { + printf("uses-permission:'android.permission.READ_CALL_LOG'\n"); + printf("uses-implied-permission:'android.permission.READ_CALL_LOG'," \ + "'targetSdkVersion < 16 and requested READ_CONTACTS'\n"); + } + if (!hasWriteCallLogPermission && hasWriteContactsPermission) { + printf("uses-permission:'android.permission.WRITE_CALL_LOG'\n"); + printf("uses-implied-permission:'android.permission.WRITE_CALL_LOG'," \ + "'targetSdkVersion < 16 and requested WRITE_CONTACTS'\n"); + } + } + + /* The following blocks handle printing "inferred" uses-features, based + * on whether related features or permissions are used by the app. + * Note that the various spec*Feature variables denote whether the + * relevant tag was *present* in the AndroidManfest, not that it was + * present and set to true. + */ + // Camera-related back-compatibility logic + if (!specCameraFeature) { + if (reqCameraFlashFeature) { + // if app requested a sub-feature (autofocus or flash) and didn't + // request the base camera feature, we infer that it meant to + printf("uses-feature:'android.hardware.camera'\n"); + printf("uses-implied-feature:'android.hardware.camera'," \ + "'requested android.hardware.camera.flash feature'\n"); + } else if (reqCameraAutofocusFeature) { + // if app requested a sub-feature (autofocus or flash) and didn't + // request the base camera feature, we infer that it meant to + printf("uses-feature:'android.hardware.camera'\n"); + printf("uses-implied-feature:'android.hardware.camera'," \ + "'requested android.hardware.camera.autofocus feature'\n"); + } else if (hasCameraPermission) { + // if app wants to use camera but didn't request the feature, we infer + // that it meant to, and further that it wants autofocus + // (which was the 1.0 - 1.5 behavior) + printf("uses-feature:'android.hardware.camera'\n"); + if (!specCameraAutofocusFeature) { + printf("uses-feature:'android.hardware.camera.autofocus'\n"); + printf("uses-implied-feature:'android.hardware.camera.autofocus'," \ + "'requested android.permission.CAMERA permission'\n"); + } + } + } + + // Location-related back-compatibility logic + if (!specLocationFeature && + (hasMockLocPermission || hasCoarseLocPermission || hasGpsPermission || + hasGeneralLocPermission || reqNetworkLocFeature || reqGpsFeature)) { + // if app either takes a location-related permission or requests one of the + // sub-features, we infer that it also meant to request the base location feature + printf("uses-feature:'android.hardware.location'\n"); + printf("uses-implied-feature:'android.hardware.location'," \ + "'requested a location access permission'\n"); + } + if (!specGpsFeature && hasGpsPermission) { + // if app takes GPS (FINE location) perm but does not request the GPS + // feature, we infer that it meant to + printf("uses-feature:'android.hardware.location.gps'\n"); + printf("uses-implied-feature:'android.hardware.location.gps'," \ + "'requested android.permission.ACCESS_FINE_LOCATION permission'\n"); + } + if (!specNetworkLocFeature && hasCoarseLocPermission) { + // if app takes Network location (COARSE location) perm but does not request the + // network location feature, we infer that it meant to + printf("uses-feature:'android.hardware.location.network'\n"); + printf("uses-implied-feature:'android.hardware.location.network'," \ + "'requested android.permission.ACCESS_COARSE_LOCATION permission'\n"); + } + + // Bluetooth-related compatibility logic + if (!specBluetoothFeature && hasBluetoothPermission && (targetSdk > 4)) { + // if app takes a Bluetooth permission but does not request the Bluetooth + // feature, we infer that it meant to + printf("uses-feature:'android.hardware.bluetooth'\n"); + printf("uses-implied-feature:'android.hardware.bluetooth'," \ + "'requested android.permission.BLUETOOTH or android.permission.BLUETOOTH_ADMIN " \ + "permission and targetSdkVersion > 4'\n"); + } + + // Microphone-related compatibility logic + if (!specMicrophoneFeature && hasRecordAudioPermission) { + // if app takes the record-audio permission but does not request the microphone + // feature, we infer that it meant to + printf("uses-feature:'android.hardware.microphone'\n"); + printf("uses-implied-feature:'android.hardware.microphone'," \ + "'requested android.permission.RECORD_AUDIO permission'\n"); + } + + // WiFi-related compatibility logic + if (!specWiFiFeature && hasWiFiPermission) { + // if app takes one of the WiFi permissions but does not request the WiFi + // feature, we infer that it meant to + printf("uses-feature:'android.hardware.wifi'\n"); + printf("uses-implied-feature:'android.hardware.wifi'," \ + "'requested android.permission.ACCESS_WIFI_STATE, " \ + "android.permission.CHANGE_WIFI_STATE, or " \ + "android.permission.CHANGE_WIFI_MULTICAST_STATE permission'\n"); + } + + // Telephony-related compatibility logic + if (!specTelephonyFeature && (hasTelephonyPermission || reqTelephonySubFeature)) { + // if app takes one of the telephony permissions or requests a sub-feature but + // does not request the base telephony feature, we infer that it meant to + printf("uses-feature:'android.hardware.telephony'\n"); + printf("uses-implied-feature:'android.hardware.telephony'," \ + "'requested a telephony-related permission or feature'\n"); + } + + // Touchscreen-related back-compatibility logic + if (!specTouchscreenFeature) { // not a typo! + // all apps are presumed to require a touchscreen, unless they explicitly say + // <uses-feature android:name="android.hardware.touchscreen" android:required="false"/> + // Note that specTouchscreenFeature is true if the tag is present, regardless + // of whether its value is true or false, so this is safe + printf("uses-feature:'android.hardware.touchscreen'\n"); + printf("uses-implied-feature:'android.hardware.touchscreen'," \ + "'assumed you require a touch screen unless explicitly made optional'\n"); + } + if (!specMultitouchFeature && reqDistinctMultitouchFeature) { + // if app takes one of the telephony permissions or requests a sub-feature but + // does not request the base telephony feature, we infer that it meant to + printf("uses-feature:'android.hardware.touchscreen.multitouch'\n"); + printf("uses-implied-feature:'android.hardware.touchscreen.multitouch'," \ + "'requested android.hardware.touchscreen.multitouch.distinct feature'\n"); + } + + // Landscape/portrait-related compatibility logic + if (!specScreenLandscapeFeature && !specScreenPortraitFeature) { + // If the app has specified any activities in its manifest + // that request a specific orientation, then assume that + // orientation is required. + if (reqScreenLandscapeFeature) { + printf("uses-feature:'android.hardware.screen.landscape'\n"); + printf("uses-implied-feature:'android.hardware.screen.landscape'," \ + "'one or more activities have specified a landscape orientation'\n"); + } + if (reqScreenPortraitFeature) { + printf("uses-feature:'android.hardware.screen.portrait'\n"); + printf("uses-implied-feature:'android.hardware.screen.portrait'," \ + "'one or more activities have specified a portrait orientation'\n"); + } + } + + if (hasMainActivity) { + printf("main\n"); + } + if (hasWidgetReceivers) { + printf("app-widget\n"); + } + if (hasImeService) { + printf("ime\n"); + } + if (hasWallpaperService) { + printf("wallpaper\n"); + } + if (hasOtherActivities) { + printf("other-activities\n"); + } + if (isSearchable) { + printf("search\n"); + } + if (hasOtherReceivers) { + printf("other-receivers\n"); + } + if (hasOtherServices) { + printf("other-services\n"); + } + + // For modern apps, if screen size buckets haven't been specified + // but the new width ranges have, then infer the buckets from them. + if (smallScreen > 0 && normalScreen > 0 && largeScreen > 0 && xlargeScreen > 0 + && requiresSmallestWidthDp > 0) { + int compatWidth = compatibleWidthLimitDp; + if (compatWidth <= 0) { + compatWidth = requiresSmallestWidthDp; + } + if (requiresSmallestWidthDp <= 240 && compatWidth >= 240) { + smallScreen = -1; + } else { + smallScreen = 0; + } + if (requiresSmallestWidthDp <= 320 && compatWidth >= 320) { + normalScreen = -1; + } else { + normalScreen = 0; + } + if (requiresSmallestWidthDp <= 480 && compatWidth >= 480) { + largeScreen = -1; + } else { + largeScreen = 0; + } + if (requiresSmallestWidthDp <= 720 && compatWidth >= 720) { + xlargeScreen = -1; + } else { + xlargeScreen = 0; + } + } + + // Determine default values for any unspecified screen sizes, + // based on the target SDK of the package. As of 4 (donut) + // the screen size support was introduced, so all default to + // enabled. + if (smallScreen > 0) { + smallScreen = targetSdk >= 4 ? -1 : 0; + } + if (normalScreen > 0) { + normalScreen = -1; + } + if (largeScreen > 0) { + largeScreen = targetSdk >= 4 ? -1 : 0; + } + if (xlargeScreen > 0) { + // Introduced in Gingerbread. + xlargeScreen = targetSdk >= 9 ? -1 : 0; + } + if (anyDensity > 0) { + anyDensity = (targetSdk >= 4 || requiresSmallestWidthDp > 0 + || compatibleWidthLimitDp > 0) ? -1 : 0; + } + printf("supports-screens:"); + if (smallScreen != 0) { + printf(" 'small'"); + } + if (normalScreen != 0) { + printf(" 'normal'"); + } + if (largeScreen != 0) { + printf(" 'large'"); + } + if (xlargeScreen != 0) { + printf(" 'xlarge'"); + } + printf("\n"); + printf("supports-any-density: '%s'\n", anyDensity ? "true" : "false"); + if (requiresSmallestWidthDp > 0) { + printf("requires-smallest-width:'%d'\n", requiresSmallestWidthDp); + } + if (compatibleWidthLimitDp > 0) { + printf("compatible-width-limit:'%d'\n", compatibleWidthLimitDp); + } + if (largestWidthLimitDp > 0) { + printf("largest-width-limit:'%d'\n", largestWidthLimitDp); + } + + printf("locales:"); + const size_t NL = locales.size(); + for (size_t i=0; i<NL; i++) { + const char* localeStr = locales[i].string(); + if (localeStr == NULL || strlen(localeStr) == 0) { + localeStr = "--_--"; + } + printf(" '%s'", localeStr); + } + printf("\n"); + + printf("densities:"); + const size_t ND = densities.size(); + for (size_t i=0; i<ND; i++) { + printf(" '%d'", densities[i]); + } + printf("\n"); + + AssetDir* dir = assets.openNonAssetDir(assetsCookie, "lib"); + if (dir != NULL) { + if (dir->getFileCount() > 0) { + printf("native-code:"); + for (size_t i=0; i<dir->getFileCount(); i++) { + printf(" '%s'", dir->getFileName(i).string()); + } + printf("\n"); + } + delete dir; + } + } else if (strcmp("badger", option) == 0) { + printf("%s", CONSOLE_DATA); + } 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 { + if (bundle->getJunkPath()) { + String8 storageName = String8(fileName).getPathLeaf(); + printf(" '%s' as '%s'...\n", fileName, storageName.string()); + result = zip->add(fileName, storageName.string(), + bundle->getCompressionMethod(), 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; + FILE* fp; + String8 dependencyFile; + + // -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(); + + // Set up the resource gathering in assets if we're going to generate + // dependency files. Every time we encounter a resource while slurping + // the tree, we'll add it to these stores so we have full resource paths + // to write to a dependency file. + if (bundle->getGenDependencies()) { + sp<FilePathStore> resPathStore = new FilePathStore; + assets->setFullResPaths(resPathStore); + sp<FilePathStore> assetPathStore = new FilePathStore; + assets->setFullAssetPaths(assetPathStore); + } + + err = assets->slurpFromArgs(bundle); + if (err < 0) { + goto bail; + } + + if (bundle->getVerbose()) { + assets->print(String8()); + } + + // If they asked for any fileAs 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; + } + + // Update symbols with information about which ones are needed as Java symbols. + assets->applyJavaSymbols(); + if (SourcePos::hasErrors()) { + goto bail; + } + + // If we've been asked to generate a dependency file, do that here + if (bundle->getGenDependencies()) { + // If this is the packaging step, generate the dependency file next to + // the output apk (e.g. bin/resources.ap_.d) + if (outputAPKFile) { + dependencyFile = String8(outputAPKFile); + // Add the .d extension to the dependency file. + dependencyFile.append(".d"); + } else { + // Else if this is the R.java dependency generation step, + // generate the dependency file in the R.java package subdirectory + // e.g. gen/com/foo/app/R.java.d + dependencyFile = String8(bundle->getRClassDir()); + dependencyFile.appendPath("R.java.d"); + } + // Make sure we have a clean dependency file to start with + fp = fopen(dependencyFile, "w"); + fclose(fp); + } + + // Write out R.java constants + if (!assets->havePrivateSymbols()) { + if (bundle->getCustomPackage() == NULL) { + // Write the R.java file into the appropriate class directory + // e.g. gen/com/foo/app/R.java + err = writeResourceSymbols(bundle, assets, assets->getPackage(), true); + } else { + const String8 customPkg(bundle->getCustomPackage()); + err = writeResourceSymbols(bundle, assets, customPkg, true); + } + if (err < 0) { + goto bail; + } + // If we have library files, we're going to write our R.java file into + // the appropriate class directory for those libraries as well. + // e.g. gen/com/foo/app/lib/R.java + if (bundle->getExtraPackages() != NULL) { + // Split on colon + String8 libs(bundle->getExtraPackages()); + char* packageString = strtok(libs.lockBuffer(libs.length()), ":"); + while (packageString != NULL) { + // Write the R.java file out with the correct package name + err = writeResourceSymbols(bundle, assets, String8(packageString), true); + if (err < 0) { + goto bail; + } + packageString = strtok(NULL, ":"); + } + libs.unlockBuffer(); + } + } 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 out the ProGuard file + err = writeProguardFile(bundle, assets); + 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; + } + } + + // If we've been asked to generate a dependency file, we need to finish up here. + // the writeResourceSymbols and writeAPK functions have already written the target + // half of the dependency file, now we need to write the prerequisites. (files that + // the R.java file or .ap_ file depend on) + if (bundle->getGenDependencies()) { + // Now that writeResourceSymbols or writeAPK has taken care of writing + // the targets to our dependency file, we'll write the prereqs + fp = fopen(dependencyFile, "a+"); + fprintf(fp, " : "); + bool includeRaw = (outputAPKFile != NULL); + err = writeDependencyPreReqs(bundle, assets, fp, includeRaw); + // Also manually add the AndroidManifeset since it's not under res/ or assets/ + // and therefore was not added to our pathstores during slurping + fprintf(fp, "%s \\\n", bundle->getAndroidManifestFile()); + fclose(fp); + } + + retVal = 0; +bail: + if (SourcePos::hasErrors()) { + SourcePos::printErrors(stderr); + } + return retVal; +} + +/* + * Do PNG Crunching + * PRECONDITIONS + * -S flag points to a source directory containing drawable* folders + * -C flag points to destination directory. The folder structure in the + * source directory will be mirrored to the destination (cache) directory + * + * POSTCONDITIONS + * Destination directory will be updated to match the PNG files in + * the source directory. + */ +int doCrunch(Bundle* bundle) +{ + fprintf(stdout, "Crunching PNG Files in "); + fprintf(stdout, "source dir: %s\n", bundle->getResourceSourceDirs()[0]); + fprintf(stdout, "To destination dir: %s\n", bundle->getCrunchedOutputDir()); + + updatePreProcessedCache(bundle); + + return NO_ERROR; +} + +/* + * Do PNG Crunching on a single flag + * -i points to a single png file + * -o points to a single png output file + */ +int doSingleCrunch(Bundle* bundle) +{ + fprintf(stdout, "Crunching single PNG file: %s\n", bundle->getSingleCrunchInputFile()); + fprintf(stdout, "\tOutput file: %s\n", bundle->getSingleCrunchOutputFile()); + + String8 input(bundle->getSingleCrunchInputFile()); + String8 output(bundle->getSingleCrunchOutputFile()); + + if (preProcessImageToCache(bundle, input, output) != NO_ERROR) { + // we can't return the status_t as it gets truncate to the lower 8 bits. + return 42; + } + + return NO_ERROR; +} + +char CONSOLE_DATA[2925] = { + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 95, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 61, 63, + 86, 35, 40, 46, 46, 95, 95, 95, 95, 97, 97, 44, 32, 46, 124, 42, 33, 83, + 62, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 58, 46, 58, 59, 61, 59, 61, 81, + 81, 81, 81, 66, 96, 61, 61, 58, 46, 46, 46, 58, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 46, 61, 59, 59, 59, 58, 106, 81, 81, 81, 81, 102, 59, 61, 59, + 59, 61, 61, 61, 58, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 61, 59, 59, + 59, 58, 109, 81, 81, 81, 81, 61, 59, 59, 59, 59, 59, 58, 59, 59, 46, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 46, 61, 59, 59, 59, 60, 81, 81, 81, 81, 87, + 58, 59, 59, 59, 59, 59, 59, 61, 119, 44, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, + 47, 61, 59, 59, 58, 100, 81, 81, 81, 81, 35, 58, 59, 59, 59, 59, 59, 58, + 121, 81, 91, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, 109, 58, 59, 59, 61, 81, 81, + 81, 81, 81, 109, 58, 59, 59, 59, 59, 61, 109, 81, 81, 76, 46, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 41, 87, 59, 61, 59, 41, 81, 81, 81, 81, 81, 81, 59, 61, 59, + 59, 58, 109, 81, 81, 87, 39, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 60, 81, 91, 59, + 59, 61, 81, 81, 81, 81, 81, 87, 43, 59, 58, 59, 60, 81, 81, 81, 76, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 52, 91, 58, 45, 59, 87, 81, 81, 81, 81, + 70, 58, 58, 58, 59, 106, 81, 81, 81, 91, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 93, 40, 32, 46, 59, 100, 81, 81, 81, 81, 40, 58, 46, 46, 58, 100, 81, + 81, 68, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 46, 46, 46, 32, 46, 46, 46, 32, 46, 32, 46, 45, 91, 59, 61, 58, 109, + 81, 81, 81, 87, 46, 58, 61, 59, 60, 81, 81, 80, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, + 32, 32, 32, 32, 32, 32, 32, 46, 46, 61, 59, 61, 61, 61, 59, 61, 61, 59, + 59, 59, 58, 58, 46, 46, 41, 58, 59, 58, 81, 81, 81, 81, 69, 58, 59, 59, + 60, 81, 81, 68, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 58, 59, + 61, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 61, 61, 46, + 61, 59, 93, 81, 81, 81, 81, 107, 58, 59, 58, 109, 87, 68, 96, 32, 32, 32, + 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 10, 32, 32, 32, 46, 60, 61, 61, 59, 59, 59, 59, 59, 59, 59, 59, + 59, 59, 59, 59, 59, 59, 59, 59, 59, 58, 58, 58, 115, 109, 68, 41, 36, 81, + 109, 46, 61, 61, 81, 69, 96, 46, 58, 58, 46, 58, 46, 46, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 46, 32, 95, 81, + 67, 61, 61, 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, + 59, 59, 59, 59, 58, 68, 39, 61, 105, 61, 63, 81, 119, 58, 106, 80, 32, 58, + 61, 59, 59, 61, 59, 61, 59, 61, 46, 95, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 10, 32, 32, 36, 81, 109, 105, 59, 61, 59, 59, 59, + 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 46, 58, 37, + 73, 108, 108, 62, 52, 81, 109, 34, 32, 61, 59, 59, 59, 59, 59, 59, 59, 59, + 59, 61, 59, 61, 61, 46, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, + 32, 46, 45, 57, 101, 43, 43, 61, 61, 59, 59, 59, 59, 59, 59, 61, 59, 59, + 59, 59, 59, 59, 59, 59, 59, 58, 97, 46, 61, 108, 62, 126, 58, 106, 80, 96, + 46, 61, 61, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 61, 61, + 97, 103, 97, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 45, 46, 32, + 46, 32, 32, 32, 32, 32, 32, 32, 32, 45, 45, 45, 58, 59, 59, 59, 59, 61, + 119, 81, 97, 124, 105, 124, 124, 39, 126, 95, 119, 58, 61, 58, 59, 59, 59, + 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 61, 119, 81, 81, 99, 32, 32, + 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 58, 59, 59, 58, 106, 81, 81, 81, 109, 119, + 119, 119, 109, 109, 81, 81, 122, 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, + 59, 59, 59, 59, 59, 58, 115, 81, 87, 81, 102, 32, 32, 32, 32, 32, 32, 10, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 61, 58, 59, 61, 81, 81, 81, 81, 81, 81, 87, 87, 81, 81, 81, 81, + 81, 58, 59, 59, 59, 59, 59, 59, 59, 59, 58, 45, 45, 45, 59, 59, 59, 41, + 87, 66, 33, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 58, 59, 59, 93, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 40, 58, 59, 59, 59, 58, + 45, 32, 46, 32, 32, 32, 32, 32, 46, 32, 126, 96, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 58, 61, 59, 58, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 40, 58, 59, 59, 59, 58, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 58, + 59, 59, 58, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 40, 58, + 59, 59, 59, 46, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 58, 61, 59, 60, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 59, 61, 59, 59, 61, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 58, 59, 59, 93, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 40, 59, 59, 59, 59, 32, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 58, 61, 58, 106, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 76, 58, 59, 59, 59, + 32, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 61, 58, 58, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 81, 81, 81, 81, 87, 58, 59, 59, 59, 59, 32, 46, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 58, 59, 61, 41, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 81, 87, 59, + 61, 58, 59, 59, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 58, 61, 58, 61, 81, 81, 81, + 81, 81, 81, 81, 81, 81, 81, 81, 81, 107, 58, 59, 59, 59, 59, 58, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 58, 59, 59, 58, 51, 81, 81, 81, 81, 81, 81, 81, 81, 81, + 81, 102, 94, 59, 59, 59, 59, 59, 61, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 58, 61, 59, + 59, 59, 43, 63, 36, 81, 81, 81, 87, 64, 86, 102, 58, 59, 59, 59, 59, 59, + 59, 59, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 46, 61, 59, 59, 59, 59, 59, 59, 59, 43, 33, + 58, 126, 126, 58, 59, 59, 59, 59, 59, 59, 59, 59, 59, 59, 32, 46, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, + 61, 59, 59, 59, 58, 45, 58, 61, 59, 58, 58, 58, 61, 59, 59, 59, 59, 59, + 59, 59, 59, 59, 59, 59, 59, 58, 32, 46, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, 61, 59, 59, 59, 59, 59, 58, 95, + 32, 45, 61, 59, 61, 59, 59, 59, 59, 59, 59, 59, 45, 58, 59, 59, 59, 59, + 61, 58, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 58, 61, 59, 59, 59, 59, 59, 61, 59, 61, 46, 46, 32, 45, 45, 45, + 59, 58, 45, 45, 46, 58, 59, 59, 59, 59, 59, 59, 61, 46, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, 58, 59, 59, 59, 59, + 59, 59, 59, 59, 59, 61, 59, 46, 32, 32, 46, 32, 46, 32, 58, 61, 59, 59, + 59, 59, 59, 59, 59, 59, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 45, 59, 59, 59, 59, 59, 59, 59, 59, 58, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 61, 59, 59, 59, 59, 59, 59, 59, 58, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 46, 61, 59, 59, 59, 59, 59, 59, 59, 32, 46, 32, 32, 32, 32, 32, 32, 61, + 46, 61, 59, 59, 59, 59, 59, 59, 58, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 61, 59, 59, 59, 59, 59, 59, + 59, 59, 32, 46, 32, 32, 32, 32, 32, 32, 32, 46, 61, 58, 59, 59, 59, 59, + 59, 58, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 58, 59, 59, 59, 59, 59, 59, 59, 59, 46, 46, 32, 32, 32, + 32, 32, 32, 32, 61, 59, 59, 59, 59, 59, 59, 59, 45, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 46, 32, 45, 61, + 59, 59, 59, 59, 59, 58, 32, 46, 32, 32, 32, 32, 32, 32, 32, 58, 59, 59, + 59, 59, 59, 58, 45, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 45, 45, 45, 45, 32, 46, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 45, 61, 59, 58, 45, 45, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 10, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 46, 32, 32, 46, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 10 + }; diff --git a/tools/aapt/CrunchCache.cpp b/tools/aapt/CrunchCache.cpp new file mode 100644 index 000000000000..c4cf6bc8494f --- /dev/null +++ b/tools/aapt/CrunchCache.cpp @@ -0,0 +1,104 @@ +// +// Copyright 2011 The Android Open Source Project +// +// Implementation file for CrunchCache +// This file defines functions laid out and documented in +// CrunchCache.h + +#include <utils/Vector.h> +#include <utils/String8.h> + +#include "DirectoryWalker.h" +#include "FileFinder.h" +#include "CacheUpdater.h" +#include "CrunchCache.h" + +using namespace android; + +CrunchCache::CrunchCache(String8 sourcePath, String8 destPath, FileFinder* ff) + : mSourcePath(sourcePath), mDestPath(destPath), mSourceFiles(0), mDestFiles(0), mFileFinder(ff) +{ + // We initialize the default value to return to 0 so if a file doesn't exist + // then all files are automatically "newer" than it. + + // Set file extensions to look for. Right now just pngs. + mExtensions.push(String8(".png")); + + // Load files into our data members + loadFiles(); +} + +size_t CrunchCache::crunch(CacheUpdater* cu, bool forceOverwrite) +{ + size_t numFilesUpdated = 0; + + // Iterate through the source files and compare to cache. + // After processing a file, remove it from the source files and + // from the dest files. + // We're done when we're out of files in source. + String8 relativePath; + while (mSourceFiles.size() > 0) { + // Get the full path to the source file, then convert to a c-string + // and offset our beginning pointer to the length of the sourcePath + // This efficiently strips the source directory prefix from our path. + // Also, String8 doesn't have a substring method so this is what we've + // got to work with. + const char* rPathPtr = mSourceFiles.keyAt(0).string()+mSourcePath.length(); + // Strip leading slash if present + int offset = 0; + if (rPathPtr[0] == OS_PATH_SEPARATOR) + offset = 1; + relativePath = String8(rPathPtr + offset); + + if (forceOverwrite || needsUpdating(relativePath)) { + cu->processImage(mSourcePath.appendPathCopy(relativePath), + mDestPath.appendPathCopy(relativePath)); + numFilesUpdated++; + // crunchFile(relativePath); + } + // Delete this file from the source files and (if it exists) from the + // dest files. + mSourceFiles.removeItemsAt(0); + mDestFiles.removeItem(mDestPath.appendPathCopy(relativePath)); + } + + // Iterate through what's left of destFiles and delete leftovers + while (mDestFiles.size() > 0) { + cu->deleteFile(mDestFiles.keyAt(0)); + mDestFiles.removeItemsAt(0); + } + + // Update our knowledge of the files cache + // both source and dest should be empty by now. + loadFiles(); + + return numFilesUpdated; +} + +void CrunchCache::loadFiles() +{ + // Clear out our data structures to avoid putting in duplicates + mSourceFiles.clear(); + mDestFiles.clear(); + + // Make a directory walker that points to the system. + DirectoryWalker* dw = new SystemDirectoryWalker(); + + // Load files in the source directory + mFileFinder->findFiles(mSourcePath, mExtensions, mSourceFiles,dw); + + // Load files in the destination directory + mFileFinder->findFiles(mDestPath,mExtensions,mDestFiles,dw); + + delete dw; +} + +bool CrunchCache::needsUpdating(String8 relativePath) const +{ + // Retrieve modification dates for this file entry under the source and + // cache directory trees. The vectors will return a modification date of 0 + // if the file doesn't exist. + time_t sourceDate = mSourceFiles.valueFor(mSourcePath.appendPathCopy(relativePath)); + time_t destDate = mDestFiles.valueFor(mDestPath.appendPathCopy(relativePath)); + return sourceDate > destDate; +}
\ No newline at end of file diff --git a/tools/aapt/CrunchCache.h b/tools/aapt/CrunchCache.h new file mode 100644 index 000000000000..be3da5c40b25 --- /dev/null +++ b/tools/aapt/CrunchCache.h @@ -0,0 +1,102 @@ +// +// Copyright 2011 The Android Open Source Project +// +// Cache manager for pre-processed PNG files. +// Contains code for managing which PNG files get processed +// at build time. +// + +#ifndef CRUNCHCACHE_H +#define CRUNCHCACHE_H + +#include <utils/KeyedVector.h> +#include <utils/String8.h> +#include "FileFinder.h" +#include "CacheUpdater.h" + +using namespace android; + +/** CrunchCache + * This class is a cache manager which can pre-process PNG files and store + * them in a mirror-cache. It's capable of doing incremental updates to its + * cache. + * + * Usage: + * Create an instance initialized with the root of the source tree, the + * root location to store the cache files, and an instance of a file finder. + * Then update the cache by calling crunch. + */ +class CrunchCache { +public: + // Constructor + CrunchCache(String8 sourcePath, String8 destPath, FileFinder* ff); + + // Nobody should be calling the default constructor + // So this space is intentionally left blank + + // Default Copy Constructor and Destructor are fine + + /** crunch is the workhorse of this class. + * It goes through all the files found in the sourcePath and compares + * them to the cached versions in the destPath. If the optional + * argument forceOverwrite is set to true, then all source files are + * re-crunched even if they have not been modified recently. Otherwise, + * source files are only crunched when they needUpdating. Afterwards, + * we delete any leftover files in the cache that are no longer present + * in source. + * + * PRECONDITIONS: + * No setup besides construction is needed + * POSTCONDITIONS: + * The cache is updated to fully reflect all changes in source. + * The function then returns the number of files changed in cache + * (counting deletions). + */ + size_t crunch(CacheUpdater* cu, bool forceOverwrite=false); + +private: + /** loadFiles is a wrapper to the FileFinder that places matching + * files into mSourceFiles and mDestFiles. + * + * POSTCONDITIONS + * mDestFiles and mSourceFiles are refreshed to reflect the current + * state of the files in the source and dest directories. + * Any previous contents of mSourceFiles and mDestFiles are cleared. + */ + void loadFiles(); + + /** needsUpdating takes a file path + * and returns true if the file represented by this path is newer in the + * sourceFiles than in the cache (mDestFiles). + * + * PRECONDITIONS: + * mSourceFiles and mDestFiles must be initialized and filled. + * POSTCONDITIONS: + * returns true if and only if source file's modification time + * is greater than the cached file's mod-time. Otherwise returns false. + * + * USAGE: + * Should be used something like the following: + * if (needsUpdating(filePath)) + * // Recrunch sourceFile out to destFile. + * + */ + bool needsUpdating(String8 relativePath) const; + + // DATA MEMBERS ==================================================== + + String8 mSourcePath; + String8 mDestPath; + + Vector<String8> mExtensions; + + // Each vector of paths contains one entry per PNG file encountered. + // Each entry consists of a path pointing to that PNG. + DefaultKeyedVector<String8,time_t> mSourceFiles; + DefaultKeyedVector<String8,time_t> mDestFiles; + + // Pointer to a FileFinder to use + FileFinder* mFileFinder; +}; + +#endif // CRUNCHCACHE_H diff --git a/tools/aapt/DirectoryWalker.h b/tools/aapt/DirectoryWalker.h new file mode 100644 index 000000000000..88031d04b2df --- /dev/null +++ b/tools/aapt/DirectoryWalker.h @@ -0,0 +1,98 @@ +// +// Copyright 2011 The Android Open Source Project +// +// Defines an abstraction for opening a directory on the filesystem and +// iterating through it. + +#ifndef DIRECTORYWALKER_H +#define DIRECTORYWALKER_H + +#include <dirent.h> +#include <sys/types.h> +#include <sys/param.h> +#include <sys/stat.h> +#include <unistd.h> +#include <utils/String8.h> + +#include <stdio.h> + +using namespace android; + +// Directory Walker +// This is an abstraction for walking through a directory and getting files +// and descriptions. + +class DirectoryWalker { +public: + virtual ~DirectoryWalker() {}; + virtual bool openDir(String8 path) = 0; + virtual bool openDir(const char* path) = 0; + // Advance to next directory entry + virtual struct dirent* nextEntry() = 0; + // Get the stats for the current entry + virtual struct stat* entryStats() = 0; + // Clean Up + virtual void closeDir() = 0; + // This class is able to replicate itself on the heap + virtual DirectoryWalker* clone() = 0; + + // DATA MEMBERS + // Current directory entry + struct dirent mEntry; + // Stats for that directory entry + struct stat mStats; + // Base path + String8 mBasePath; +}; + +// System Directory Walker +// This is an implementation of the above abstraction that calls +// real system calls and is fully functional. +// functions are inlined since they're very short and simple + +class SystemDirectoryWalker : public DirectoryWalker { + + // Default constructor, copy constructor, and destructor are fine +public: + virtual bool openDir(String8 path) { + mBasePath = path; + dir = NULL; + dir = opendir(mBasePath.string() ); + + if (dir == NULL) + return false; + + return true; + }; + virtual bool openDir(const char* path) { + String8 p(path); + openDir(p); + return true; + }; + // Advance to next directory entry + virtual struct dirent* nextEntry() { + struct dirent* entryPtr = readdir(dir); + if (entryPtr == NULL) + return NULL; + + mEntry = *entryPtr; + // Get stats + String8 fullPath = mBasePath.appendPathCopy(mEntry.d_name); + stat(fullPath.string(),&mStats); + return &mEntry; + }; + // Get the stats for the current entry + virtual struct stat* entryStats() { + return &mStats; + }; + virtual void closeDir() { + closedir(dir); + }; + virtual DirectoryWalker* clone() { + return new SystemDirectoryWalker(*this); + }; +private: + DIR* dir; +}; + +#endif // DIRECTORYWALKER_H diff --git a/tools/aapt/FileFinder.cpp b/tools/aapt/FileFinder.cpp new file mode 100644 index 000000000000..18775c06863f --- /dev/null +++ b/tools/aapt/FileFinder.cpp @@ -0,0 +1,98 @@ +// +// Copyright 2011 The Android Open Source Project +// + +// File Finder implementation. +// Implementation for the functions declared and documented in FileFinder.h + +#include <utils/Vector.h> +#include <utils/String8.h> +#include <utils/KeyedVector.h> + +#include <dirent.h> +#include <sys/stat.h> + +#include "DirectoryWalker.h" +#include "FileFinder.h" + +//#define DEBUG + +using android::String8; + +// Private function to check whether a file is a directory or not +bool isDirectory(const char* filename) { + struct stat fileStat; + if (stat(filename, &fileStat) == -1) { + return false; + } + return(S_ISDIR(fileStat.st_mode)); +} + + +// Private function to check whether a file is a regular file or not +bool isFile(const char* filename) { + struct stat fileStat; + if (stat(filename, &fileStat) == -1) { + return false; + } + return(S_ISREG(fileStat.st_mode)); +} + +bool SystemFileFinder::findFiles(String8 basePath, Vector<String8>& extensions, + KeyedVector<String8,time_t>& fileStore, + DirectoryWalker* dw) +{ + // Scan the directory pointed to by basePath + // check files and recurse into subdirectories. + if (!dw->openDir(basePath)) { + return false; + } + /* + * Go through all directory entries. Check each file using checkAndAddFile + * and recurse into sub-directories. + */ + struct dirent* entry; + while ((entry = dw->nextEntry()) != NULL) { + String8 entryName(entry->d_name); + if (entry->d_name[0] == '.') // Skip hidden files and directories + continue; + + String8 fullPath = basePath.appendPathCopy(entryName); + // If this entry is a directory we'll recurse into it + if (isDirectory(fullPath.string()) ) { + DirectoryWalker* copy = dw->clone(); + findFiles(fullPath, extensions, fileStore,copy); + delete copy; + } + + // If this entry is a file, we'll pass it over to checkAndAddFile + if (isFile(fullPath.string()) ) { + checkAndAddFile(fullPath,dw->entryStats(),extensions,fileStore); + } + } + + // Clean up + dw->closeDir(); + + return true; +} + +void SystemFileFinder::checkAndAddFile(String8 path, const struct stat* stats, + Vector<String8>& extensions, + KeyedVector<String8,time_t>& fileStore) +{ + // Loop over the extensions, checking for a match + bool done = false; + String8 ext(path.getPathExtension()); + ext.toLower(); + for (size_t i = 0; i < extensions.size() && !done; ++i) { + String8 ext2 = extensions[i].getPathExtension(); + ext2.toLower(); + // Compare the extensions. If a match is found, add to storage. + if (ext == ext2) { + done = true; + fileStore.add(path,stats->st_mtime); + } + } +} + diff --git a/tools/aapt/FileFinder.h b/tools/aapt/FileFinder.h new file mode 100644 index 000000000000..6974aee033a8 --- /dev/null +++ b/tools/aapt/FileFinder.h @@ -0,0 +1,80 @@ +// +// Copyright 2011 The Android Open Source Project +// + +// File Finder. +// This is a collection of useful functions for finding paths and modification +// times of files that match an extension pattern in a directory tree. +// and finding files in it. + +#ifndef FILEFINDER_H +#define FILEFINDER_H + +#include <utils/Vector.h> +#include <utils/KeyedVector.h> +#include <utils/String8.h> + +#include "DirectoryWalker.h" + +using namespace android; + +// Abstraction to allow for dependency injection. See MockFileFinder.h +// for the testing implementation. +class FileFinder { +public: + virtual bool findFiles(String8 basePath, Vector<String8>& extensions, + KeyedVector<String8,time_t>& fileStore, + DirectoryWalker* dw) = 0; + + virtual ~FileFinder() {}; +}; + +class SystemFileFinder : public FileFinder { +public: + + /* findFiles takes a path, a Vector of extensions, and a destination KeyedVector + * and places path/modification date key/values pointing to + * all files with matching extensions found into the KeyedVector + * PRECONDITIONS + * path is a valid system path + * extensions should include leading "." + * This is not necessary, but the comparison directly + * compares the end of the path string so if the "." + * is excluded there is a small chance you could have + * a false positive match. (For example: extension "png" + * would match a file called "blahblahpng") + * + * POSTCONDITIONS + * fileStore contains (in no guaranteed order) paths to all + * matching files encountered in subdirectories of path + * as keys in the KeyedVector. Each key has the modification time + * of the file as its value. + * + * Calls checkAndAddFile on each file encountered in the directory tree + * Recursively descends into subdirectories. + */ + virtual bool findFiles(String8 basePath, Vector<String8>& extensions, + KeyedVector<String8,time_t>& fileStore, + DirectoryWalker* dw); + +private: + /** + * checkAndAddFile looks at a single file path and stat combo + * to determine whether it is a matching file (by looking at + * the extension) + * + * PRECONDITIONS + * no setup is needed + * + * POSTCONDITIONS + * If the given file has a matching extension then a new entry + * is added to the KeyedVector with the path as the key and the modification + * time as the value. + * + */ + static void checkAndAddFile(String8 path, const struct stat* stats, + Vector<String8>& extensions, + KeyedVector<String8,time_t>& fileStore); + +}; +#endif // FILEFINDER_H diff --git a/tools/aapt/Images.cpp b/tools/aapt/Images.cpp new file mode 100644 index 000000000000..b2cbf49843af --- /dev/null +++ b/tools/aapt/Images.cpp @@ -0,0 +1,1387 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#define PNG_INTERNAL + +#include "Images.h" + +#include <androidfw/ResourceTypes.h> +#include <utils/ByteOrder.h> + +#include <png.h> +#include <zlib.h> + +#define NOISY(x) //x + +static void +png_write_aapt_file(png_structp png_ptr, png_bytep data, png_size_t length) +{ + AaptFile* aaptfile = (AaptFile*) png_get_io_ptr(png_ptr); + status_t err = aaptfile->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); + } + free(info9Patch.xDivs); + free(info9Patch.yDivs); + free(info9Patch.colors); + } + + png_uint_32 width; + png_uint_32 height; + png_bytepp rows; + + // 9-patch info. + bool is9Patch; + Res_png_9patch info9Patch; + + // Layout padding, if relevant + bool haveLayoutBounds; + int32_t layoutBoundsLeft; + int32_t layoutBoundsTop; + int32_t layoutBoundsRight; + int32_t layoutBoundsBottom; + + 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_expand_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 * 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); +} + +#define COLOR_TRANSPARENT 0 +#define COLOR_WHITE 0xFFFFFFFF +#define COLOR_TICK 0xFF000000 +#define COLOR_LAYOUT_BOUNDS_TICK 0xFF0000FF + +enum { + TICK_TYPE_NONE, + TICK_TYPE_TICK, + TICK_TYPE_LAYOUT_BOUNDS, + TICK_TYPE_BOTH +}; + +static int tick_type(png_bytep p, bool transparent, const char** outError) +{ + png_uint_32 color = p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); + + if (transparent) { + if (p[3] == 0) { + return TICK_TYPE_NONE; + } + if (color == COLOR_LAYOUT_BOUNDS_TICK) { + return TICK_TYPE_LAYOUT_BOUNDS; + } + if (color == COLOR_TICK) { + return TICK_TYPE_TICK; + } + + // Error cases + if (p[3] != 0xff) { + *outError = "Frame pixels must be either solid or transparent (not intermediate alphas)"; + return TICK_TYPE_NONE; + } + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in transparent frame must be black or red"; + } + return TICK_TYPE_TICK; + } + + if (p[3] != 0xFF) { + *outError = "White frame must be a solid color (no alpha)"; + } + if (color == COLOR_WHITE) { + return TICK_TYPE_NONE; + } + if (color == COLOR_TICK) { + return TICK_TYPE_TICK; + } + if (color == COLOR_LAYOUT_BOUNDS_TICK) { + return TICK_TYPE_LAYOUT_BOUNDS; + } + + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in white frame must be black or red"; + return TICK_TYPE_NONE; + } + return TICK_TYPE_TICK; +} + +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 (TICK_TYPE_TICK == tick_type(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 (TICK_TYPE_TICK == tick_type(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 status_t get_horizontal_layout_bounds_ticks( + png_bytep row, int width, bool transparent, bool required, + int32_t* outLeft, int32_t* outRight, const char** outError) +{ + int i; + *outLeft = *outRight = 0; + + // Look for left tick + if (TICK_TYPE_LAYOUT_BOUNDS == tick_type(row + 4, transparent, outError)) { + // Starting with a layout padding tick + i = 1; + while (i < width - 1) { + (*outLeft)++; + i++; + int tick = tick_type(row + i * 4, transparent, outError); + if (tick != TICK_TYPE_LAYOUT_BOUNDS) { + break; + } + } + } + + // Look for right tick + if (TICK_TYPE_LAYOUT_BOUNDS == tick_type(row + (width - 2) * 4, transparent, outError)) { + // Ending with a layout padding tick + i = width - 2; + while (i > 1) { + (*outRight)++; + i--; + int tick = tick_type(row+i*4, transparent, outError); + if (tick != TICK_TYPE_LAYOUT_BOUNDS) { + break; + } + } + } + + return NO_ERROR; +} + +static status_t get_vertical_layout_bounds_ticks( + png_bytepp rows, int offset, int height, bool transparent, bool required, + int32_t* outTop, int32_t* outBottom, const char** outError) +{ + int i; + *outTop = *outBottom = 0; + + // Look for top tick + if (TICK_TYPE_LAYOUT_BOUNDS == tick_type(rows[1] + offset, transparent, outError)) { + // Starting with a layout padding tick + i = 1; + while (i < height - 1) { + (*outTop)++; + i++; + int tick = tick_type(rows[i] + offset, transparent, outError); + if (tick != TICK_TYPE_LAYOUT_BOUNDS) { + break; + } + } + } + + // Look for bottom tick + if (TICK_TYPE_LAYOUT_BOUNDS == tick_type(rows[height - 2] + offset, transparent, outError)) { + // Ending with a layout padding tick + i = height - 2; + while (i > 1) { + (*outBottom)++; + i--; + int tick = tick_type(rows[i] + offset, transparent, outError); + if (tick != TICK_TYPE_LAYOUT_BOUNDS) { + break; + } + } + } + + 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 * sizeof(int32_t); + int maxSizeYDivs = H * 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; + + image->layoutBoundsLeft = image->layoutBoundsRight = + image->layoutBoundsTop = image->layoutBoundsBottom = 0; + + png_bytep p = image->rows[0]; + bool transparent = p[3] == 0; + bool hasColor = false; + + const char* errorMsg = NULL; + int errorPixel = -1; + const char* errorEdge = NULL; + + 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; + } + + // Find left and right of layout padding... + get_horizontal_layout_bounds_ticks(image->rows[H-1], W, transparent, false, + &image->layoutBoundsLeft, + &image->layoutBoundsRight, &errorMsg); + + get_vertical_layout_bounds_ticks(image->rows, (W-1)*4, H, transparent, false, + &image->layoutBoundsTop, + &image->layoutBoundsBottom, &errorMsg); + + image->haveLayoutBounds = image->layoutBoundsLeft != 0 + || image->layoutBoundsRight != 0 + || image->layoutBoundsTop != 0 + || image->layoutBoundsBottom != 0; + + if (image->haveLayoutBounds) { + NOISY(printf("layoutBounds=%d %d %d %d\n", image->layoutBoundsLeft, image->layoutBoundsTop, + image->layoutBoundsRight, image->layoutBoundsBottom)); + } + + // 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) * 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--; + } + + // Make sure the amount of rows and columns will fit in the number of + // colors we can use in the 9-patch format. + if (numRows * numCols > 0x7F) { + errorMsg = "Too many rows and columns in 9-patch perimeter"; + goto getout; + } + + 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 (errorEdge != NULL) { + 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[2]; + unknowns[0].data = NULL; + unknowns[1].data = NULL; + + png_bytepp outRows = (png_bytepp) malloc((int) imageInfo.height * 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) { + int chunk_count = 1 + (imageInfo.haveLayoutBounds ? 1 : 0); + int p_index = imageInfo.haveLayoutBounds ? 1 : 0; + int b_index = 0; + png_byte *chunk_names = imageInfo.haveLayoutBounds + ? (png_byte*)"npLb\0npTc\0" + : (png_byte*)"npTc"; + NOISY(printf("Adding 9-patch info...\n")); + strcpy((char*)unknowns[p_index].name, "npTc"); + unknowns[p_index].data = (png_byte*)imageInfo.info9Patch.serialize(); + unknowns[p_index].size = imageInfo.info9Patch.serializedSize(); + // TODO: remove the check below when everything works + checkNinePatchSerialization(&imageInfo.info9Patch, unknowns[p_index].data); + + if (imageInfo.haveLayoutBounds) { + int chunk_size = sizeof(png_uint_32) * 4; + strcpy((char*)unknowns[b_index].name, "npLb"); + unknowns[b_index].data = (png_byte*) calloc(chunk_size, 1); + memcpy(unknowns[b_index].data, &imageInfo.layoutBoundsLeft, chunk_size); + unknowns[b_index].size = chunk_size; + } + + for (int i = 0; i < chunk_count; i++) { + unknowns[i].location = PNG_HAVE_PLTE; + } + png_set_keep_unknown_chunks(write_ptr, PNG_HANDLE_CHUNK_ALWAYS, + chunk_names, chunk_count); + png_set_unknown_chunks(write_ptr, write_info, unknowns, chunk_count); +#if PNG_LIBPNG_VER < 10600 + /* Deal with unknown chunk location bug in 1.5.x and earlier */ + png_set_unknown_chunk_location(write_ptr, write_info, 0, PNG_HAVE_PLTE); + if (imageInfo.haveLayoutBounds) { + png_set_unknown_chunk_location(write_ptr, write_info, 1, PNG_HAVE_PLTE); + } +#endif + } + + + png_write_info(write_ptr, write_info); + + png_bytepp rows; + if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA) { + if (color_type == PNG_COLOR_TYPE_RGB) { + 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); + free(unknowns[0].data); + free(unknowns[1].data); + + 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(const 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()); + + if (bundle->getVerbose()) { + printf("Processing image: %s\n", printableName.string()); + } + + 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 preProcessImageToCache(const Bundle* bundle, const String8& source, const String8& dest) +{ + 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; + + if (bundle->getVerbose()) { + printf("Processing image to cache: %s => %s\n", source.string(), dest.string()); + } + + // Get a file handler to read from + fp = fopen(source.string(),"rb"); + if (fp == NULL) { + fprintf(stderr, "%s ERROR: Unable to open PNG file\n", source.string()); + return error; + } + + // Call libpng to get a struct to read image data into + read_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!read_ptr) { + fclose(fp); + png_destroy_read_struct(&read_ptr, &read_info,NULL); + return error; + } + + // Call libpng to get a struct to read image info into + read_info = png_create_info_struct(read_ptr); + if (!read_info) { + fclose(fp); + png_destroy_read_struct(&read_ptr, &read_info,NULL); + return error; + } + + // Set a jump point for libpng to long jump back to on error + if (setjmp(png_jmpbuf(read_ptr))) { + fclose(fp); + png_destroy_read_struct(&read_ptr, &read_info,NULL); + return error; + } + + // Set up libpng to read from our file. + png_init_io(read_ptr,fp); + + // Actually read data from the file + read_png(source.string(), read_ptr, read_info, &imageInfo); + + // We're done reading so we can clean up + // Find old file size before releasing handle + fseek(fp, 0, SEEK_END); + size_t oldSize = (size_t)ftell(fp); + fclose(fp); + png_destroy_read_struct(&read_ptr, &read_info,NULL); + + // Check to see if we're dealing with a 9-patch + // If we are, process appropriately + if (source.getBasePath().getPathExtension() == ".9") { + if (do_9patch(source.string(), &imageInfo) != NO_ERROR) { + return error; + } + } + + // Call libpng to create a structure to hold the processed image data + // that can be written to disk + write_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!write_ptr) { + png_destroy_write_struct(&write_ptr, &write_info); + return error; + } + + // Call libpng to create a structure to hold processed image info that can + // be written to disk + write_info = png_create_info_struct(write_ptr); + if (!write_info) { + png_destroy_write_struct(&write_ptr, &write_info); + return error; + } + + // Open up our destination file for writing + fp = fopen(dest.string(), "wb"); + if (!fp) { + fprintf(stderr, "%s ERROR: Unable to open PNG file\n", dest.string()); + png_destroy_write_struct(&write_ptr, &write_info); + return error; + } + + // Set up libpng to write to our file + png_init_io(write_ptr, fp); + + // Set up a jump for libpng to long jump back on on errors + if (setjmp(png_jmpbuf(write_ptr))) { + fclose(fp); + png_destroy_write_struct(&write_ptr, &write_info); + return error; + } + + // Actually write out to the new png + write_png(dest.string(), write_ptr, write_info, imageInfo, + bundle->getGrayscaleTolerance()); + + if (bundle->getVerbose()) { + // Find the size of our new file + FILE* reader = fopen(dest.string(), "rb"); + fseek(reader, 0, SEEK_END); + size_t newSize = (size_t)ftell(reader); + fclose(reader); + + float factor = ((float)newSize)/oldSize; + int percent = (int)(factor*100); + printf(" (processed image to cache entry %s: %d%% size of source)\n", + dest.string(), percent); + } + + //Clean up + fclose(fp); + png_destroy_write_struct(&write_ptr, &write_info); + + return NO_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 000000000000..91b6554c02c9 --- /dev/null +++ b/tools/aapt/Images.h @@ -0,0 +1,26 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#ifndef IMAGES_H +#define IMAGES_H + +#include "ResourceTable.h" +#include "Bundle.h" + +#include <utils/String8.h> +#include <utils/RefBase.h> + +using android::String8; + +status_t preProcessImage(const Bundle* bundle, const sp<AaptAssets>& assets, + const sp<AaptFile>& file, String8* outNewLeafName); + +status_t preProcessImageToCache(const Bundle* bundle, const String8& source, const String8& dest); + +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 000000000000..4a8aa9cca471 --- /dev/null +++ b/tools/aapt/Main.cpp @@ -0,0 +1,655 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Android Asset Packaging Tool main entry point. +// +#include "Main.h" +#include "Bundle.h" + +#include <utils/Log.h> +#include <utils/threads.h> +#include <utils/List.h> +#include <utils/Errors.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] [--values] [--include-meta-data] WHAT file.{apk} [asset [asset ...]]\n" + " strings Print the contents of the resource table string pool in the APK.\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 ...]] [-g tolerance] [-j jarfile] \\\n" + " [--debug-mode] [--min-sdk-version VAL] [--target-sdk-version VAL] \\\n" + " [--app-version VAL] [--app-version-name TEXT] [--custom-package VAL] \\\n" + " [--rename-manifest-package PACKAGE] \\\n" + " [--rename-instrumentation-target-package PACKAGE] \\\n" + " [--utf16] [--auto-add-overlay] \\\n" + " [--max-res-version VAL] \\\n" + " [-I base-package [-I base-package ...]] \\\n" + " [-A asset-source-dir] [-G class-list-file] [-P public-definitions-file] \\\n" + " [-S resource-sources [-S resource-sources ...]] \\\n" + " [-F apk-file] [-J R-file-dir] \\\n" + " [--product product1,product2,...] \\\n" + " [-c CONFIGS] [--preferred-configurations CONFIGS] \\\n" + " [raw-files-dir [raw-files-dir] ...] \\\n" + " [--output-text-symbols 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 c[runch] [-v] -S resource-sources ... -C output-folder ...\n" + " Do PNG preprocessing on one or several resource folders\n" + " and store the results in the output folder.\n\n", gProgName); + fprintf(stderr, + " %s s[ingleCrunch] [-v] -i input-file -o outputfile\n" + " Do PNG preprocessing on a single file.\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" + " -k junk path of file(s) added\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" + " -G A file to output proguard options into.\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\n" + " and the first match found (left to right) will take precedence.\n" + " -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" + " --debug-mode\n" + " inserts android:debuggable=\"true\" in to the application node of the\n" + " manifest, making the application debuggable even on production devices.\n" + " --include-meta-data\n" + " when used with \"dump badging\" also includes meta-data tags.\n" + " --min-sdk-version\n" + " inserts android:minSdkVersion in to manifest. If the version is 7 or\n" + " higher, the default encoding for resources will be in UTF-8.\n" + " --target-sdk-version\n" + " inserts android:targetSdkVersion in to manifest.\n" + " --max-res-version\n" + " ignores versioned resource directories above the given value.\n" + " --values\n" + " when used with \"dump resources\" also includes resource values.\n" + " --version-code\n" + " inserts android:versionCode in to manifest.\n" + " --version-name\n" + " inserts android:versionName in to manifest.\n" + " --custom-package\n" + " generates R.java into a different package.\n" + " --extra-packages\n" + " generate R.java for libraries. Separate libraries with ':'.\n" + " --generate-dependencies\n" + " generate dependency files in the same directories for R.java and resource package\n" + " --auto-add-overlay\n" + " Automatically add resources that are only in overlays.\n" + " --preferred-configurations\n" + " Like the -c option for filtering out unneeded configurations, but\n" + " only expresses a preference. If there is no resource available with\n" + " the preferred configuration then it will not be stripped.\n" + " --rename-manifest-package\n" + " Rewrite the manifest so that its package name is the package name\n" + " given here. Relative class names (for example .Foo) will be\n" + " changed to absolute names with the old package so that the code\n" + " does not need to change.\n" + " --rename-instrumentation-target-package\n" + " Rewrite the manifest so that all of its instrumentation\n" + " components target the given package. Useful when used in\n" + " conjunction with --rename-manifest-package to fix tests against\n" + " a package that has been renamed.\n" + " --product\n" + " Specifies which variant to choose for strings that have\n" + " product variants\n" + " --utf16\n" + " changes default encoding for resources to UTF-16. Only useful when API\n" + " level is set to 7 or higher where the default encoding is UTF-8.\n" + " --non-constant-id\n" + " Make the resources ID non constant. This is required to make an R java class\n" + " that does not contain the final value but is used to make reusable compiled\n" + " libraries that need to access resources.\n" + " --error-on-failed-insert\n" + " Forces aapt to return an error if it fails to insert values into the manifest\n" + " with --debug-mode, --min-sdk-version, --target-sdk-version --version-code\n" + " and --version-name.\n" + " Insertion typically fails if the manifest already defines the attribute.\n" + " --output-text-symbols\n" + " Generates a text file containing the resource symbols of the R class in the\n" + " specified folder.\n" + " --ignore-assets\n" + " Assets to be ignored. Default pattern is:\n" + " %s\n", + gDefaultIgnoreAssets); +} + +/* + * 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); + case kCommandCrunch: return doCrunch(bundle); + case kCommandSingleCrunch: return doSingleCrunch(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 if (argv[1][0] == 'c') + bundle.setCommand(kCommandCrunch); + else if (argv[1][0] == 's') + bundle.setCommand(kCommandSingleCrunch); + 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 'k': + bundle.setJunkPath(true); + 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 'G': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-G' option\n"); + wantUsage = true; + goto bail; + } + convertPath(argv[0]); + bundle.setProguardFile(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 'C': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-C' option\n"); + wantUsage = true; + goto bail; + } + convertPath(argv[0]); + bundle.setCrunchedOutputDir(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.setSingleCrunchInputFile(argv[0]); + break; + case 'o': + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-o' option\n"); + wantUsage = true; + goto bail; + } + convertPath(argv[0]); + bundle.setSingleCrunchOutputFile(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; + case '-': + if (strcmp(cp, "-debug-mode") == 0) { + bundle.setDebugMode(true); + } else if (strcmp(cp, "-min-sdk-version") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--min-sdk-version' option\n"); + wantUsage = true; + goto bail; + } + bundle.setMinSdkVersion(argv[0]); + } else if (strcmp(cp, "-target-sdk-version") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--target-sdk-version' option\n"); + wantUsage = true; + goto bail; + } + bundle.setTargetSdkVersion(argv[0]); + } else if (strcmp(cp, "-max-sdk-version") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--max-sdk-version' option\n"); + wantUsage = true; + goto bail; + } + bundle.setMaxSdkVersion(argv[0]); + } else if (strcmp(cp, "-max-res-version") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--max-res-version' option\n"); + wantUsage = true; + goto bail; + } + bundle.setMaxResVersion(argv[0]); + } else if (strcmp(cp, "-version-code") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--version-code' option\n"); + wantUsage = true; + goto bail; + } + bundle.setVersionCode(argv[0]); + } else if (strcmp(cp, "-version-name") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--version-name' option\n"); + wantUsage = true; + goto bail; + } + bundle.setVersionName(argv[0]); + } else if (strcmp(cp, "-values") == 0) { + bundle.setValues(true); + } else if (strcmp(cp, "-include-meta-data") == 0) { + bundle.setIncludeMetaData(true); + } else if (strcmp(cp, "-custom-package") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--custom-package' option\n"); + wantUsage = true; + goto bail; + } + bundle.setCustomPackage(argv[0]); + } else if (strcmp(cp, "-extra-packages") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--extra-packages' option\n"); + wantUsage = true; + goto bail; + } + bundle.setExtraPackages(argv[0]); + } else if (strcmp(cp, "-generate-dependencies") == 0) { + bundle.setGenDependencies(true); + } else if (strcmp(cp, "-utf16") == 0) { + bundle.setWantUTF16(true); + } else if (strcmp(cp, "-preferred-configurations") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--preferred-configurations' option\n"); + wantUsage = true; + goto bail; + } + bundle.addPreferredConfigurations(argv[0]); + } else if (strcmp(cp, "-rename-manifest-package") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--rename-manifest-package' option\n"); + wantUsage = true; + goto bail; + } + bundle.setManifestPackageNameOverride(argv[0]); + } else if (strcmp(cp, "-rename-instrumentation-target-package") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--rename-instrumentation-target-package' option\n"); + wantUsage = true; + goto bail; + } + bundle.setInstrumentationPackageNameOverride(argv[0]); + } else if (strcmp(cp, "-auto-add-overlay") == 0) { + bundle.setAutoAddOverlay(true); + } else if (strcmp(cp, "-error-on-failed-insert") == 0) { + bundle.setErrorOnFailedInsert(true); + } else if (strcmp(cp, "-output-text-symbols") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '-output-text-symbols' option\n"); + wantUsage = true; + goto bail; + } + bundle.setOutputTextSymbols(argv[0]); + } else if (strcmp(cp, "-product") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--product' option\n"); + wantUsage = true; + goto bail; + } + bundle.setProduct(argv[0]); + } else if (strcmp(cp, "-non-constant-id") == 0) { + bundle.setNonConstantId(true); + } else if (strcmp(cp, "-no-crunch") == 0) { + bundle.setUseCrunchCache(true); + } else if (strcmp(cp, "-ignore-assets") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for '--ignore-assets' option\n"); + wantUsage = true; + goto bail; + } + gUserIgnoreAssets = argv[0]; + } else { + fprintf(stderr, "ERROR: Unknown option '-%s'\n", cp); + wantUsage = true; + goto bail; + } + cp += strlen(cp) - 1; + 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 000000000000..a6b39ac418dc --- /dev/null +++ b/tools/aapt/Main.h @@ -0,0 +1,63 @@ +// +// 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/Log.h> +#include <utils/threads.h> +#include <utils/List.h> +#include <utils/Errors.h> +#include "Bundle.h" +#include "AaptAssets.h" +#include "ZipFile.h" + + +/* Benchmarking Flag */ +//#define BENCHMARK 1 + +#if BENCHMARK + #include <time.h> +#endif /* BENCHMARK */ + +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 doCrunch(Bundle* bundle); +extern int doSingleCrunch(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 updatePreProcessedCache(Bundle* bundle); + +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 android::status_t writeProguardFile(Bundle* bundle, const sp<AaptAssets>& assets); + +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); + +String8 getAttribute(const ResXMLTree& tree, const char* ns, + const char* attr, String8* outError); + +status_t writeDependencyPreReqs(Bundle* bundle, const sp<AaptAssets>& assets, + FILE* fp, bool includeRaw); +#endif // __MAIN_H diff --git a/tools/aapt/NOTICE b/tools/aapt/NOTICE new file mode 100644 index 000000000000..c5b1efa7aac7 --- /dev/null +++ b/tools/aapt/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-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. + + 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. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/tools/aapt/Package.cpp b/tools/aapt/Package.cpp new file mode 100644 index 000000000000..872d95c509f7 --- /dev/null +++ b/tools/aapt/Package.cpp @@ -0,0 +1,505 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Package assets into Zip files. +// +#include "Main.h" +#include "AaptAssets.h" +#include "ResourceTable.h" +#include "ResourceFilter.h" + +#include <androidfw/misc.h> + +#include <utils/Log.h> +#include <utils/threads.h> +#include <utils/List.h> +#include <utils/Errors.h> +#include <utils/misc.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, const ResourceFilter* filter); +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) +{ + #if BENCHMARK + fprintf(stdout, "BENCHMARK: Starting APK Bundling \n"); + long startAPKTime = clock(); + #endif /* BENCHMARK */ + + 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()); + } + } + + // If we've been asked to generate a dependency file for the .ap_ package, + // do so here + if (bundle->getGenDependencies()) { + // The dependency file gets output to the same directory + // as the specified output file with an additional .d extension. + // e.g. bin/resources.ap_.d + String8 dependencyFile = outputFile; + dependencyFile.append(".d"); + + FILE* fp = fopen(dependencyFile.string(), "a"); + // Add this file to the dependency file + fprintf(fp, "%s \\\n", outputFile.string()); + fclose(fp); + } + + 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"); + + #if BENCHMARK + fprintf(stdout, "BENCHMARK: End APK Bundling. Time Elapsed: %f ms \n",(clock() - startAPKTime)/1000.0); + #endif /* BENCHMARK */ + 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]; + + ssize_t res = processAssets(bundle, zip, assets, ge, &filter); + if (res < 0) { + return res; + } + + count += res; + } + + return count; +} + +ssize_t processAssets(Bundle* bundle, ZipFile* zip, const sp<AaptDir>& dir, + const AaptGroupEntry& ge, const ResourceFilter* filter) +{ + ssize_t count = 0; + + const size_t ND = dir->getDirs().size(); + size_t i; + for (i=0; i<ND; i++) { + const sp<AaptDir>& subDir = dir->getDirs().valueAt(i); + + const bool filterable = filter != NULL && subDir->getLeaf().find("mipmap-") != 0; + + if (filterable && subDir->getLeaf() != subDir->getPath() && !filter->match(ge.toParams())) { + continue; + } + + ssize_t res = processAssets(bundle, zip, subDir, ge, filterable ? filter : NULL); + if (res < 0) { + return res; + } + count += res; + } + + if (filter != NULL && !filter->match(ge.toParams())) { + return count; + } + + 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) +{ + status_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: %d\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 000000000000..d61792828d7c --- /dev/null +++ b/tools/aapt/Resource.cpp @@ -0,0 +1,2676 @@ +// +// 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" + +#include "CrunchCache.h" +#include "FileFinder.h" +#include "CacheUpdater.h" + +#include "WorkQueue.h" + +#if HAVE_PRINTF_ZD +# define ZD "%zd" +# define ZD_TYPE ssize_t +#else +# define ZD "%ld" +# define ZD_TYPE long +#endif + +#define NOISY(x) // x + +// Number of threads to use for preprocessing images. +static const size_t MAX_THREADS = 4; + +// ========================================================================== +// ========================================================================== +// ========================================================================== + +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); + } +} + +ResourceTypeSet::ResourceTypeSet() + :RefBase(), + KeyedVector<String8,sp<AaptGroup> >() +{ +} + +FilePathStore::FilePathStore() + :RefBase(), + Vector<String8>() +{ +} + +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 ui=%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.uiMode, + 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 == "animator" || type == "interpolator" + || type == "transition" || type == "scene" + || type == "drawable" || type == "layout" + || type == "values" || type == "xml" || type == "raw" + || type == "color" || type == "menu" || type == "mipmap"; +} + +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(Bundle* bundle, 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))); + + String16 uses_sdk16("uses-sdk"); + while ((code=block.next()) != ResXMLTree::END_DOCUMENT + && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::START_TAG) { + if (strcmp16(block.getElementName(&len), uses_sdk16.string()) == 0) { + ssize_t minSdkIndex = block.indexOfAttribute(RESOURCES_ANDROID_NAMESPACE, + "minSdkVersion"); + if (minSdkIndex >= 0) { + const uint16_t* minSdk16 = block.getAttributeStringValue(minSdkIndex, &len); + const char* minSdk8 = strdup(String8(minSdk16).string()); + bundle->setManifestMinSdkVersion(minSdk8); + } + } + } + } + + 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; +} + +class PreProcessImageWorkUnit : public WorkQueue::WorkUnit { +public: + PreProcessImageWorkUnit(const Bundle* bundle, const sp<AaptAssets>& assets, + const sp<AaptFile>& file, volatile bool* hasErrors) : + mBundle(bundle), mAssets(assets), mFile(file), mHasErrors(hasErrors) { + } + + virtual bool run() { + status_t status = preProcessImage(mBundle, mAssets, mFile, NULL); + if (status) { + *mHasErrors = true; + } + return true; // continue even if there are errors + } + +private: + const Bundle* mBundle; + sp<AaptAssets> mAssets; + sp<AaptFile> mFile; + volatile bool* mHasErrors; +}; + +static status_t preProcessImages(const Bundle* bundle, const sp<AaptAssets>& assets, + const sp<ResourceTypeSet>& set, const char* type) +{ + volatile bool hasErrors = false; + ssize_t res = NO_ERROR; + if (bundle->getUseCrunchCache() == false) { + WorkQueue wq(MAX_THREADS, false); + ResourceDirIterator it(set, String8(type)); + while ((res=it.next()) == NO_ERROR) { + PreProcessImageWorkUnit* w = new PreProcessImageWorkUnit( + bundle, assets, it.getFile(), &hasErrors); + status_t status = wq.schedule(w); + if (status) { + fprintf(stderr, "preProcessImages failed: schedule() returned %d\n", status); + hasErrors = true; + delete w; + break; + } + } + status_t status = wq.finish(); + if (status) { + fprintf(stderr, "preProcessImages failed: finish() returned %d\n", status); + hasErrors = true; + } + } + return (hasErrors || (res < NO_ERROR)) ? UNKNOWN_ERROR : NO_ERROR; +} + +status_t postProcessImages(const sp<AaptAssets>& assets, + ResourceTable* table, + const sp<ResourceTypeSet>& set) +{ + ResourceDirIterator it(set, String8("drawable")); + bool hasErrors = false; + ssize_t res; + while ((res=it.next()) == NO_ERROR) { + res = postProcessImage(assets, table, it.getFile()); + if (res < NO_ERROR) { + hasErrors = true; + } + } + + return (hasErrors || (res < NO_ERROR)) ? UNKNOWN_ERROR : 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(); + NOISY(printf("Creating new resource type set for leaf %s with group %s (%p)\n", + leafName.string(), group->getPath().string(), group.get())); + set->add(leafName, group); + resources->add(resType, set); + } else { + sp<ResourceTypeSet> set = resources->valueAt(index); + index = set->indexOfKey(leafName); + if (index < 0) { + NOISY(printf("Adding to resource type set for leaf %s group %s (%p)\n", + leafName.string(), group->getPath().string(), group.get())); + set->add(leafName, group); + } else { + sp<AaptGroup> existingGroup = set->valueAt(index); + NOISY(printf("Extending to resource type set for leaf %s group %s (%p)\n", + leafName.string(), group->getPath().string(), group.get())); + for (size_t j=0; j<files.size(); j++) { + NOISY(printf("Adding file %s in group %s resType %s\n", + files.valueAt(j)->getSourceFile().string(), + files.keyAt(j).toDirName(String8()).string(), + resType.string())); + status_t err = 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); + NOISY(printf("Collecting dir #%d %p: %s, leaf %s\n", i, d.get(), d->getPath().string(), + d->getLeaf().string())); + collect_files(d, resources); + + // don't try to include the res dir + NOISY(printf("Removing dir leaf %s\n", d->getLeaf().string())); + 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 ResTable& table, + 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; + Res_value value; + if (index >= 0 && parser.getAttributeValue(index, &value) >= 0) { + const ResStringPool* pool = &parser.getStrings(); + if (value.dataType == Res_value::TYPE_REFERENCE) { + uint32_t specFlags = 0; + int strIdx; + if ((strIdx=table.resolveReference(&value, 0x10000000, NULL, &specFlags)) < 0) { + fprintf(stderr, "%s:%d: Tag <%s> attribute %s references unknown resid 0x%08x.\n", + path.string(), parser.getLineNumber(), + String8(parser.getElementName(&len)).string(), attr, + value.data); + return ATTR_NOT_FOUND; + } + + pool = table.getTableStringBlock(strIdx); + #if 0 + if (pool != NULL) { + str = pool->stringAt(value.data, &len); + } + printf("***** RES ATTR: %s specFlags=0x%x strIdx=%d: %s\n", attr, + specFlags, strIdx, str != NULL ? String8(str).string() : "???"); + #endif + if ((specFlags&~ResTable_typeSpec::SPEC_PUBLIC) != 0 && false) { + fprintf(stderr, "%s:%d: Tag <%s> attribute %s varies by configurations 0x%x.\n", + path.string(), parser.getLineNumber(), + String8(parser.getElementName(&len)).string(), attr, + specFlags); + return ATTR_NOT_FOUND; + } + } + if (value.dataType == Res_value::TYPE_STRING) { + if (pool == NULL) { + fprintf(stderr, "%s:%d: Tag <%s> attribute %s has no string block.\n", + path.string(), parser.getLineNumber(), + String8(parser.getElementName(&len)).string(), attr); + return ATTR_NOT_FOUND; + } + if ((str=pool->stringAt(value.data, &len)) == NULL) { + fprintf(stderr, "%s:%d: Tag <%s> attribute %s has corrupt string value.\n", + path.string(), parser.getLineNumber(), + String8(parser.getElementName(&len)).string(), attr); + return ATTR_NOT_FOUND; + } + } else { + fprintf(stderr, "%s:%d: Tag <%s> attribute %s has invalid type %d.\n", + path.string(), parser.getLineNumber(), + String8(parser.getElementName(&len)).string(), attr, + value.dataType); + return ATTR_NOT_FOUND; + } + 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 bool applyFileOverlay(Bundle *bundle, + const sp<AaptAssets>& assets, + sp<ResourceTypeSet> *baseSet, + const char *resType) +{ + if (bundle->getVerbose()) { + printf("applyFileOverlay for %s\n", 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++) { + if (bundle->getVerbose()) { + printf("trying overlaySet Key=%s\n",overlaySet->keyAt(overlayIndex).string()); + } + size_t baseIndex = UNKNOWN_ERROR; + if (baseSet->get() != NULL) { + 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> > overlayFiles = + overlayGroup->getFiles(); + if (bundle->getVerbose()) { + DefaultKeyedVector<AaptGroupEntry, sp<AaptFile> > baseFiles = + baseGroup->getFiles(); + for (size_t i=0; i < baseFiles.size(); i++) { + printf("baseFile " ZD " has flavor %s\n", (ZD_TYPE) i, + baseFiles.keyAt(i).toString().string()); + } + for (size_t i=0; i < overlayFiles.size(); i++) { + printf("overlayFile " ZD " has flavor %s\n", (ZD_TYPE) i, + overlayFiles.keyAt(i).toString().string()); + } + } + + size_t overlayGroupSize = overlayFiles.size(); + for (size_t overlayGroupIndex = 0; + overlayGroupIndex<overlayGroupSize; + overlayGroupIndex++) { + size_t baseFileIndex = + baseGroup->getFiles().indexOfKey(overlayFiles. + keyAt(overlayGroupIndex)); + if (baseFileIndex < UNKNOWN_ERROR) { + if (bundle->getVerbose()) { + printf("found a match (" ZD ") for overlay file %s, for flavor %s\n", + (ZD_TYPE) baseFileIndex, + overlayGroup->getLeaf().string(), + overlayFiles.keyAt(overlayGroupIndex).toString().string()); + } + baseGroup->removeFile(baseFileIndex); + } else { + // didn't find a match fall through and add it.. + if (true || bundle->getVerbose()) { + printf("nothing matches overlay file %s, for flavor %s\n", + overlayGroup->getLeaf().string(), + overlayFiles.keyAt(overlayGroupIndex).toString().string()); + } + } + baseGroup->addFile(overlayFiles.valueAt(overlayGroupIndex)); + assets->addGroupEntry(overlayFiles.keyAt(overlayGroupIndex)); + } + } else { + if (baseSet->get() == NULL) { + *baseSet = new ResourceTypeSet(); + assets->getResources()->add(String8(resType), *baseSet); + } + // this group doesn't exist (a file that's only in the overlay) + (*baseSet)->add(overlaySet->keyAt(overlayIndex), + overlaySet->valueAt(overlayIndex)); + // make sure all flavors are defined in the resources. + sp<AaptGroup> overlayGroup = overlaySet->valueAt(overlayIndex); + DefaultKeyedVector<AaptGroupEntry, sp<AaptFile> > overlayFiles = + overlayGroup->getFiles(); + size_t overlayGroupSize = overlayFiles.size(); + for (size_t overlayGroupIndex = 0; + overlayGroupIndex<overlayGroupSize; + overlayGroupIndex++) { + assets->addGroupEntry(overlayFiles.keyAt(overlayGroupIndex)); + } + } + } + // this overlay didn't have resources for this type + } + // try next overlay + overlay = overlay->getOverlay(); + } + return true; +} + +/* + * Inserts an attribute in a given node, only if the attribute does not + * exist. + * If errorOnFailedInsert is true, and the attribute already exists, returns false. + * Returns true otherwise, even if the attribute already exists. + */ +bool addTagAttribute(const sp<XMLNode>& node, const char* ns8, + const char* attr8, const char* value, bool errorOnFailedInsert) +{ + if (value == NULL) { + return true; + } + + const String16 ns(ns8); + const String16 attr(attr8); + + if (node->getAttribute(ns, attr) != NULL) { + if (errorOnFailedInsert) { + fprintf(stderr, "Error: AndroidManifest.xml already defines %s (in %s);" + " cannot insert new value %s.\n", + String8(attr).string(), String8(ns).string(), value); + return false; + } + + fprintf(stderr, "Warning: AndroidManifest.xml already defines %s (in %s);" + " using existing value in manifest.\n", + String8(attr).string(), String8(ns).string()); + + // don't stop the build. + return true; + } + + node->addAttribute(ns, attr, String16(value)); + return true; +} + +static void fullyQualifyClassName(const String8& package, sp<XMLNode> node, + const String16& attrName) { + XMLNode::attribute_entry* attr = node->editAttribute( + String16("http://schemas.android.com/apk/res/android"), attrName); + if (attr != NULL) { + String8 name(attr->string); + + // asdf --> package.asdf + // .asdf .a.b --> package.asdf package.a.b + // asdf.adsf --> asdf.asdf + String8 className; + const char* p = name.string(); + const char* q = strchr(p, '.'); + if (p == q) { + className += package; + className += name; + } else if (q == NULL) { + className += package; + className += "."; + className += name; + } else { + className += name; + } + NOISY(printf("Qualifying class '%s' to '%s'", name.string(), className.string())); + attr->string.setTo(String16(className)); + } +} + +status_t massageManifest(Bundle* bundle, sp<XMLNode> root) +{ + root = root->searchElement(String16(), String16("manifest")); + if (root == NULL) { + fprintf(stderr, "No <manifest> tag.\n"); + return UNKNOWN_ERROR; + } + + bool errorOnFailedInsert = bundle->getErrorOnFailedInsert(); + + if (!addTagAttribute(root, RESOURCES_ANDROID_NAMESPACE, "versionCode", + bundle->getVersionCode(), errorOnFailedInsert)) { + return UNKNOWN_ERROR; + } + if (!addTagAttribute(root, RESOURCES_ANDROID_NAMESPACE, "versionName", + bundle->getVersionName(), errorOnFailedInsert)) { + return UNKNOWN_ERROR; + } + + if (bundle->getMinSdkVersion() != NULL + || bundle->getTargetSdkVersion() != NULL + || bundle->getMaxSdkVersion() != NULL) { + sp<XMLNode> vers = root->getChildElement(String16(), String16("uses-sdk")); + if (vers == NULL) { + vers = XMLNode::newElement(root->getFilename(), String16(), String16("uses-sdk")); + root->insertChildAt(vers, 0); + } + + if (!addTagAttribute(vers, RESOURCES_ANDROID_NAMESPACE, "minSdkVersion", + bundle->getMinSdkVersion(), errorOnFailedInsert)) { + return UNKNOWN_ERROR; + } + if (!addTagAttribute(vers, RESOURCES_ANDROID_NAMESPACE, "targetSdkVersion", + bundle->getTargetSdkVersion(), errorOnFailedInsert)) { + return UNKNOWN_ERROR; + } + if (!addTagAttribute(vers, RESOURCES_ANDROID_NAMESPACE, "maxSdkVersion", + bundle->getMaxSdkVersion(), errorOnFailedInsert)) { + return UNKNOWN_ERROR; + } + } + + if (bundle->getDebugMode()) { + sp<XMLNode> application = root->getChildElement(String16(), String16("application")); + if (application != NULL) { + if (!addTagAttribute(application, RESOURCES_ANDROID_NAMESPACE, "debuggable", "true", + errorOnFailedInsert)) { + return UNKNOWN_ERROR; + } + } + } + + // Deal with manifest package name overrides + const char* manifestPackageNameOverride = bundle->getManifestPackageNameOverride(); + if (manifestPackageNameOverride != NULL) { + // Update the actual package name + XMLNode::attribute_entry* attr = root->editAttribute(String16(), String16("package")); + if (attr == NULL) { + fprintf(stderr, "package name is required with --rename-manifest-package.\n"); + return UNKNOWN_ERROR; + } + String8 origPackage(attr->string); + attr->string.setTo(String16(manifestPackageNameOverride)); + NOISY(printf("Overriding package '%s' to be '%s'\n", origPackage.string(), manifestPackageNameOverride)); + + // Make class names fully qualified + sp<XMLNode> application = root->getChildElement(String16(), String16("application")); + if (application != NULL) { + fullyQualifyClassName(origPackage, application, String16("name")); + fullyQualifyClassName(origPackage, application, String16("backupAgent")); + + Vector<sp<XMLNode> >& children = const_cast<Vector<sp<XMLNode> >&>(application->getChildren()); + for (size_t i = 0; i < children.size(); i++) { + sp<XMLNode> child = children.editItemAt(i); + String8 tag(child->getElementName()); + if (tag == "activity" || tag == "service" || tag == "receiver" || tag == "provider") { + fullyQualifyClassName(origPackage, child, String16("name")); + } else if (tag == "activity-alias") { + fullyQualifyClassName(origPackage, child, String16("name")); + fullyQualifyClassName(origPackage, child, String16("targetActivity")); + } + } + } + } + + // Deal with manifest package name overrides + const char* instrumentationPackageNameOverride = bundle->getInstrumentationPackageNameOverride(); + if (instrumentationPackageNameOverride != NULL) { + // Fix up instrumentation targets. + Vector<sp<XMLNode> >& children = const_cast<Vector<sp<XMLNode> >&>(root->getChildren()); + for (size_t i = 0; i < children.size(); i++) { + sp<XMLNode> child = children.editItemAt(i); + String8 tag(child->getElementName()); + if (tag == "instrumentation") { + XMLNode::attribute_entry* attr = child->editAttribute( + String16("http://schemas.android.com/apk/res/android"), String16("targetPackage")); + if (attr != NULL) { + attr->string.setTo(String16(instrumentationPackageNameOverride)); + } + } + } + } + + return NO_ERROR; +} + +#define ASSIGN_IT(n) \ + do { \ + ssize_t index = resources->indexOfKey(String8(#n)); \ + if (index >= 0) { \ + n ## s = resources->valueAt(index); \ + } \ + } while (0) + +status_t updatePreProcessedCache(Bundle* bundle) +{ + #if BENCHMARK + fprintf(stdout, "BENCHMARK: Starting PNG PreProcessing \n"); + long startPNGTime = clock(); + #endif /* BENCHMARK */ + + String8 source(bundle->getResourceSourceDirs()[0]); + String8 dest(bundle->getCrunchedOutputDir()); + + FileFinder* ff = new SystemFileFinder(); + CrunchCache cc(source,dest,ff); + + CacheUpdater* cu = new SystemCacheUpdater(bundle); + size_t numFiles = cc.crunch(cu); + + if (bundle->getVerbose()) + fprintf(stdout, "Crunched %d PNG files to update cache\n", (int)numFiles); + + delete ff; + delete cu; + + #if BENCHMARK + fprintf(stdout, "BENCHMARK: End PNG PreProcessing. Time Elapsed: %f ms \n" + ,(clock() - startPNGTime)/1000.0); + #endif /* BENCHMARK */ + return 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(bundle, 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())); + + // Standard flags for compiled XML and optional UTF-8 encoding + int xmlFlags = XML_COMPILE_STANDARD_RESOURCE; + + /* Only enable UTF-8 if the caller of aapt didn't specifically + * request UTF-16 encoding and the parameters of this package + * allow UTF-8 to be used. + */ + if (!bundle->getUTF16StringsOption()) { + xmlFlags |= XML_COMPILE_UTF8; + } + + // -------------------------------------------------------------- + // 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> animators; + sp<ResourceTypeSet> interpolators; + sp<ResourceTypeSet> transitions; + sp<ResourceTypeSet> scenes; + sp<ResourceTypeSet> xmls; + sp<ResourceTypeSet> raws; + sp<ResourceTypeSet> colors; + sp<ResourceTypeSet> menus; + sp<ResourceTypeSet> mipmaps; + + ASSIGN_IT(drawable); + ASSIGN_IT(layout); + ASSIGN_IT(anim); + ASSIGN_IT(animator); + ASSIGN_IT(interpolator); + ASSIGN_IT(transition); + ASSIGN_IT(scene); + ASSIGN_IT(xml); + ASSIGN_IT(raw); + ASSIGN_IT(color); + ASSIGN_IT(menu); + ASSIGN_IT(mipmap); + + 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 + if (!applyFileOverlay(bundle, assets, &drawables, "drawable") || + !applyFileOverlay(bundle, assets, &layouts, "layout") || + !applyFileOverlay(bundle, assets, &anims, "anim") || + !applyFileOverlay(bundle, assets, &animators, "animator") || + !applyFileOverlay(bundle, assets, &interpolators, "interpolator") || + !applyFileOverlay(bundle, assets, &transitions, "transition") || + !applyFileOverlay(bundle, assets, &scenes, "scene") || + !applyFileOverlay(bundle, assets, &xmls, "xml") || + !applyFileOverlay(bundle, assets, &raws, "raw") || + !applyFileOverlay(bundle, assets, &colors, "color") || + !applyFileOverlay(bundle, assets, &menus, "menu") || + !applyFileOverlay(bundle, assets, &mipmaps, "mipmap")) { + return UNKNOWN_ERROR; + } + + bool hasErrors = false; + + if (drawables != NULL) { + if (bundle->getOutputAPKFile() != NULL) { + err = preProcessImages(bundle, assets, drawables, "drawable"); + } + if (err == NO_ERROR) { + err = makeFileResources(bundle, assets, &table, drawables, "drawable"); + if (err != NO_ERROR) { + hasErrors = true; + } + } else { + hasErrors = true; + } + } + + if (mipmaps != NULL) { + if (bundle->getOutputAPKFile() != NULL) { + err = preProcessImages(bundle, assets, mipmaps, "mipmap"); + } + if (err == NO_ERROR) { + err = makeFileResources(bundle, assets, &table, mipmaps, "mipmap"); + 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 (animators != NULL) { + err = makeFileResources(bundle, assets, &table, animators, "animator"); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (transitions != NULL) { + err = makeFileResources(bundle, assets, &table, transitions, "transition"); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (scenes != NULL) { + err = makeFileResources(bundle, assets, &table, scenes, "scene"); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (interpolators != NULL) { + err = makeFileResources(bundle, assets, &table, interpolators, "interpolator"); + 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, xmlFlags); + 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, xmlFlags); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (err < NO_ERROR) { + hasErrors = true; + } + err = NO_ERROR; + } + + if (animators != NULL) { + ResourceDirIterator it(animators, String8("animator")); + while ((err=it.next()) == NO_ERROR) { + err = compileXmlFile(assets, it.getFile(), &table, xmlFlags); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (err < NO_ERROR) { + hasErrors = true; + } + err = NO_ERROR; + } + + if (interpolators != NULL) { + ResourceDirIterator it(interpolators, String8("interpolator")); + while ((err=it.next()) == NO_ERROR) { + err = compileXmlFile(assets, it.getFile(), &table, xmlFlags); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (err < NO_ERROR) { + hasErrors = true; + } + err = NO_ERROR; + } + + if (transitions != NULL) { + ResourceDirIterator it(transitions, String8("transition")); + while ((err=it.next()) == NO_ERROR) { + err = compileXmlFile(assets, it.getFile(), &table, xmlFlags); + if (err != NO_ERROR) { + hasErrors = true; + } + } + + if (err < NO_ERROR) { + hasErrors = true; + } + err = NO_ERROR; + } + + if (scenes != NULL) { + ResourceDirIterator it(scenes, String8("scene")); + while ((err=it.next()) == NO_ERROR) { + err = compileXmlFile(assets, it.getFile(), &table, xmlFlags); + 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, xmlFlags); + 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, xmlFlags); + 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, xmlFlags); + 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; + } + + if (table.validateLocalizations()) { + hasErrors = true; + } + + if (hasErrors) { + return UNKNOWN_ERROR; + } + + const sp<AaptFile> manifestFile(androidManifestFile->getFiles().valueAt(0)); + String8 manifestPath(manifestFile->getPrintableSource()); + + // Generate final compiled manifest file. + manifestFile->clearData(); + sp<XMLNode> manifestTree = XMLNode::parse(manifestFile); + if (manifestTree == NULL) { + return UNKNOWN_ERROR; + } + err = massageManifest(bundle, manifestTree); + if (err < NO_ERROR) { + return err; + } + err = compileXmlFile(assets, manifestTree, 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 + // -------------------------------------------------------------- + + ResTable finalResTable; + sp<AaptFile> resFile; + + if (table.hasResources()) { + sp<AaptSymbols> symbols = assets->getSymbolsFor(String8("R")); + err = table.addSymbols(symbols); + if (err < NO_ERROR) { + return err; + } + + 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); + fclose(fp); + } + + // Read resources back in, + finalResTable.add(resFile->getData(), resFile->getSize(), NULL); + +#if 0 + NOISY( + printf("Generated resources:\n"); + finalResTable.print(); + ) +#endif + } + + // 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. + sp<AaptFile> outManifestFile = new AaptFile(manifestFile->getSourceFile(), + manifestFile->getGroupEntry(), + manifestFile->getResourceType()); + err = compileXmlFile(assets, manifestFile, + outManifestFile, &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(outManifestFile->getData(), outManifestFile->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, finalResTable, block, NULL, "package", + packageIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, finalResTable, block, RESOURCES_ANDROID_NAMESPACE, + "sharedUserId", packageIdentChars, false) != 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, finalResTable, 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, finalResTable, block, RESOURCES_ANDROID_NAMESPACE, + "name", packageIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + } else if (strcmp16(block.getElementName(&len), instrumentation16.string()) == 0) { + if (validateAttr(manifestPath, finalResTable, block, RESOURCES_ANDROID_NAMESPACE, + "name", classIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, finalResTable, block, + RESOURCES_ANDROID_NAMESPACE, "targetPackage", + packageIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + } else if (strcmp16(block.getElementName(&len), application16.string()) == 0) { + if (validateAttr(manifestPath, finalResTable, block, RESOURCES_ANDROID_NAMESPACE, + "name", classIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, finalResTable, block, + RESOURCES_ANDROID_NAMESPACE, "permission", + packageIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, finalResTable, block, + RESOURCES_ANDROID_NAMESPACE, "process", + processIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, finalResTable, block, + RESOURCES_ANDROID_NAMESPACE, "taskAffinity", + processIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + } else if (strcmp16(block.getElementName(&len), provider16.string()) == 0) { + if (validateAttr(manifestPath, finalResTable, block, RESOURCES_ANDROID_NAMESPACE, + "name", classIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, finalResTable, block, + RESOURCES_ANDROID_NAMESPACE, "authorities", + authoritiesIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, finalResTable, block, + RESOURCES_ANDROID_NAMESPACE, "permission", + packageIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, finalResTable, 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, finalResTable, block, RESOURCES_ANDROID_NAMESPACE, + "name", classIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, finalResTable, block, + RESOURCES_ANDROID_NAMESPACE, "permission", + packageIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, finalResTable, block, + RESOURCES_ANDROID_NAMESPACE, "process", + processIdentChars, false) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, finalResTable, 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, finalResTable, block, + RESOURCES_ANDROID_NAMESPACE, "name", + packageIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + } else if (strcmp16(block.getElementName(&len), data16.string()) == 0) { + if (validateAttr(manifestPath, finalResTable, block, + RESOURCES_ANDROID_NAMESPACE, "mimeType", + typeIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + if (validateAttr(manifestPath, finalResTable, block, + RESOURCES_ANDROID_NAMESPACE, "scheme", + schemeIdentChars, true) != ATTR_OKAY) { + hasErrors = true; + } + } + } + } + + if (resFile != NULL) { + // 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 String8 flattenSymbol(const String8& symbol) { + String8 result(symbol); + ssize_t first; + if ((first = symbol.find(":", 0)) >= 0 + || (first = symbol.find(".", 0)) >= 0) { + size_t size = symbol.size(); + char* buf = result.lockBuffer(size); + for (size_t i = first; i < size; i++) { + if (buf[i] == ':' || buf[i] == '.') { + buf[i] = '_'; + } + } + result.unlockBuffer(size); + } + return result; +} + +static String8 getSymbolPackage(const String8& symbol, const sp<AaptAssets>& assets, bool pub) { + ssize_t colon = symbol.find(":", 0); + if (colon >= 0) { + return String8(symbol.string(), colon); + } + return pub ? assets->getPackage() : assets->getSymbolsPrivatePackage(); +} + +static String8 getSymbolName(const String8& symbol) { + ssize_t colon = symbol.find(":", 0); + if (colon >= 0) { + return String8(symbol.string() + colon + 1); + } + return symbol; +} + +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); + String8 realClassName(symbols->getNestedSymbols().keyAt(i)); + String8 nclassName(flattenSymbol(realClassName)); + + 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(); + + bool deprecated = false; + + String16 comment = symbols->getComment(realClassName); + fprintf(fp, "%s/** ", indentStr); + if (comment.size() > 0) { + String8 cmt(comment); + fprintf(fp, "%s\n", cmt.string()); + if (strstr(cmt.string(), "@deprecated") != NULL) { + deprecated = true; + } + } 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>\n" + "%s <colgroup align=\"left\" />\n" + "%s <colgroup align=\"left\" />\n" + "%s <tr><th>Attribute</th><th>Description</th></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()); + } + fprintf(fp, "%s <tr><td><code>{@link #%s_%s %s:%s}</code></td><td>%s</td></tr>\n", + indentStr, nclassName.string(), + flattenSymbol(name8).string(), + getSymbolPackage(name8, assets, true).string(), + getSymbolName(name8).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; + } + fprintf(fp, "%s @see #%s_%s\n", + indentStr, nclassName.string(), + flattenSymbol(sym.name).string()); + } + } + fprintf(fp, "%s */\n", getIndentSpace(indent)); + + if (deprecated) { + fprintf(fp, "%s@Deprecated\n", indentStr); + } + + 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); + } + + 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; + + bool deprecated = false; + + fprintf(fp, "%s/**\n", indentStr); + if (comment.size() > 0) { + String8 cmt(comment); + fprintf(fp, "%s <p>\n%s @attr description\n", indentStr, indentStr); + fprintf(fp, "%s %s\n", indentStr, cmt.string()); + if (strstr(cmt.string(), "@deprecated") != NULL) { + deprecated = true; + } + } 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, + getSymbolPackage(name8, assets, pub).string(), + getSymbolName(name8).string(), + indentStr, nclassName.string()); + } + if (typeComment.size() > 0) { + String8 cmt(typeComment); + fprintf(fp, "\n\n%s %s\n", indentStr, cmt.string()); + if (strstr(cmt.string(), "@deprecated") != NULL) { + deprecated = true; + } + } + if (comment.size() > 0) { + if (pub) { + fprintf(fp, + "%s <p>This corresponds to the global attribute\n" + "%s resource symbol {@link %s.R.attr#%s}.\n", + indentStr, indentStr, + getSymbolPackage(name8, assets, true).string(), + getSymbolName(name8).string()); + } else { + fprintf(fp, + "%s <p>This is a private symbol.\n", indentStr); + } + } + fprintf(fp, "%s @attr name %s:%s\n", indentStr, + getSymbolPackage(name8, assets, pub).string(), + getSymbolName(name8).string()); + fprintf(fp, "%s*/\n", indentStr); + if (deprecated) { + fprintf(fp, "%s@Deprecated\n", indentStr); + } + fprintf(fp, + "%spublic static final int %s_%s = %d;\n", + indentStr, nclassName.string(), + flattenSymbol(name8).string(), (int)pos); + } + } + } + + indent--; + fprintf(fp, "%s};\n", getIndentSpace(indent)); + return hasErrors ? UNKNOWN_ERROR : NO_ERROR; +} + +static status_t writeTextLayoutClasses( + FILE* fp, const sp<AaptAssets>& assets, + const sp<AaptSymbols>& symbols, bool includePrivate) +{ + String16 attr16("attr"); + String16 package16(assets->getPackage()); + + 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); + String8 realClassName(symbols->getNestedSymbols().keyAt(i)); + String8 nclassName(flattenSymbol(realClassName)); + + 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(); + + fprintf(fp, "int[] styleable %s {", nclassName.string()); + + for (a=0; a<NA; a++) { + if (a != 0) { + fprintf(fp, ","); + } + fprintf(fp, " 0x%08x", idents[a]); + } + + fprintf(fp, " }\n"); + + 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); + } + + 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, + "int styleable %s_%s %d\n", + nclassName.string(), + flattenSymbol(name8).string(), (int)pos); + } + } + } + + 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, + bool nonConstantId) +{ + fprintf(fp, "%spublic %sfinal class %s {\n", + getIndentSpace(indent), + indent != 0 ? "static " : "", className.string()); + indent++; + + size_t i; + status_t err = NO_ERROR; + + const char * id_format = nonConstantId ? + "%spublic static int %s=0x%08x;\n" : + "%spublic static final int %s=0x%08x;\n"; + + 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 (!assets->isJavaSymbol(sym, includePrivate)) { + continue; + } + String8 name8(sym.name); + String16 comment(sym.comment); + bool haveComment = false; + bool deprecated = false; + if (comment.size() > 0) { + haveComment = true; + String8 cmt(comment); + fprintf(fp, + "%s/** %s\n", + getIndentSpace(indent), cmt.string()); + if (strstr(cmt.string(), "@deprecated") != NULL) { + deprecated = true; + } + } 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) { + String8 cmt(typeComment); + if (!haveComment) { + haveComment = true; + fprintf(fp, + "%s/** %s\n", getIndentSpace(indent), cmt.string()); + } else { + fprintf(fp, + "%s %s\n", getIndentSpace(indent), cmt.string()); + } + if (strstr(cmt.string(), "@deprecated") != NULL) { + deprecated = true; + } + } + if (haveComment) { + fprintf(fp,"%s */\n", getIndentSpace(indent)); + } + if (deprecated) { + fprintf(fp, "%s@Deprecated\n", getIndentSpace(indent)); + } + fprintf(fp, id_format, + getIndentSpace(indent), + flattenSymbol(name8).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 (!assets->isJavaSymbol(sym, includePrivate)) { + continue; + } + String8 name8(sym.name); + String16 comment(sym.comment); + bool deprecated = false; + if (comment.size() > 0) { + String8 cmt(comment); + fprintf(fp, + "%s/** %s\n" + "%s */\n", + getIndentSpace(indent), cmt.string(), + getIndentSpace(indent)); + if (strstr(cmt.string(), "@deprecated") != NULL) { + deprecated = true; + } + } 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()); + } + if (deprecated) { + fprintf(fp, "%s@Deprecated\n", getIndentSpace(indent)); + } + fprintf(fp, "%spublic static final String %s=\"%s\";\n", + getIndentSpace(indent), + flattenSymbol(name8).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, nonConstantId); + } + 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; +} + +static status_t writeTextSymbolClass( + FILE* fp, const sp<AaptAssets>& assets, bool includePrivate, + const sp<AaptSymbols>& symbols, const String8& className) +{ + 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 (!assets->isJavaSymbol(sym, includePrivate)) { + continue; + } + + String8 name8(sym.name); + fprintf(fp, "int %s %s 0x%08x\n", + className.string(), + flattenSymbol(name8).string(), (int)sym.int32Val); + } + + 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") { + err = writeTextLayoutClasses(fp, assets, nsymbols, includePrivate); + } else { + err = writeTextSymbolClass(fp, assets, includePrivate, nsymbols, nclassName); + } + if (err != NO_ERROR) { + return err; + } + } + + return NO_ERROR; +} + +status_t writeResourceSymbols(Bundle* bundle, const sp<AaptAssets>& assets, + const String8& package, bool includePrivate) +{ + if (!bundle->getRClassDir()) { + return NO_ERROR; + } + + const char* textSymbolsDest = bundle->getOutputTextSymbols(); + + String8 R("R"); + 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, bundle->getNonConstantId()); + if (err != NO_ERROR) { + return err; + } + fclose(fp); + + if (textSymbolsDest != NULL && R == className) { + String8 textDest(textSymbolsDest); + textDest.appendPath(className); + textDest.append(".txt"); + + FILE* fp = fopen(textDest.string(), "w+"); + if (fp == NULL) { + fprintf(stderr, "ERROR: Unable to open text symbol file %s: %s\n", + textDest.string(), strerror(errno)); + return UNKNOWN_ERROR; + } + if (bundle->getVerbose()) { + printf(" Writing text symbols for class %s.\n", className.string()); + } + + status_t err = writeTextSymbolClass(fp, assets, includePrivate, symbols, + className); + if (err != NO_ERROR) { + return err; + } + fclose(fp); + } + + // If we were asked to generate a dependency file, we'll go ahead and add this R.java + // as a target in the dependency file right next to it. + if (bundle->getGenDependencies() && R == className) { + // Add this R.java to the dependency file + String8 dependencyFile(bundle->getRClassDir()); + dependencyFile.appendPath("R.java.d"); + + FILE *fp = fopen(dependencyFile.string(), "a"); + fprintf(fp,"%s \\\n", dest.string()); + fclose(fp); + } + } + + return NO_ERROR; +} + + +class ProguardKeepSet +{ +public: + // { rule --> { file locations } } + KeyedVector<String8, SortedVector<String8> > rules; + + void add(const String8& rule, const String8& where); +}; + +void ProguardKeepSet::add(const String8& rule, const String8& where) +{ + ssize_t index = rules.indexOfKey(rule); + if (index < 0) { + index = rules.add(rule, SortedVector<String8>()); + } + rules.editValueAt(index).add(where); +} + +void +addProguardKeepRule(ProguardKeepSet* keep, const String8& inClassName, + const char* pkg, const String8& srcName, int line) +{ + String8 className(inClassName); + if (pkg != NULL) { + // asdf --> package.asdf + // .asdf .a.b --> package.asdf package.a.b + // asdf.adsf --> asdf.asdf + const char* p = className.string(); + const char* q = strchr(p, '.'); + if (p == q) { + className = pkg; + className.append(inClassName); + } else if (q == NULL) { + className = pkg; + className.append("."); + className.append(inClassName); + } + } + + String8 rule("-keep class "); + rule += className; + rule += " { <init>(...); }"; + + String8 location("view "); + location += srcName; + char lineno[20]; + sprintf(lineno, ":%d", line); + location += lineno; + + keep->add(rule, location); +} + +void +addProguardKeepMethodRule(ProguardKeepSet* keep, const String8& memberName, + const char* pkg, const String8& srcName, int line) +{ + String8 rule("-keepclassmembers class * { *** "); + rule += memberName; + rule += "(...); }"; + + String8 location("onClick "); + location += srcName; + char lineno[20]; + sprintf(lineno, ":%d", line); + location += lineno; + + keep->add(rule, location); +} + +status_t +writeProguardForAndroidManifest(ProguardKeepSet* keep, const sp<AaptAssets>& assets) +{ + status_t err; + ResXMLTree tree; + size_t len; + ResXMLTree::event_code_t code; + int depth = 0; + bool inApplication = false; + String8 error; + sp<AaptGroup> assGroup; + sp<AaptFile> assFile; + String8 pkg; + + // First, look for a package file to parse. This is required to + // be able to generate the resource information. + assGroup = assets->getFiles().valueFor(String8("AndroidManifest.xml")); + if (assGroup == NULL) { + fprintf(stderr, "ERROR: No AndroidManifest.xml file found.\n"); + return -1; + } + + if (assGroup->getFiles().size() != 1) { + fprintf(stderr, "warning: Multiple AndroidManifest.xml files found, using %s\n", + assGroup->getFiles().valueAt(0)->getPrintableSource().string()); + } + + assFile = assGroup->getFiles().valueAt(0); + + err = parseXMLResource(assFile, &tree); + if (err != NO_ERROR) { + return err; + } + + tree.restart(); + + while ((code=tree.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::END_TAG) { + if (/* name == "Application" && */ depth == 2) { + inApplication = false; + } + depth--; + continue; + } + if (code != ResXMLTree::START_TAG) { + continue; + } + depth++; + String8 tag(tree.getElementName(&len)); + // printf("Depth %d tag %s\n", depth, tag.string()); + bool keepTag = false; + if (depth == 1) { + if (tag != "manifest") { + fprintf(stderr, "ERROR: manifest does not start with <manifest> tag\n"); + return -1; + } + pkg = getAttribute(tree, NULL, "package", NULL); + } else if (depth == 2) { + if (tag == "application") { + inApplication = true; + keepTag = true; + + String8 agent = getAttribute(tree, "http://schemas.android.com/apk/res/android", + "backupAgent", &error); + if (agent.length() > 0) { + addProguardKeepRule(keep, agent, pkg.string(), + assFile->getPrintableSource(), tree.getLineNumber()); + } + } else if (tag == "instrumentation") { + keepTag = true; + } + } + if (!keepTag && inApplication && depth == 3) { + if (tag == "activity" || tag == "service" || tag == "receiver" || tag == "provider") { + keepTag = true; + } + } + if (keepTag) { + String8 name = getAttribute(tree, "http://schemas.android.com/apk/res/android", + "name", &error); + if (error != "") { + fprintf(stderr, "ERROR: %s\n", error.string()); + return -1; + } + if (name.length() > 0) { + addProguardKeepRule(keep, name, pkg.string(), + assFile->getPrintableSource(), tree.getLineNumber()); + } + } + } + + return NO_ERROR; +} + +struct NamespaceAttributePair { + const char* ns; + const char* attr; + + NamespaceAttributePair(const char* n, const char* a) : ns(n), attr(a) {} + NamespaceAttributePair() : ns(NULL), attr(NULL) {} +}; + +status_t +writeProguardForXml(ProguardKeepSet* keep, const sp<AaptFile>& layoutFile, + const char* startTag, const KeyedVector<String8, Vector<NamespaceAttributePair> >* tagAttrPairs) +{ + status_t err; + ResXMLTree tree; + size_t len; + ResXMLTree::event_code_t code; + + err = parseXMLResource(layoutFile, &tree); + if (err != NO_ERROR) { + return err; + } + + tree.restart(); + + if (startTag != NULL) { + bool haveStart = false; + while ((code=tree.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code != ResXMLTree::START_TAG) { + continue; + } + String8 tag(tree.getElementName(&len)); + if (tag == startTag) { + haveStart = true; + } + break; + } + if (!haveStart) { + return NO_ERROR; + } + } + + while ((code=tree.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code != ResXMLTree::START_TAG) { + continue; + } + String8 tag(tree.getElementName(&len)); + + // If there is no '.', we'll assume that it's one of the built in names. + if (strchr(tag.string(), '.')) { + addProguardKeepRule(keep, tag, NULL, + layoutFile->getPrintableSource(), tree.getLineNumber()); + } else if (tagAttrPairs != NULL) { + ssize_t tagIndex = tagAttrPairs->indexOfKey(tag); + if (tagIndex >= 0) { + const Vector<NamespaceAttributePair>& nsAttrVector = tagAttrPairs->valueAt(tagIndex); + for (size_t i = 0; i < nsAttrVector.size(); i++) { + const NamespaceAttributePair& nsAttr = nsAttrVector[i]; + + ssize_t attrIndex = tree.indexOfAttribute(nsAttr.ns, nsAttr.attr); + if (attrIndex < 0) { + // fprintf(stderr, "%s:%d: <%s> does not have attribute %s:%s.\n", + // layoutFile->getPrintableSource().string(), tree.getLineNumber(), + // tag.string(), nsAttr.ns, nsAttr.attr); + } else { + size_t len; + addProguardKeepRule(keep, + String8(tree.getAttributeStringValue(attrIndex, &len)), NULL, + layoutFile->getPrintableSource(), tree.getLineNumber()); + } + } + } + } + ssize_t attrIndex = tree.indexOfAttribute(RESOURCES_ANDROID_NAMESPACE, "onClick"); + if (attrIndex >= 0) { + size_t len; + addProguardKeepMethodRule(keep, + String8(tree.getAttributeStringValue(attrIndex, &len)), NULL, + layoutFile->getPrintableSource(), tree.getLineNumber()); + } + } + + return NO_ERROR; +} + +static void addTagAttrPair(KeyedVector<String8, Vector<NamespaceAttributePair> >* dest, + const char* tag, const char* ns, const char* attr) { + String8 tagStr(tag); + ssize_t index = dest->indexOfKey(tagStr); + + if (index < 0) { + Vector<NamespaceAttributePair> vector; + vector.add(NamespaceAttributePair(ns, attr)); + dest->add(tagStr, vector); + } else { + dest->editValueAt(index).add(NamespaceAttributePair(ns, attr)); + } +} + +status_t +writeProguardForLayouts(ProguardKeepSet* keep, const sp<AaptAssets>& assets) +{ + status_t err; + + // tag:attribute pairs that should be checked in layout files. + KeyedVector<String8, Vector<NamespaceAttributePair> > kLayoutTagAttrPairs; + addTagAttrPair(&kLayoutTagAttrPairs, "view", NULL, "class"); + addTagAttrPair(&kLayoutTagAttrPairs, "fragment", NULL, "class"); + addTagAttrPair(&kLayoutTagAttrPairs, "fragment", RESOURCES_ANDROID_NAMESPACE, "name"); + + // tag:attribute pairs that should be checked in xml files. + KeyedVector<String8, Vector<NamespaceAttributePair> > kXmlTagAttrPairs; + addTagAttrPair(&kXmlTagAttrPairs, "PreferenceScreen", RESOURCES_ANDROID_NAMESPACE, "fragment"); + addTagAttrPair(&kXmlTagAttrPairs, "header", RESOURCES_ANDROID_NAMESPACE, "fragment"); + + const Vector<sp<AaptDir> >& dirs = assets->resDirs(); + const size_t K = dirs.size(); + for (size_t k=0; k<K; k++) { + const sp<AaptDir>& d = dirs.itemAt(k); + const String8& dirName = d->getLeaf(); + const char* startTag = NULL; + const KeyedVector<String8, Vector<NamespaceAttributePair> >* tagAttrPairs = NULL; + if ((dirName == String8("layout")) || (strncmp(dirName.string(), "layout-", 7) == 0)) { + tagAttrPairs = &kLayoutTagAttrPairs; + } else if ((dirName == String8("xml")) || (strncmp(dirName.string(), "xml-", 4) == 0)) { + startTag = "PreferenceScreen"; + tagAttrPairs = &kXmlTagAttrPairs; + } else if ((dirName == String8("menu")) || (strncmp(dirName.string(), "menu-", 5) == 0)) { + startTag = "menu"; + tagAttrPairs = NULL; + } else { + continue; + } + + const KeyedVector<String8,sp<AaptGroup> > groups = d->getFiles(); + const size_t N = groups.size(); + for (size_t i=0; i<N; i++) { + const sp<AaptGroup>& group = groups.valueAt(i); + const DefaultKeyedVector<AaptGroupEntry, sp<AaptFile> >& files = group->getFiles(); + const size_t M = files.size(); + for (size_t j=0; j<M; j++) { + err = writeProguardForXml(keep, files.valueAt(j), startTag, tagAttrPairs); + if (err < 0) { + return err; + } + } + } + } + // Handle the overlays + sp<AaptAssets> overlay = assets->getOverlay(); + if (overlay.get()) { + return writeProguardForLayouts(keep, overlay); + } + + return NO_ERROR; +} + +status_t +writeProguardFile(Bundle* bundle, const sp<AaptAssets>& assets) +{ + status_t err = -1; + + if (!bundle->getProguardFile()) { + return NO_ERROR; + } + + ProguardKeepSet keep; + + err = writeProguardForAndroidManifest(&keep, assets); + if (err < 0) { + return err; + } + + err = writeProguardForLayouts(&keep, assets); + if (err < 0) { + return err; + } + + FILE* fp = fopen(bundle->getProguardFile(), "w+"); + if (fp == NULL) { + fprintf(stderr, "ERROR: Unable to open class file %s: %s\n", + bundle->getProguardFile(), strerror(errno)); + return UNKNOWN_ERROR; + } + + const KeyedVector<String8, SortedVector<String8> >& rules = keep.rules; + const size_t N = rules.size(); + for (size_t i=0; i<N; i++) { + const SortedVector<String8>& locations = rules.valueAt(i); + const size_t M = locations.size(); + for (size_t j=0; j<M; j++) { + fprintf(fp, "# %s\n", locations.itemAt(j).string()); + } + fprintf(fp, "%s\n\n", rules.keyAt(i).string()); + } + fclose(fp); + + return err; +} + +// Loops through the string paths and writes them to the file pointer +// Each file path is written on its own line with a terminating backslash. +status_t writePathsToFile(const sp<FilePathStore>& files, FILE* fp) +{ + status_t deps = -1; + for (size_t file_i = 0; file_i < files->size(); ++file_i) { + // Add the full file path to the dependency file + fprintf(fp, "%s \\\n", files->itemAt(file_i).string()); + deps++; + } + return deps; +} + +status_t +writeDependencyPreReqs(Bundle* bundle, const sp<AaptAssets>& assets, FILE* fp, bool includeRaw) +{ + status_t deps = -1; + deps += writePathsToFile(assets->getFullResPaths(), fp); + if (includeRaw) { + deps += writePathsToFile(assets->getFullAssetPaths(), fp); + } + return deps; +} diff --git a/tools/aapt/ResourceFilter.cpp b/tools/aapt/ResourceFilter.cpp new file mode 100644 index 000000000000..8cfd2a5ec0d6 --- /dev/null +++ b/tools/aapt/ResourceFilter.cpp @@ -0,0 +1,112 @@ +// +// Copyright 2011 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#include "ResourceFilter.h" + +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::isEmpty() const +{ + return mData.size() == 0; +} + +bool +ResourceFilter::match(int axis, uint32_t value) const +{ + 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(int axis, const ResTable_config& config) const +{ + return match(axis, AaptGroupEntry::getConfigValueForAxis(config, axis)); +} + +bool +ResourceFilter::match(const ResTable_config& config) const +{ + for (int i=AXIS_START; i<=AXIS_END; i++) { + if (!match(i, AaptGroupEntry::getConfigValueForAxis(config, i))) { + return false; + } + } + return true; +} + +const SortedVector<uint32_t>* ResourceFilter::configsForAxis(int axis) const +{ + ssize_t index = mData.indexOfKey(axis); + if (index < 0) { + return NULL; + } + return &mData.valueAt(index); +} diff --git a/tools/aapt/ResourceFilter.h b/tools/aapt/ResourceFilter.h new file mode 100644 index 000000000000..647b7bbfdd23 --- /dev/null +++ b/tools/aapt/ResourceFilter.h @@ -0,0 +1,33 @@ +// +// Copyright 2011 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#ifndef RESOURCE_FILTER_H +#define RESOURCE_FILTER_H + +#include "AaptAssets.h" + +/** + * Implements logic for parsing and handling "-c" and "--preferred-configurations" + * options. + */ +class ResourceFilter +{ +public: + ResourceFilter() : mData(), mContainsPseudo(false) {} + status_t parse(const char* arg); + bool isEmpty() const; + bool match(int axis, uint32_t value) const; + bool match(int axis, const ResTable_config& config) const; + bool match(const ResTable_config& config) const; + const SortedVector<uint32_t>* configsForAxis(int axis) const; + inline bool containsPseudo() const { return mContainsPseudo; } + +private: + KeyedVector<int,SortedVector<uint32_t> > mData; + bool mContainsPseudo; +}; + +#endif diff --git a/tools/aapt/ResourceIdCache.cpp b/tools/aapt/ResourceIdCache.cpp new file mode 100644 index 000000000000..e03f4f668a1e --- /dev/null +++ b/tools/aapt/ResourceIdCache.cpp @@ -0,0 +1,107 @@ +// +// Copyright 2012 The Android Open Source Project +// +// Manage a resource ID cache. + +#define LOG_TAG "ResourceIdCache" + +#include <utils/String16.h> +#include <utils/Log.h> +#include "ResourceIdCache.h" +#include <map> +using namespace std; + + +static size_t mHits = 0; +static size_t mMisses = 0; +static size_t mCollisions = 0; + +static const size_t MAX_CACHE_ENTRIES = 2048; +static const android::String16 TRUE16("1"); +static const android::String16 FALSE16("0"); + +struct CacheEntry { + // concatenation of the relevant strings into a single instance + android::String16 hashedName; + uint32_t id; + + CacheEntry() {} + CacheEntry(const android::String16& name, uint32_t resId) : hashedName(name), id(resId) { } +}; + +static map< uint32_t, CacheEntry > mIdMap; + + +// djb2; reasonable choice for strings when collisions aren't particularly important +static inline uint32_t hashround(uint32_t hash, int c) { + return ((hash << 5) + hash) + c; /* hash * 33 + c */ +} + +static uint32_t hash(const android::String16& hashableString) { + uint32_t hash = 5381; + const char16_t* str = hashableString.string(); + while (int c = *str++) hash = hashround(hash, c); + return hash; +} + +namespace android { + +static inline String16 makeHashableName(const android::String16& package, + const android::String16& type, + const android::String16& name, + bool onlyPublic) { + String16 hashable = String16(name); + hashable += type; + hashable += package; + hashable += (onlyPublic ? TRUE16 : FALSE16); + return hashable; +} + +uint32_t ResourceIdCache::lookup(const android::String16& package, + const android::String16& type, + const android::String16& name, + bool onlyPublic) { + const String16 hashedName = makeHashableName(package, type, name, onlyPublic); + const uint32_t hashcode = hash(hashedName); + map<uint32_t, CacheEntry>::iterator item = mIdMap.find(hashcode); + if (item == mIdMap.end()) { + // cache miss + mMisses++; + return 0; + } + + // legit match? + if (hashedName == (*item).second.hashedName) { + mHits++; + return (*item).second.id; + } + + // collision + mCollisions++; + mIdMap.erase(hashcode); + return 0; +} + +// returns the resource ID being stored, for callsite convenience +uint32_t ResourceIdCache::store(const android::String16& package, + const android::String16& type, + const android::String16& name, + bool onlyPublic, + uint32_t resId) { + if (mIdMap.size() < MAX_CACHE_ENTRIES) { + const String16 hashedName = makeHashableName(package, type, name, onlyPublic); + const uint32_t hashcode = hash(hashedName); + mIdMap[hashcode] = CacheEntry(hashedName, resId); + } + return resId; +} + +void ResourceIdCache::dump() { + printf("ResourceIdCache dump:\n"); + printf("Size: %ld\n", mIdMap.size()); + printf("Hits: %ld\n", mHits); + printf("Misses: %ld\n", mMisses); + printf("(Collisions: %ld)\n", mCollisions); +} + +} diff --git a/tools/aapt/ResourceIdCache.h b/tools/aapt/ResourceIdCache.h new file mode 100644 index 000000000000..65f77818b055 --- /dev/null +++ b/tools/aapt/ResourceIdCache.h @@ -0,0 +1,30 @@ +// +// Copyright 2012 The Android Open Source Project +// +// Manage a resource ID cache. + +#ifndef RESOURCE_ID_CACHE_H +#define RESOURCE_ID_CACHE_H + +namespace android { +class android::String16; + +class ResourceIdCache { +public: + static uint32_t lookup(const android::String16& package, + const android::String16& type, + const android::String16& name, + bool onlyPublic); + + static uint32_t store(const android::String16& package, + const android::String16& type, + const android::String16& name, + bool onlyPublic, + uint32_t resId); + + static void dump(void); +}; + +} + +#endif diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp new file mode 100644 index 000000000000..52ebaf0f7775 --- /dev/null +++ b/tools/aapt/ResourceTable.cpp @@ -0,0 +1,3905 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#include "ResourceTable.h" + +#include "XMLNode.h" +#include "ResourceFilter.h" +#include "ResourceIdCache.h" + +#include <androidfw/ResourceTypes.h> +#include <utils/ByteOrder.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; + } + + return compileXmlFile(assets, root, target, table, options); +} + +status_t compileXmlFile(const sp<AaptAssets>& assets, + const sp<AaptFile>& target, + const sp<AaptFile>& outTarget, + ResourceTable* table, + int options) +{ + sp<XMLNode> root = XMLNode::parse(target); + if (root == NULL) { + return UNKNOWN_ERROR; + } + + return compileXmlFile(assets, root, outTarget, table, options); +} + +status_t compileXmlFile(const sp<AaptAssets>& assets, + const sp<XMLNode>& root, + const sp<AaptFile>& target, + ResourceTable* table, + int options) +{ + if ((options&XML_COMPILE_STRIP_WHITESPACE) != 0) { + root->removeWhitespace(true, NULL); + } else if ((options&XML_COMPILE_COMPACT_WHITESPACE) != 0) { + root->removeWhitespace(false, NULL); + } + + if ((options&XML_COMPILE_UTF8) != 0) { + root->setUTF8(true); + } + + 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[11]; + 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>\n" + "<colgroup align=\"left\" />\n" + "<colgroup align=\"left\" />\n" + "<colgroup align=\"left\" />\n" + "<tr><th>Constant</th><th>Value</th><th>Description</th></tr>")); + } + + enumOrFlagsComment.append(String16("\n<tr><td><code>")); + enumOrFlagsComment.append(itemIdent); + enumOrFlagsComment.append(String16("</code></td><td>")); + enumOrFlagsComment.append(value); + enumOrFlagsComment.append(String16("</td><td>")); + if (block.getComment(&len)) { + enumOrFlagsComment.append(String16(block.getComment(&len))); + } + enumOrFlagsComment.append(String16("</td></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 isFormatted, + const String16& product, + 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, isFormatted, + 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; +} + +/* + * Returns true if needle is one of the elements in the comma-separated list + * haystack, false otherwise. + */ +bool isInProductList(const String16& needle, const String16& haystack) { + const char16_t *needle2 = needle.string(); + const char16_t *haystack2 = haystack.string(); + size_t needlesize = needle.size(); + + while (*haystack2 != '\0') { + if (strncmp16(haystack2, needle2, needlesize) == 0) { + if (haystack2[needlesize] == '\0' || haystack2[needlesize] == ',') { + return true; + } + } + + while (*haystack2 != '\0' && *haystack2 != ',') { + haystack2++; + } + if (*haystack2 == ',') { + haystack2++; + } + } + + return false; +} + +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 isFormatted, + const String16& product, + 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, + isFormatted, pseudolocalize); + + if (err < NO_ERROR) { + return err; + } + + /* + * If a product type was specified on the command line + * and also in the string, and the two are not the same, + * return without adding the string. + */ + + const char *bundleProduct = bundle->getProduct(); + if (bundleProduct == NULL) { + bundleProduct = ""; + } + + if (product.size() != 0) { + /* + * If the command-line-specified product is empty, only "default" + * matches. Other variants are skipped. This is so generation + * of the R.java file when the product is not known is predictable. + */ + + if (bundleProduct[0] == '\0') { + if (strcmp16(String16("default").string(), product.string()) != 0) { + return NO_ERROR; + } + } else { + /* + * The command-line product is not empty. + * If the product for this string is on the command-line list, + * it matches. "default" also matches, but only if nothing + * else has matched already. + */ + + if (isInProductList(product, String16(bundleProduct))) { + ; + } else if (strcmp16(String16("default").string(), product.string()) == 0 && + !outTable->hasBagOrEntry(myPackage, curType, ident, config)) { + ; + } else { + return NO_ERROR; + } + } + } + + 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 public_padding16("public-padding"); + const String16 private_symbols16("private-symbols"); + const String16 java_symbol16("java-symbol"); + const String16 add_resource16("add-resource"); + 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 formatted16("formatted"); + const String16 false16("false"); + + const String16 myPackage(assets->getPackage()); + + bool hasErrors = false; + + bool fileIsTranslatable = true; + if (strstr(in->getPrintableSource().string(), "donottranslate") != NULL) { + fileIsTranslatable = false; + } + + DefaultKeyedVector<String16, 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 curIsBagReplaceOnOverwrite = false; + bool curIsStyled = false; + bool curIsPseudolocalizable = false; + bool curIsFormatted = fileIsTranslatable; + 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.replaceValueFor(type, ident+1); + } + } else if (nextPublicId.indexOfKey(type) < 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.valueFor(type); + nextPublicId.replaceValueFor(type, ident+1); + } + + 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), public_padding16.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-padding>\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-padding>\n"); + hasErrors = localHasErrors = true; + } + name = String16(block.getAttributeStringValue(nameIdx, &len)); + + uint32_t start = 0; + ssize_t startIdx = block.indexOfAttribute(NULL, "start"); + if (startIdx >= 0) { + const char16_t* startStr = block.getAttributeStringValue(startIdx, &len); + Res_value startValue; + if (!ResTable::stringToInt(startStr, len, &startValue)) { + srcPos.error("Given 'start' attribute is not an integer: %s\n", + String8(block.getAttributeStringValue(startIdx, &len)).string()); + hasErrors = localHasErrors = true; + } else { + start = startValue.data; + } + } else if (nextPublicId.indexOfKey(type) < 0) { + srcPos.error("No 'start' attribute supplied <public-padding>," + " and no previous id defined in this file.\n"); + hasErrors = localHasErrors = true; + } else if (!localHasErrors) { + start = nextPublicId.valueFor(type); + } + + uint32_t end = 0; + ssize_t endIdx = block.indexOfAttribute(NULL, "end"); + if (endIdx >= 0) { + const char16_t* endStr = block.getAttributeStringValue(endIdx, &len); + Res_value endValue; + if (!ResTable::stringToInt(endStr, len, &endValue)) { + srcPos.error("Given 'end' attribute is not an integer: %s\n", + String8(block.getAttributeStringValue(endIdx, &len)).string()); + hasErrors = localHasErrors = true; + } else { + end = endValue.data; + } + } else { + srcPos.error("No 'end' attribute supplied <public-padding>\n"); + hasErrors = localHasErrors = true; + } + + if (end >= start) { + nextPublicId.replaceValueFor(type, end+1); + } else { + srcPos.error("Padding start '%ul' is after end '%ul'\n", + start, end); + hasErrors = localHasErrors = true; + } + + String16 comment( + block.getComment(&len) ? block.getComment(&len) : nulStr); + for (uint32_t curIdent=start; curIdent<=end; curIdent++) { + if (localHasErrors) { + break; + } + String16 curName(name); + char buf[64]; + sprintf(buf, "%d", (int)(end-curIdent+1)); + curName.append(String16(buf)); + + err = outTable->addEntry(srcPos, myPackage, type, curName, + String16("padding"), NULL, &curParams, false, + ResTable_map::TYPE_STRING, overwrite); + if (err < NO_ERROR) { + hasErrors = localHasErrors = true; + break; + } + err = outTable->addPublic(srcPos, myPackage, type, + curName, curIdent); + if (err < NO_ERROR) { + hasErrors = localHasErrors = true; + break; + } + sp<AaptSymbols> symbols = assets->getSymbolsFor(String8("R")); + if (symbols != NULL) { + symbols = symbols->addNestedSymbol(String8(type), srcPos); + } + if (symbols != NULL) { + symbols->makeSymbolPublic(String8(curName), srcPos); + symbols->appendComment(String8(curName), 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), public_padding16.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), java_symbol16.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)); + + sp<AaptSymbols> symbols = assets->getJavaSymbolsFor(String8("R")); + if (symbols != NULL) { + symbols = symbols->addNestedSymbol(String8(type), srcPos); + } + if (symbols != NULL) { + symbols->makeSymbolJavaSymbol(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), java_symbol16.string()) == 0) { + break; + } + } + } + continue; + + + } else if (strcmp16(block.getElementName(&len), add_resource16.string()) == 0) { + SourcePos srcPos(in->getPrintableSource(), block.getLineNumber()); + + String16 typeName; + ssize_t typeIdx = block.indexOfAttribute(NULL, "type"); + if (typeIdx < 0) { + srcPos.error("A 'type' attribute is required for <add-resource>\n"); + hasErrors = localHasErrors = true; + } + typeName = 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 <add-resource>\n"); + hasErrors = localHasErrors = true; + } + name = String16(block.getAttributeStringValue(nameIdx, &len)); + + outTable->canAddEntry(srcPos, myPackage, typeName, name); + + while ((code=block.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { + if (code == ResXMLTree::END_TAG) { + if (strcmp16(block.getElementName(&len), add_resource16.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; + String16 formatted; + + 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)); + } else if (strcmp16(attr, formatted16.string()) == 0) { + formatted.setTo(block.getAttributeStringValue(i, &length)); + } + } + + if (name.size() > 0) { + if (translatable == false16) { + curIsFormatted = false; + // 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); + } + + if (formatted == false16) { + curIsFormatted = false; + } + } + + 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; + curIsBagReplaceOnOverwrite = 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) { + // Check whether these strings need valid formats. + // (simplified form of what string16 does above) + 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, translatable16.string()) == 0 + || strcmp16(attr, formatted16.string()) == 0) { + const uint16_t* value = block.getAttributeStringValue(i, &length); + if (strcmp16(value, false16.string()) == 0) { + curIsFormatted = false; + break; + } + } + } + + curTag = &string_array16; + curType = array16; + curFormat = ResTable_map::TYPE_REFERENCE|ResTable_map::TYPE_STRING; + curIsBag = true; + curIsBagReplaceOnOverwrite = 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; + curIsBagReplaceOnOverwrite = 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 product; + identIdx = block.indexOfAttribute(NULL, "product"); + if (identIdx >= 0) { + product = String16(block.getAttributeStringValue(identIdx, &len)); + } + + 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, + overwrite, curIsBagReplaceOnOverwrite); + 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, curIsFormatted, + product, 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, + curIsFormatted, product, 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, curIsFormatted, + product, 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, + curIsFormatted, product, + true, overwrite, 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 resources 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, overwrite, + 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 overlay, + bool replace, bool isId) +{ + status_t result = NO_ERROR; + + // 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 + if (overlay && !mBundle->getAutoAddOverlay() && !hasBagOrEntry(package, type, name)) { + bool canAdd = false; + sp<Package> p = mPackages.valueFor(package); + if (p != NULL) { + sp<Type> t = p->getTypes().valueFor(type); + if (t != NULL) { + if (t->getCanAddEntries().indexOf(name) >= 0) { + canAdd = true; + } + } + } + if (!canAdd) { + sourcePos.error("Resource does not already exist in overlay at '%s'; use <add-resource> to add.\n", + String8(name).string()); + return UNKNOWN_ERROR; + } + } + sp<Entry> e = getEntry(package, type, name, sourcePos, overlay, params); + if (e == NULL) { + return UNKNOWN_ERROR; + } + + // If a parent is explicitly specified, set it. + if (bagParent.size() > 0) { + e->setParent(bagParent); + } + + if ((result = e->makeItABag(sourcePos)) != NO_ERROR) { + return result; + } + + if (overlay && replace) { + return e->emptyBag(sourcePos); + } + return result; +} + +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, replace, params); + if (e == NULL) { + return UNKNOWN_ERROR; + } + + // If a parent is explicitly specified, set it. + if (bagParent.size() > 0) { + 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& package, + const String16& type, + const String16& name, + const ResTable_config& config) 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) { + sp<Entry> e = c->getEntries().valueFor(config); + if (e != 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; +} + +void ResourceTable::canAddEntry(const SourcePos& pos, + const String16& package, const String16& type, const String16& name) +{ + sp<Type> t = getType(package, type, pos); + if (t != NULL) { + t->canAddEntry(name); + } +} + +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 +{ + uint32_t id = ResourceIdCache::lookup(package, type, name, onlyPublic); + if (id != 0) return id; // cache hit + + 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 ResourceIdCache::store(package, type, name, onlyPublic, rid); + } + return ResourceIdCache::store(package, type, name, onlyPublic, + 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 ResourceIdCache::store(package, type, name, onlyPublic, + 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; + bool refOnlyPublic = true; + if (!ResTable::expandResourceRef( + ref.string(), ref.size(), &package, &type, &name, + defType, defPackage ? defPackage:&mAssetsPackage, + outErrorMsg, &refOnlyPublic)) { + 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 && refOnlyPublic); + 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, const String8* configTypeName, + const ConfigDescription* config) +{ + 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) { + String8 configStr; + if (config != NULL) { + configStr = config->toString(); + } else { + configStr = "(null)"; + } + NOISY(printf("Adding to pool string style #%d config %s: %s\n", + style != NULL ? style->size() : 0, + configStr.string(), String8(finalStr).string())); + if (style != NULL && style->size() > 0) { + outValue->data = pool->add(finalStr, *style, configTypeName, config); + } else { + outValue->data = pool->add(finalStr, true, configTypeName, config); + } + } 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; + while (pos < end) { + const char16_t* start = pos; + 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; + } + 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>::const_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: warning: " + "**** string '%s' has no default or required localization " + "for '%s' in %s\n", + String8(nameIter->first).string(), + config.string(), + mBundle->getResourceSourceDirs()[0]); + } + } + } + } + } while (comma != NULL); + } + } + + return err; +} + +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 ConfigDescription nullConfig; + + const size_t N = mOrderedPackages.size(); + size_t pi; + + const static String16 mipmap16("mipmap"); + + bool useUTF8 = !bundle->getUTF16StringsOption(); + + // Iterate through all data, collecting all values (strings, + // references, etc). + StringPool valueStrings(useUTF8); + Vector<sp<Entry> > allEntries; + for (pi=0; pi<N; pi++) { + sp<Package> p = mOrderedPackages.itemAt(pi); + if (p->getTypes().size() == 0) { + // Empty, skip! + continue; + } + + StringPool typeStrings(useUTF8); + StringPool keyStrings(useUTF8); + + 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; + } + const String16 typeName(t->getName()); + typeStrings.add(typeName, false); + + // This is a hack to tweak the sorting order of the final strings, + // to put stuff that is generally not language-specific first. + String8 configTypeName(typeName); + if (configTypeName == "drawable" || configTypeName == "layout" + || configTypeName == "color" || configTypeName == "anim" + || configTypeName == "interpolator" || configTypeName == "animator" + || configTypeName == "xml" || configTypeName == "menu" + || configTypeName == "mipmap" || configTypeName == "raw") { + configTypeName = "1complex"; + } else { + configTypeName = "2value"; + } + + const bool filterable = (typeName != mipmap16); + + 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 (filterable && !filter.match(config)) { + continue; + } + sp<Entry> e = c->getEntries().valueAt(ei); + if (e == NULL) { + continue; + } + e->setNameIndex(keyStrings.add(e->getName(), true)); + + // If this entry has no values for other configs, + // and is the default config, then it is special. Otherwise + // we want to add it with the config info. + ConfigDescription* valueConfig = NULL; + if (N != 1 || config == nullConfig) { + valueConfig = &config; + } + + status_t err = e->prepareFlatten(&valueStrings, this, + &configTypeName, &config); + if (err != NO_ERROR) { + return err; + } + allEntries.add(e); + } + } + } + + p->setTypeStrings(typeStrings.createStringBlock()); + p->setKeyStrings(keyStrings.createStringBlock()); + } + + if (bundle->getOutputAPKFile() != NULL) { + // Now we want to sort the value strings for better locality. This will + // cause the positions of the strings to change, so we need to go back + // through out resource entries and update them accordingly. Only need + // to do this if actually writing the output file. + valueStrings.sortByConfig(); + for (pi=0; pi<allEntries.size(); pi++) { + allEntries[pi]->remapStringValue(&valueStrings); + } + } + + 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 bool filterable = (typeName != mipmap16); + + const size_t N = t != NULL ? t->getOrderedConfigs().size() : 0; + + // Until a non-NO_ENTRY value has been written for a resource, + // that resource is invalid; validResources[i] represents + // the item at t->getOrderedConfigs().itemAt(i). + Vector<bool> validResources; + validResources.insertAt(false, 0, N); + + // 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 (filterable && !filter.match(cl->getEntries().keyAt(ci))) { + continue; + } + for (size_t cj=ci+1; cj<CN; cj++) { + if (filterable && !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 ui:%d touch:%d density:%d key:%d inp:%d nav:%d sz:%dx%d " + "sw%ddp w%ddp h%ddp dir:%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.uiMode, + config.touchscreen, + config.density, + config.keyboard, + config.inputFlags, + config.navigation, + config.screenWidth, + config.screenHeight, + config.smallestScreenWidthDp, + config.screenWidthDp, + config.screenHeightDp, + config.layoutDirection)); + + if (filterable && !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 ui:%d touch:%d density:%d key:%d inp:%d nav:%d sz:%dx%d " + "sw%ddp w%ddp h%ddp dir:%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.uiMode, + tHeader->config.touchscreen, + tHeader->config.density, + tHeader->config.keyboard, + tHeader->config.inputFlags, + tHeader->config.navigation, + tHeader->config.screenWidth, + tHeader->config.screenHeight, + tHeader->config.smallestScreenWidthDp, + tHeader->config.screenWidthDp, + tHeader->config.screenHeightDp, + tHeader->config.layoutDirection)); + 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; + } + validResources.editItemAt(ei) = true; + } 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); + } + + for (size_t i = 0; i < N; ++i) { + if (!validResources[i]) { + sp<ConfigList> c = t->getOrderedConfigs().itemAt(i); + fprintf(stderr, "warning: no entries written for %s/%s\n", + String8(typeName).string(), String8(c->getName()).string()); + } + } + } + + // 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::emptyBag(const SourcePos& sourcePos) +{ + status_t err = makeItABag(sourcePos); + if (err != NO_ERROR) { + return err; + } + + mBag.clear(); + 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, + const String8* configTypeName, const ConfigDescription* config) +{ + 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, + configTypeName, config)) { + 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, + configTypeName, config)) { + 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; +} + +status_t ResourceTable::Entry::remapStringValue(StringPool* strings) +{ + if (mType == TYPE_ITEM) { + Item& it = mItem; + if (it.parsedValue.dataType == Res_value::TYPE_STRING) { + it.parsedValue.data = strings->mapOriginalPosToNewPos(it.parsedValue.data); + } + } else if (mType == TYPE_BAG) { + const size_t N = mBag.size(); + for (size_t i=0; i<N; i++) { + Item& it = mBag.editValueAt(i); + if (it.parsedValue.dataType == Res_value::TYPE_STRING) { + it.parsedValue.data = strings->mapOriginalPosToNewPos(it.parsedValue.data); + } + } + } 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; +} + +void ResourceTable::Type::canAddEntry(const String16& name) +{ + mCanAddEntries.add(name); +} + +sp<ResourceTable::Entry> ResourceTable::Type::getEntry(const String16& entry, + const SourcePos& sourcePos, + const ResTable_config* config, + bool doSetIndex, + bool overlay, + bool autoAddOverlay) +{ + int pos = -1; + sp<ConfigList> c = mConfigs.valueFor(entry); + if (c == NULL) { + if (overlay && !autoAddOverlay && mCanAddEntries.indexOf(entry) < 0) { + sourcePos.error("Resource at %s appears in overlay but not" + " in the base package; use <add-resource> to add.\n", + String8(entry).string()); + return 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 sz:%dx%d " + "sw%ddp w%ddp h%ddp dir:%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, + config->smallestScreenWidthDp, + config->screenWidthDp, + config->screenHeightDp, + config->layoutDirection)); + } 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, + bool overlay, + 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, overlay, mBundle->getAutoAddOverlay()); +} + +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 000000000000..a3e066690fba --- /dev/null +++ b/tools/aapt/ResourceTable.h @@ -0,0 +1,557 @@ +// +// 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 XMLNode; +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_UTF8 = 1<<5, + + 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 compileXmlFile(const sp<AaptAssets>& assets, + const sp<AaptFile>& target, + const sp<AaptFile>& outTarget, + ResourceTable* table, + int options = XML_COMPILE_STANDARD_RESOURCE); + +status_t compileXmlFile(const sp<AaptAssets>& assets, + const sp<XMLNode>& xmlTree, + 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; + + 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; } + }; + + 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 overlay = false, + 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& package, + const String16& type, + const String16& name, + const ResTable_config& config) 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); + + void canAddEntry(const SourcePos& pos, + const String16& package, const String16& type, const String16& name); + + 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 = true) const; + + uint32_t getResId(const String16& ref, + const String16* defType = NULL, + const String16* defPackage = NULL, + const char** outErrorMsg = NULL, + bool onlyPublic = true) 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, + const String8* configTypeName = NULL, + const ConfigDescription* config = NULL); + + 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 emptyBag(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, + const String8* configTypeName, const ConfigDescription* config); + + status_t remapStringValue(StringPool* strings); + + 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; + }; + + 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); + + void canAddEntry(const String16& name); + + String16 getName() const { return mName; } + sp<Entry> getEntry(const String16& entry, + const SourcePos& pos, + const ResTable_config* config = NULL, + bool doSetIndex = false, + bool overlay = false, + bool autoAddOverlay = 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 SortedVector<String16>& getCanAddEntries() const { return mCanAddEntries; } + + 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; + SortedVector<String16> mCanAddEntries; + 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, + bool overlay, + 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; +}; + +#endif diff --git a/tools/aapt/SourcePos.cpp b/tools/aapt/SourcePos.cpp new file mode 100644 index 000000000000..e2a921c060c5 --- /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 000000000000..33f72a9c1e1c --- /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 000000000000..158b39196cb3 --- /dev/null +++ b/tools/aapt/StringPool.cpp @@ -0,0 +1,574 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#include "StringPool.h" +#include "ResourceTable.h" + +#include <utils/ByteOrder.h> +#include <utils/SortedVector.h> +#include "qsort_r_compat.h" + +#if HAVE_PRINTF_ZD +# define ZD "%zd" +# define ZD_TYPE ssize_t +#else +# define ZD "%ld" +# define ZD_TYPE long +#endif + +#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) +{ + SortedVector<const void*> uniqueStrings; + const size_t N = pool->size(); + for (size_t i=0; i<N; i++) { + size_t len; + if (pool->isUTF8()) { + uniqueStrings.add(pool->string8At(i, &len)); + } else { + uniqueStrings.add(pool->stringAt(i, &len)); + } + } + + printf("String pool of " ZD " unique %s %s strings, " ZD " entries and " + ZD " styles using " ZD " bytes:\n", + (ZD_TYPE)uniqueStrings.size(), pool->isUTF8() ? "UTF-8" : "UTF-16", + pool->isSorted() ? "sorted" : "non-sorted", + (ZD_TYPE)N, (ZD_TYPE)pool->styleCount(), (ZD_TYPE)pool->bytes()); + + const size_t NS = pool->size(); + for (size_t s=0; s<NS; s++) { + String8 str = pool->string8ObjectAt(s); + printf("String #" ZD ": %s\n", (ZD_TYPE) s, str.string()); + } +} + +String8 StringPool::entry::makeConfigsString() const { + String8 configStr(configTypeName); + if (configStr.size() > 0) configStr.append(" "); + if (configs.size() > 0) { + for (size_t j=0; j<configs.size(); j++) { + if (j > 0) configStr.append(", "); + configStr.append(configs[j].toString()); + } + } else { + configStr = "(none)"; + } + return configStr; +} + +int StringPool::entry::compare(const entry& o) const { + // Strings with styles go first, to reduce the size of the styles array. + // We don't care about the relative order of these strings. + if (hasStyles) { + return o.hasStyles ? 0 : -1; + } + if (o.hasStyles) { + return 1; + } + + // Sort unstyled strings by type, then by logical configuration. + int comp = configTypeName.compare(o.configTypeName); + if (comp != 0) { + return comp; + } + const size_t LHN = configs.size(); + const size_t RHN = o.configs.size(); + size_t i=0; + while (i < LHN && i < RHN) { + comp = configs[i].compareLogical(o.configs[i]); + if (comp != 0) { + return comp; + } + i++; + } + if (LHN < RHN) return -1; + else if (LHN > RHN) return 1; + return 0; +} + +StringPool::StringPool(bool utf8) : + mUTF8(utf8), mValues(-1) +{ +} + +ssize_t StringPool::add(const String16& value, const Vector<entry_style_span>& spans, + const String8* configTypeName, const ResTable_config* config) +{ + ssize_t res = add(value, false, configTypeName, config); + if (res >= 0) { + addStyleSpans(res, spans); + } + return res; +} + +ssize_t StringPool::add(const String16& value, + bool mergeDuplicates, const String8* configTypeName, const ResTable_config* config) +{ + 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; + } + } + + if (configTypeName != NULL) { + entry& ent = mEntries.editItemAt(eidx); + NOISY(printf("*** adding config type name %s, was %s\n", + configTypeName->string(), ent.configTypeName.string())); + if (ent.configTypeName.size() <= 0) { + ent.configTypeName = *configTypeName; + } else if (ent.configTypeName != *configTypeName) { + ent.configTypeName = " "; + } + } + + if (config != NULL) { + // Add this to the set of configs associated with the string. + entry& ent = mEntries.editItemAt(eidx); + size_t addPos; + for (addPos=0; addPos<ent.configs.size(); addPos++) { + int cmp = ent.configs.itemAt(addPos).compareLogical(*config); + if (cmp >= 0) { + if (cmp > 0) { + NOISY(printf("*** inserting config: %s\n", config->toString().string())); + ent.configs.insertAt(*config, addPos); + } + break; + } + } + if (addPos >= ent.configs.size()) { + NOISY(printf("*** adding config: %s\n", config->toString().string())); + ent.configs.add(*config); + } + } + + const bool first = vidx < 0; + const bool styled = (pos >= 0 && (size_t)pos < mEntryStyleArray.size()) ? + mEntryStyleArray[pos].spans.size() : 0; + if (first || styled || !mergeDuplicates) { + pos = mEntryArray.add(eidx); + if (first) { + vidx = mValues.add(value, pos); + } + entry& ent = mEntries.editItemAt(eidx); + ent.indices.add(pos); + } + + 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) +{ + // 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); + mEntries.editItemAt(mEntryArray[idx]).hasStyles = true; + return NO_ERROR; +} + +int StringPool::config_sort(void* state, const void* lhs, const void* rhs) +{ + StringPool* pool = (StringPool*)state; + const entry& lhe = pool->mEntries[pool->mEntryArray[*static_cast<const size_t*>(lhs)]]; + const entry& rhe = pool->mEntries[pool->mEntryArray[*static_cast<const size_t*>(rhs)]]; + return lhe.compare(rhe); +} + +void StringPool::sortByConfig() +{ + LOG_ALWAYS_FATAL_IF(mOriginalPosToNewPos.size() > 0, "Can't sort string pool after already sorted."); + + const size_t N = mEntryArray.size(); + + // This is a vector that starts out with a 1:1 mapping to entries + // in the array, which we will sort to come up with the desired order. + // At that point it maps from the new position in the array to the + // original position the entry appeared. + Vector<size_t> newPosToOriginalPos; + newPosToOriginalPos.setCapacity(N); + for (size_t i=0; i < N; i++) { + newPosToOriginalPos.add(i); + } + + // Sort the array. + NOISY(printf("SORTING STRINGS BY CONFIGURATION...\n")); + // Vector::sort uses insertion sort, which is very slow for this data set. + // Use quicksort instead because we don't need a stable sort here. + qsort_r_compat(newPosToOriginalPos.editArray(), N, sizeof(size_t), this, config_sort); + //newPosToOriginalPos.sort(config_sort, this); + NOISY(printf("DONE SORTING STRINGS BY CONFIGURATION.\n")); + + // Create the reverse mapping from the original position in the array + // to the new position where it appears in the sorted array. This is + // so that clients can re-map any positions they had previously stored. + mOriginalPosToNewPos = newPosToOriginalPos; + for (size_t i=0; i<N; i++) { + mOriginalPosToNewPos.editItemAt(newPosToOriginalPos[i]) = i; + } + +#if 0 + SortedVector<entry> entries; + + for (size_t i=0; i<N; i++) { + printf("#%d was %d: %s\n", i, newPosToOriginalPos[i], + mEntries[mEntryArray[newPosToOriginalPos[i]]].makeConfigsString().string()); + entries.add(mEntries[mEntryArray[i]]); + } + + for (size_t i=0; i<entries.size(); i++) { + printf("Sorted config #%d: %s\n", i, + entries[i].makeConfigsString().string()); + } +#endif + + // Now we rebuild the arrays. + Vector<entry> newEntries; + Vector<size_t> newEntryArray; + Vector<entry_style> newEntryStyleArray; + DefaultKeyedVector<size_t, size_t> origOffsetToNewOffset; + + for (size_t i=0; i<N; i++) { + // We are filling in new offset 'i'; oldI is where we can find it + // in the original data structure. + size_t oldI = newPosToOriginalPos[i]; + // This is the actual entry associated with the old offset. + const entry& oldEnt = mEntries[mEntryArray[oldI]]; + // This is the same entry the last time we added it to the + // new entry array, if any. + ssize_t newIndexOfOffset = origOffsetToNewOffset.indexOfKey(oldI); + size_t newOffset; + if (newIndexOfOffset < 0) { + // This is the first time we have seen the entry, so add + // it. + newOffset = newEntries.add(oldEnt); + newEntries.editItemAt(newOffset).indices.clear(); + } else { + // We have seen this entry before, use the existing one + // instead of adding it again. + newOffset = origOffsetToNewOffset.valueAt(newIndexOfOffset); + } + // Update the indices to include this new position. + newEntries.editItemAt(newOffset).indices.add(i); + // And add the offset of the entry to the new entry array. + newEntryArray.add(newOffset); + // Add any old style to the new style array. + if (mEntryStyleArray.size() > 0) { + if (oldI < mEntryStyleArray.size()) { + newEntryStyleArray.add(mEntryStyleArray[oldI]); + } else { + newEntryStyleArray.add(entry_style()); + } + } + } + + // Now trim any entries at the end of the new style array that are + // not needed. + for (ssize_t i=newEntryStyleArray.size()-1; i>=0; i--) { + const entry_style& style = newEntryStyleArray[i]; + if (style.spans.size() > 0) { + // That's it. + break; + } + // This one is not needed; remove. + newEntryStyleArray.removeAt(i); + } + + // All done, install the new data structures and upate mValues with + // the new positions. + mEntries = newEntries; + mEntryArray = newEntryArray; + mEntryStyleArray = newEntryStyleArray; + mValues.clear(); + for (size_t i=0; i<mEntries.size(); i++) { + const entry& ent = mEntries[i]; + mValues.add(ent.value, ent.indices[0]); + } + +#if 0 + printf("FINAL SORTED STRING CONFIGS:\n"); + for (size_t i=0; i<mEntries.size(); i++) { + const entry& ent = mEntries[i]; + printf("#" ZD " %s: %s\n", (ZD_TYPE)i, ent.makeConfigsString().string(), + String8(ent.value).string()); + } +#endif +} + +sp<AaptFile> StringPool::createStringBlock() +{ + sp<AaptFile> pool = new AaptFile(String8(), AaptGroupEntry(), + String8()); + status_t err = writeStringBlock(pool); + return err == NO_ERROR ? pool : NULL; +} + +#define ENCODE_LENGTH(str, chrsz, strSize) \ +{ \ + size_t maxMask = 1 << ((chrsz*8)-1); \ + size_t maxSize = maxMask-1; \ + if (strSize > maxSize) { \ + *str++ = maxMask | ((strSize>>(chrsz*8))&maxSize); \ + } \ + *str++ = strSize; \ +} + +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 = mEntryArray.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; + } + + const size_t charSize = mUTF8 ? sizeof(uint8_t) : sizeof(char16_t); + + 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 > (size_t)(1<<((charSize*8)-1))-1 ? + charSize*2 : charSize; + + String8 encStr; + if (mUTF8) { + encStr = String8(ent.value); + } + + const size_t encSize = mUTF8 ? encStr.size() : 0; + const size_t encLenSize = mUTF8 ? + (encSize > (size_t)(1<<((charSize*8)-1))-1 ? + charSize*2 : charSize) : 0; + + ent.offset = strPos; + + const size_t totalSize = lenSize + encLenSize + + ((mUTF8 ? encSize : strSize)+1)*charSize; + + void* dat = (void*)pool->editData(preSize + strPos + totalSize); + if (dat == NULL) { + fprintf(stderr, "ERROR: Out of memory for string pool\n"); + return NO_MEMORY; + } + dat = (uint8_t*)dat + preSize + strPos; + if (mUTF8) { + uint8_t* strings = (uint8_t*)dat; + + ENCODE_LENGTH(strings, sizeof(uint8_t), strSize) + + ENCODE_LENGTH(strings, sizeof(uint8_t), encSize) + + strncpy((char*)strings, encStr, encSize+1); + } else { + uint16_t* strings = (uint16_t*)dat; + + ENCODE_LENGTH(strings, sizeof(uint16_t), strSize) + + strcpy16_htod(strings, ent.value); + } + + strPos += totalSize; + } + + // 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 (mUTF8) { + header->flags |= htodl(ResStringPool_header::UTF8_FLAG); + } + header->stringsStart = htodl(preSize); + header->stylesStart = htodl(STYLES > 0 ? (preSize+strPos) : 0); + + // Write string index array. + + uint32_t* index = (uint32_t*)(header+1); + 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. + + 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 000000000000..1b3abfd9b4c6 --- /dev/null +++ b/tools/aapt/StringPool.h @@ -0,0 +1,183 @@ +// +// 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 <androidfw/ResourceTypes.h> +#include <utils/String16.h> +#include <utils/TypeHelpers.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <ctype.h> +#include <errno.h> + +#include <libexpat/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), hasStyles(false) { } + entry(const entry& o) : value(o.value), offset(o.offset), + hasStyles(o.hasStyles), indices(o.indices), + configTypeName(o.configTypeName), configs(o.configs) { } + + String16 value; + size_t offset; + bool hasStyles; + Vector<size_t> indices; + String8 configTypeName; + Vector<ResTable_config> configs; + + String8 makeConfigsString() const; + + int compare(const entry& o) const; + + inline bool operator<(const entry& o) const { return compare(o) < 0; } + inline bool operator<=(const entry& o) const { return compare(o) <= 0; } + inline bool operator==(const entry& o) const { return compare(o) == 0; } + inline bool operator!=(const entry& o) const { return compare(o) != 0; } + inline bool operator>=(const entry& o) const { return compare(o) >= 0; } + inline bool operator>(const entry& o) const { return compare(o) > 0; } + }; + + 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 'utf8' is true, strings will be encoded with UTF-8 instead of + * left in Java's native UTF-16. + */ + explicit StringPool(bool utf8 = 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. + */ + ssize_t add(const String16& value, bool mergeDuplicates = false, + const String8* configTypeName = NULL, const ResTable_config* config = NULL); + + ssize_t add(const String16& value, const Vector<entry_style_span>& spans, + const String8* configTypeName = NULL, const ResTable_config* config = NULL); + + 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); + + // Sort the contents of the string block by the configuration associated + // with each item. After doing this you can use mapOriginalPosToNewPos() + // to find out the new position given the position originally returned by + // add(). + void sortByConfig(); + + // For use after sortByConfig() to map from the original position of + // a string to its new sorted position. + size_t mapOriginalPosToNewPos(size_t originalPos) const { + return mOriginalPosToNewPos.itemAt(originalPos); + } + + 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: + static int config_sort(void* state, const void* lhs, const void* rhs); + + const bool mUTF8; + + // The following data structures represent the actual structures + // that will be generated for the final string pool. + + // Raw array of unique strings, in some arbitrary order. This is the + // actual strings that appear in the final string pool, in the order + // that they will be written. + 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). + // This is the lookup array that will be written for finding + // the string for each offset/position in the string pool. + Vector<size_t> mEntryArray; + // Optional style span information associated with each index of + // mEntryArray. + Vector<entry_style> mEntryStyleArray; + + // The following data structures are used for book-keeping as the + // string pool is constructed. + + // 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; + // This array maps from the original position a string was placed at + // in mEntryArray to its new position after being sorted with sortByConfig(). + Vector<size_t> mOriginalPosToNewPos; +}; + +// The entry types are trivially movable because all fields they contain, including +// the vectors and strings, are trivially movable. +namespace android { + ANDROID_TRIVIAL_MOVE_TRAIT(StringPool::entry); + ANDROID_TRIVIAL_MOVE_TRAIT(StringPool::entry_style_span); + ANDROID_TRIVIAL_MOVE_TRAIT(StringPool::entry_style); +}; + +#endif + diff --git a/tools/aapt/WorkQueue.cpp b/tools/aapt/WorkQueue.cpp new file mode 100644 index 000000000000..24a962fcfdb6 --- /dev/null +++ b/tools/aapt/WorkQueue.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2012 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. + */ + +// #define LOG_NDEBUG 0 +#define LOG_TAG "WorkQueue" + +#include <utils/Log.h> +#include "WorkQueue.h" + +namespace android { + +// --- WorkQueue --- + +WorkQueue::WorkQueue(size_t maxThreads, bool canCallJava) : + mMaxThreads(maxThreads), mCanCallJava(canCallJava), + mCanceled(false), mFinished(false), mIdleThreads(0) { +} + +WorkQueue::~WorkQueue() { + if (!cancel()) { + finish(); + } +} + +status_t WorkQueue::schedule(WorkUnit* workUnit, size_t backlog) { + AutoMutex _l(mLock); + + if (mFinished || mCanceled) { + return INVALID_OPERATION; + } + + if (mWorkThreads.size() < mMaxThreads + && mIdleThreads < mWorkUnits.size() + 1) { + sp<WorkThread> workThread = new WorkThread(this, mCanCallJava); + status_t status = workThread->run("WorkQueue::WorkThread"); + if (status) { + return status; + } + mWorkThreads.add(workThread); + mIdleThreads += 1; + } else if (backlog) { + while (mWorkUnits.size() >= mMaxThreads * backlog) { + mWorkDequeuedCondition.wait(mLock); + if (mFinished || mCanceled) { + return INVALID_OPERATION; + } + } + } + + mWorkUnits.add(workUnit); + mWorkChangedCondition.broadcast(); + return OK; +} + +status_t WorkQueue::cancel() { + AutoMutex _l(mLock); + + return cancelLocked(); +} + +status_t WorkQueue::cancelLocked() { + if (mFinished) { + return INVALID_OPERATION; + } + + if (!mCanceled) { + mCanceled = true; + + size_t count = mWorkUnits.size(); + for (size_t i = 0; i < count; i++) { + delete mWorkUnits.itemAt(i); + } + mWorkUnits.clear(); + mWorkChangedCondition.broadcast(); + mWorkDequeuedCondition.broadcast(); + } + return OK; +} + +status_t WorkQueue::finish() { + { // acquire lock + AutoMutex _l(mLock); + + if (mFinished) { + return INVALID_OPERATION; + } + + mFinished = true; + mWorkChangedCondition.broadcast(); + } // release lock + + // It is not possible for the list of work threads to change once the mFinished + // flag has been set, so we can access mWorkThreads outside of the lock here. + size_t count = mWorkThreads.size(); + for (size_t i = 0; i < count; i++) { + mWorkThreads.itemAt(i)->join(); + } + mWorkThreads.clear(); + return OK; +} + +bool WorkQueue::threadLoop() { + WorkUnit* workUnit; + { // acquire lock + AutoMutex _l(mLock); + + for (;;) { + if (mCanceled) { + return false; + } + + if (!mWorkUnits.isEmpty()) { + workUnit = mWorkUnits.itemAt(0); + mWorkUnits.removeAt(0); + mIdleThreads -= 1; + mWorkDequeuedCondition.broadcast(); + break; + } + + if (mFinished) { + return false; + } + + mWorkChangedCondition.wait(mLock); + } + } // release lock + + bool shouldContinue = workUnit->run(); + delete workUnit; + + { // acquire lock + AutoMutex _l(mLock); + + mIdleThreads += 1; + + if (!shouldContinue) { + cancelLocked(); + return false; + } + } // release lock + + return true; +} + +// --- WorkQueue::WorkThread --- + +WorkQueue::WorkThread::WorkThread(WorkQueue* workQueue, bool canCallJava) : + Thread(canCallJava), mWorkQueue(workQueue) { +} + +WorkQueue::WorkThread::~WorkThread() { +} + +bool WorkQueue::WorkThread::threadLoop() { + return mWorkQueue->threadLoop(); +} + +}; // namespace android diff --git a/tools/aapt/WorkQueue.h b/tools/aapt/WorkQueue.h new file mode 100644 index 000000000000..d38f05d034e9 --- /dev/null +++ b/tools/aapt/WorkQueue.h @@ -0,0 +1,119 @@ +/*] + * Copyright (C) 2012 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. + */ + +#ifndef AAPT_WORK_QUEUE_H +#define AAPT_WORK_QUEUE_H + +#include <utils/Errors.h> +#include <utils/Vector.h> +#include <utils/threads.h> + +namespace android { + +/* + * A threaded work queue. + * + * This class is designed to make it easy to run a bunch of isolated work + * units in parallel, using up to the specified number of threads. + * To use it, write a loop to post work units to the work queue, then synchronize + * on the queue at the end. + */ +class WorkQueue { +public: + class WorkUnit { + public: + WorkUnit() { } + virtual ~WorkUnit() { } + + /* + * Runs the work unit. + * If the result is 'true' then the work queue continues scheduling work as usual. + * If the result is 'false' then the work queue is canceled. + */ + virtual bool run() = 0; + }; + + /* Creates a work queue with the specified maximum number of work threads. */ + WorkQueue(size_t maxThreads, bool canCallJava = true); + + /* Destroys the work queue. + * Cancels pending work and waits for all remaining threads to complete. + */ + ~WorkQueue(); + + /* Posts a work unit to run later. + * If the work queue has been canceled or is already finished, returns INVALID_OPERATION + * and does not take ownership of the work unit (caller must destroy it itself). + * Otherwise, returns OK and takes ownership of the work unit (the work queue will + * destroy it automatically). + * + * For flow control, this method blocks when the size of the pending work queue is more + * 'backlog' times the number of threads. This condition reduces the rate of entry into + * the pending work queue and prevents it from growing much more rapidly than the + * work threads can actually handle. + * + * If 'backlog' is 0, then no throttle is applied. + */ + status_t schedule(WorkUnit* workUnit, size_t backlog = 2); + + /* Cancels all pending work. + * If the work queue is already finished, returns INVALID_OPERATION. + * If the work queue is already canceled, returns OK and does nothing else. + * Otherwise, returns OK, discards all pending work units and prevents additional + * work units from being scheduled. + * + * Call finish() after cancel() to wait for all remaining work to complete. + */ + status_t cancel(); + + /* Waits for all work to complete. + * If the work queue is already finished, returns INVALID_OPERATION. + * Otherwise, waits for all work to complete and returns OK. + */ + status_t finish(); + +private: + class WorkThread : public Thread { + public: + WorkThread(WorkQueue* workQueue, bool canCallJava); + virtual ~WorkThread(); + + private: + virtual bool threadLoop(); + + WorkQueue* const mWorkQueue; + }; + + status_t cancelLocked(); + bool threadLoop(); // called from each work thread + + const size_t mMaxThreads; + const bool mCanCallJava; + + Mutex mLock; + Condition mWorkChangedCondition; + Condition mWorkDequeuedCondition; + + bool mCanceled; + bool mFinished; + size_t mIdleThreads; + Vector<sp<WorkThread> > mWorkThreads; + Vector<WorkUnit*> mWorkUnits; +}; + +}; // namespace android + +#endif // AAPT_WORK_QUEUE_H diff --git a/tools/aapt/XMLNode.cpp b/tools/aapt/XMLNode.cpp new file mode 100644 index 000000000000..a663ad58fc55 --- /dev/null +++ b/tools/aapt/XMLNode.cpp @@ -0,0 +1,1510 @@ +// +// Copyright 2006 The Android Open Source Project +// +// Build resource files from raw assets. +// + +#include "XMLNode.h" +#include "ResourceTable.h" +#include "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_AUTO_PACKAGE_NAMESPACE = "http://schemas.android.com/apk/res-auto"; +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_PREFIX_AUTO_PACKAGE(RESOURCES_AUTO_PACKAGE_NAMESPACE); +static const String16 RESOURCES_PRV_PREFIX(RESOURCES_ROOT_PRV_NAMESPACE); +static const String16 RESOURCES_TOOLS_NAMESPACE("http://schemas.android.com/tools"); + +String16 getNamespaceResourcePackage(String16 appPackage, 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_AUTO_PACKAGE)) { + NOISY(printf("Using default application package: %s -> %s\n", String8(namespaceUri).string(), String8(appPackage).string())); + isPublic = true; + return appPackage; + } else 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 hasSubstitutionErrors(const char* fileName, + ResXMLTree* inXml, + String16 str16) +{ + const char16_t* str = str16.string(); + const char16_t* p = str; + const char16_t* end = str + str16.size(); + + bool nonpositional = false; + int argCount = 0; + + while (p < end) { + /* + * Look for the start of a Java-style substitution sequence. + */ + if (*p == '%' && p + 1 < end) { + p++; + + // A literal percent sign represented by %% + if (*p == '%') { + p++; + continue; + } + + argCount++; + + if (*p >= '0' && *p <= '9') { + do { + p++; + } while (*p >= '0' && *p <= '9'); + if (*p != '$') { + // This must be a size specification instead of position. + nonpositional = true; + } + } else if (*p == '<') { + // Reusing last argument; bad idea since it can be re-arranged. + nonpositional = true; + p++; + + // Optionally '$' can be specified at the end. + if (p < end && *p == '$') { + p++; + } + } else { + nonpositional = true; + } + + // Ignore flags and widths + while (p < end && (*p == '-' || + *p == '#' || + *p == '+' || + *p == ' ' || + *p == ',' || + *p == '(' || + (*p >= '0' && *p <= '9'))) { + p++; + } + + /* + * This is a shortcut to detect strings that are going to Time.format() + * instead of String.format() + * + * Comparison of String.format() and Time.format() args: + * + * String: ABC E GH ST X abcdefgh nost x + * Time: DEFGHKMS W Za d hkm s w yz + * + * Therefore we know it's definitely Time if we have: + * DFKMWZkmwyz + */ + if (p < end) { + switch (*p) { + case 'D': + case 'F': + case 'K': + case 'M': + case 'W': + case 'Z': + case 'k': + case 'm': + case 'w': + case 'y': + case 'z': + return NO_ERROR; + } + } + } + + p++; + } + + /* + * If we have more than one substitution in this string and any of them + * are not in positional form, give the user an error. + */ + if (argCount > 1 && nonpositional) { + SourcePos(String8(fileName), inXml->getLineNumber()).error( + "Multiple substitutions specified in non-positional format; " + "did you mean to add the formatted=\"false\" attribute?\n"); + return NOT_ENOUGH_DATA; + } + + return NO_ERROR; +} + +status_t parseStyledString(Bundle* bundle, + const char* fileName, + ResXMLTree* inXml, + const String16& endTag, + String16* outString, + Vector<StringPool::entry_style_span>* outSpans, + bool isFormatted, + 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 { + if (isFormatted && hasSubstitutionErrors(fileName, inXml, text) != NO_ERROR) { + return UNKNOWN_ERROR; + } 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(); + + /* + * This warning seems to be just an irritation to most people, + * since it is typically introduced by translators who then never + * see the warning. + */ + if (0 && 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\"", + ResTable::normalizeForOutput(String8(block->getAttributeStringValue(i, + &len)).string()).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\")", ResTable::normalizeForOutput(String8(val).string()). + 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(), + ResTable::normalizeForOutput(String8(block->getText(&len)).string()).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) + , mUTF8(false) +{ + if (isNamespace) { + mNamespacePrefix = s1; + mNamespaceUri = s2; + } else { + mNamespaceUri = s1; + mElementName = s2; + } +} + +XMLNode::XMLNode(const String8& filename) + : mFilename(filename) +{ + memset(&mCharsValue, 0, sizeof(mCharsValue)); +} + +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 String8& XMLNode::getFilename() const +{ + return mFilename; +} + +const Vector<XMLNode::attribute_entry>& + XMLNode::getAttributes() const +{ + return mAttributes; +} + +const XMLNode::attribute_entry* XMLNode::getAttribute(const String16& ns, + const String16& name) const +{ + for (size_t i=0; i<mAttributes.size(); i++) { + const attribute_entry& ae(mAttributes.itemAt(i)); + if (ae.ns == ns && ae.name == name) { + return &ae; + } + } + + return NULL; +} + +XMLNode::attribute_entry* XMLNode::editAttribute(const String16& ns, + const String16& name) +{ + for (size_t i=0; i<mAttributes.size(); i++) { + attribute_entry * ae = &mAttributes.editItemAt(i); + if (ae->ns == ns && ae->name == name) { + return ae; + } + } + + return NULL; +} + +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; +} + +sp<XMLNode> XMLNode::searchElement(const String16& tagNamespace, const String16& tagName) +{ + if (getType() == XMLNode::TYPE_ELEMENT + && mNamespaceUri == tagNamespace + && mElementName == tagName) { + return this; + } + + for (size_t i=0; i<mChildren.size(); i++) { + sp<XMLNode> found = mChildren.itemAt(i)->searchElement(tagNamespace, tagName); + if (found != NULL) { + return found; + } + } + + return NULL; +} + +sp<XMLNode> XMLNode::getChildElement(const String16& tagNamespace, const String16& tagName) +{ + for (size_t i=0; i<mChildren.size(); i++) { + sp<XMLNode> child = mChildren.itemAt(i); + if (child->getType() == XMLNode::TYPE_ELEMENT + && child->mNamespaceUri == tagNamespace + && child->mElementName == tagName) { + return child; + } + } + + return NULL; +} + +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::insertChildAt(const sp<XMLNode>& child, size_t index) +{ + 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.insertAt(child, index); + 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; + } + + if (ns != RESOURCES_TOOLS_NAMESPACE) { + 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(String16(assets->getPackage()), 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(mUTF8); + 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 (RESOURCES_TOOLS_NAMESPACE != mNamespaceUri) { + 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; + bool writeCurrentNode = true; + + 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) { + if (mNamespaceUri == RESOURCES_TOOLS_NAMESPACE) { + writeCurrentNode = false; + } else { + 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)); + + if (writeCurrentNode) { + 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) { + if (writeCurrentNode) { + 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 000000000000..05624b745176 --- /dev/null +++ b/tools/aapt/XMLNode.h @@ -0,0 +1,202 @@ +// +// 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 isFormatted, + 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; + + const String8& getFilename() 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 attribute_entry* getAttribute(const String16& ns, const String16& name) const; + + attribute_entry* editAttribute(const String16& ns, const String16& name); + + const String16& getCData() const; + + const String16& getComment() const; + + int32_t getStartLineNumber() const; + int32_t getEndLineNumber() const; + + sp<XMLNode> searchElement(const String16& tagNamespace, const String16& tagName); + + sp<XMLNode> getChildElement(const String16& tagNamespace, const String16& tagName); + + status_t addChild(const sp<XMLNode>& child); + + status_t insertChildAt(const sp<XMLNode>& child, size_t index); + + 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); + + void setUTF8(bool val) { mUTF8 = val; } + + 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; + + // Encode compiled XML with UTF-8 StringPools? + bool mUTF8; +}; + +#endif diff --git a/tools/aapt/ZipEntry.cpp b/tools/aapt/ZipEntry.cpp new file mode 100644 index 000000000000..b575988f0277 --- /dev/null +++ b/tools/aapt/ZipEntry.cpp @@ -0,0 +1,696 @@ +/* + * 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. + */ + +// +// Access to entries in a Zip archive. +// + +#define LOG_TAG "zip" + +#include "ZipEntry.h" +#include <utils/Log.h> + +#include <stdio.h> +#include <string.h> +#include <assert.h> + +using namespace android; + +/* + * Initialize a new ZipEntry structure from a FILE* positioned at a + * CentralDirectoryEntry. + * + * On exit, the file pointer will be at the start of the next CDE or + * at the EOCD. + */ +status_t ZipEntry::initFromCDE(FILE* fp) +{ + status_t result; + long posn; + bool hasDD; + + //ALOGV("initFromCDE ---\n"); + + /* read the CDE */ + result = mCDE.read(fp); + if (result != NO_ERROR) { + ALOGD("mCDE.read failed\n"); + return result; + } + + //mCDE.dump(); + + /* using the info in the CDE, go load up the LFH */ + posn = ftell(fp); + if (fseek(fp, mCDE.mLocalHeaderRelOffset, SEEK_SET) != 0) { + ALOGD("local header seek failed (%ld)\n", + mCDE.mLocalHeaderRelOffset); + return UNKNOWN_ERROR; + } + + result = mLFH.read(fp); + if (result != NO_ERROR) { + ALOGD("mLFH.read failed\n"); + return result; + } + + if (fseek(fp, posn, SEEK_SET) != 0) + return UNKNOWN_ERROR; + + //mLFH.dump(); + + /* + * We *might* need to read the Data Descriptor at this point and + * integrate it into the LFH. If this bit is set, the CRC-32, + * compressed size, and uncompressed size will be zero. In practice + * these seem to be rare. + */ + hasDD = (mLFH.mGPBitFlag & kUsesDataDescr) != 0; + if (hasDD) { + // do something clever + //ALOGD("+++ has data descriptor\n"); + } + + /* + * Sanity-check the LFH. Note that this will fail if the "kUsesDataDescr" + * flag is set, because the LFH is incomplete. (Not a problem, since we + * prefer the CDE values.) + */ + if (!hasDD && !compareHeaders()) { + ALOGW("warning: header mismatch\n"); + // keep going? + } + + /* + * If the mVersionToExtract is greater than 20, we may have an + * issue unpacking the record -- could be encrypted, compressed + * with something we don't support, or use Zip64 extensions. We + * can defer worrying about that to when we're extracting data. + */ + + return NO_ERROR; +} + +/* + * Initialize a new entry. Pass in the file name and an optional comment. + * + * Initializes the CDE and the LFH. + */ +void ZipEntry::initNew(const char* fileName, const char* comment) +{ + assert(fileName != NULL && *fileName != '\0'); // name required + + /* most fields are properly initialized by constructor */ + mCDE.mVersionMadeBy = kDefaultMadeBy; + mCDE.mVersionToExtract = kDefaultVersion; + mCDE.mCompressionMethod = kCompressStored; + mCDE.mFileNameLength = strlen(fileName); + if (comment != NULL) + mCDE.mFileCommentLength = strlen(comment); + mCDE.mExternalAttrs = 0x81b60020; // matches what WinZip does + + if (mCDE.mFileNameLength > 0) { + mCDE.mFileName = new unsigned char[mCDE.mFileNameLength+1]; + strcpy((char*) mCDE.mFileName, fileName); + } + if (mCDE.mFileCommentLength > 0) { + /* TODO: stop assuming null-terminated ASCII here? */ + mCDE.mFileComment = new unsigned char[mCDE.mFileCommentLength+1]; + strcpy((char*) mCDE.mFileComment, comment); + } + + copyCDEtoLFH(); +} + +/* + * Initialize a new entry, starting with the ZipEntry from a different + * archive. + * + * Initializes the CDE and the LFH. + */ +status_t ZipEntry::initFromExternal(const ZipFile* pZipFile, + const ZipEntry* pEntry) +{ + /* + * Copy everything in the CDE over, then fix up the hairy bits. + */ + memcpy(&mCDE, &pEntry->mCDE, sizeof(mCDE)); + + if (mCDE.mFileNameLength > 0) { + mCDE.mFileName = new unsigned char[mCDE.mFileNameLength+1]; + if (mCDE.mFileName == NULL) + return NO_MEMORY; + strcpy((char*) mCDE.mFileName, (char*)pEntry->mCDE.mFileName); + } + if (mCDE.mFileCommentLength > 0) { + mCDE.mFileComment = new unsigned char[mCDE.mFileCommentLength+1]; + if (mCDE.mFileComment == NULL) + return NO_MEMORY; + strcpy((char*) mCDE.mFileComment, (char*)pEntry->mCDE.mFileComment); + } + if (mCDE.mExtraFieldLength > 0) { + /* we null-terminate this, though it may not be a string */ + mCDE.mExtraField = new unsigned char[mCDE.mExtraFieldLength+1]; + if (mCDE.mExtraField == NULL) + return NO_MEMORY; + memcpy(mCDE.mExtraField, pEntry->mCDE.mExtraField, + mCDE.mExtraFieldLength+1); + } + + /* construct the LFH from the CDE */ + copyCDEtoLFH(); + + /* + * The LFH "extra" field is independent of the CDE "extra", so we + * handle it here. + */ + assert(mLFH.mExtraField == NULL); + mLFH.mExtraFieldLength = pEntry->mLFH.mExtraFieldLength; + if (mLFH.mExtraFieldLength > 0) { + mLFH.mExtraField = new unsigned char[mLFH.mExtraFieldLength+1]; + if (mLFH.mExtraField == NULL) + return NO_MEMORY; + memcpy(mLFH.mExtraField, pEntry->mLFH.mExtraField, + mLFH.mExtraFieldLength+1); + } + + return NO_ERROR; +} + +/* + * Insert pad bytes in the LFH by tweaking the "extra" field. This will + * potentially confuse something that put "extra" data in here earlier, + * but I can't find an actual problem. + */ +status_t ZipEntry::addPadding(int padding) +{ + if (padding <= 0) + return INVALID_OPERATION; + + //ALOGI("HEY: adding %d pad bytes to existing %d in %s\n", + // padding, mLFH.mExtraFieldLength, mCDE.mFileName); + + if (mLFH.mExtraFieldLength > 0) { + /* extend existing field */ + unsigned char* newExtra; + + newExtra = new unsigned char[mLFH.mExtraFieldLength + padding]; + if (newExtra == NULL) + return NO_MEMORY; + memset(newExtra + mLFH.mExtraFieldLength, 0, padding); + memcpy(newExtra, mLFH.mExtraField, mLFH.mExtraFieldLength); + + delete[] mLFH.mExtraField; + mLFH.mExtraField = newExtra; + mLFH.mExtraFieldLength += padding; + } else { + /* create new field */ + mLFH.mExtraField = new unsigned char[padding]; + memset(mLFH.mExtraField, 0, padding); + mLFH.mExtraFieldLength = padding; + } + + return NO_ERROR; +} + +/* + * Set the fields in the LFH equal to the corresponding fields in the CDE. + * + * This does not touch the LFH "extra" field. + */ +void ZipEntry::copyCDEtoLFH(void) +{ + mLFH.mVersionToExtract = mCDE.mVersionToExtract; + mLFH.mGPBitFlag = mCDE.mGPBitFlag; + mLFH.mCompressionMethod = mCDE.mCompressionMethod; + mLFH.mLastModFileTime = mCDE.mLastModFileTime; + mLFH.mLastModFileDate = mCDE.mLastModFileDate; + mLFH.mCRC32 = mCDE.mCRC32; + mLFH.mCompressedSize = mCDE.mCompressedSize; + mLFH.mUncompressedSize = mCDE.mUncompressedSize; + mLFH.mFileNameLength = mCDE.mFileNameLength; + // the "extra field" is independent + + delete[] mLFH.mFileName; + if (mLFH.mFileNameLength > 0) { + mLFH.mFileName = new unsigned char[mLFH.mFileNameLength+1]; + strcpy((char*) mLFH.mFileName, (const char*) mCDE.mFileName); + } else { + mLFH.mFileName = NULL; + } +} + +/* + * Set some information about a file after we add it. + */ +void ZipEntry::setDataInfo(long uncompLen, long compLen, unsigned long crc32, + int compressionMethod) +{ + mCDE.mCompressionMethod = compressionMethod; + mCDE.mCRC32 = crc32; + mCDE.mCompressedSize = compLen; + mCDE.mUncompressedSize = uncompLen; + mCDE.mCompressionMethod = compressionMethod; + if (compressionMethod == kCompressDeflated) { + mCDE.mGPBitFlag |= 0x0002; // indicates maximum compression used + } + copyCDEtoLFH(); +} + +/* + * See if the data in mCDE and mLFH match up. This is mostly useful for + * debugging these classes, but it can be used to identify damaged + * archives. + * + * Returns "false" if they differ. + */ +bool ZipEntry::compareHeaders(void) const +{ + if (mCDE.mVersionToExtract != mLFH.mVersionToExtract) { + ALOGV("cmp: VersionToExtract\n"); + return false; + } + if (mCDE.mGPBitFlag != mLFH.mGPBitFlag) { + ALOGV("cmp: GPBitFlag\n"); + return false; + } + if (mCDE.mCompressionMethod != mLFH.mCompressionMethod) { + ALOGV("cmp: CompressionMethod\n"); + return false; + } + if (mCDE.mLastModFileTime != mLFH.mLastModFileTime) { + ALOGV("cmp: LastModFileTime\n"); + return false; + } + if (mCDE.mLastModFileDate != mLFH.mLastModFileDate) { + ALOGV("cmp: LastModFileDate\n"); + return false; + } + if (mCDE.mCRC32 != mLFH.mCRC32) { + ALOGV("cmp: CRC32\n"); + return false; + } + if (mCDE.mCompressedSize != mLFH.mCompressedSize) { + ALOGV("cmp: CompressedSize\n"); + return false; + } + if (mCDE.mUncompressedSize != mLFH.mUncompressedSize) { + ALOGV("cmp: UncompressedSize\n"); + return false; + } + if (mCDE.mFileNameLength != mLFH.mFileNameLength) { + ALOGV("cmp: FileNameLength\n"); + return false; + } +#if 0 // this seems to be used for padding, not real data + if (mCDE.mExtraFieldLength != mLFH.mExtraFieldLength) { + ALOGV("cmp: ExtraFieldLength\n"); + return false; + } +#endif + if (mCDE.mFileName != NULL) { + if (strcmp((char*) mCDE.mFileName, (char*) mLFH.mFileName) != 0) { + ALOGV("cmp: FileName\n"); + return false; + } + } + + return true; +} + + +/* + * Convert the DOS date/time stamp into a UNIX time stamp. + */ +time_t ZipEntry::getModWhen(void) const +{ + struct tm parts; + + parts.tm_sec = (mCDE.mLastModFileTime & 0x001f) << 1; + parts.tm_min = (mCDE.mLastModFileTime & 0x07e0) >> 5; + parts.tm_hour = (mCDE.mLastModFileTime & 0xf800) >> 11; + parts.tm_mday = (mCDE.mLastModFileDate & 0x001f); + parts.tm_mon = ((mCDE.mLastModFileDate & 0x01e0) >> 5) -1; + parts.tm_year = ((mCDE.mLastModFileDate & 0xfe00) >> 9) + 80; + parts.tm_wday = parts.tm_yday = 0; + parts.tm_isdst = -1; // DST info "not available" + + return mktime(&parts); +} + +/* + * Set the CDE/LFH timestamp from UNIX time. + */ +void ZipEntry::setModWhen(time_t when) +{ +#ifdef HAVE_LOCALTIME_R + struct tm tmResult; +#endif + time_t even; + unsigned short zdate, ztime; + + struct tm* ptm; + + /* round up to an even number of seconds */ + even = (time_t)(((unsigned long)(when) + 1) & (~1)); + + /* expand */ +#ifdef HAVE_LOCALTIME_R + ptm = localtime_r(&even, &tmResult); +#else + ptm = localtime(&even); +#endif + + int year; + year = ptm->tm_year; + if (year < 80) + year = 80; + + zdate = (year - 80) << 9 | (ptm->tm_mon+1) << 5 | ptm->tm_mday; + ztime = ptm->tm_hour << 11 | ptm->tm_min << 5 | ptm->tm_sec >> 1; + + mCDE.mLastModFileTime = mLFH.mLastModFileTime = ztime; + mCDE.mLastModFileDate = mLFH.mLastModFileDate = zdate; +} + + +/* + * =========================================================================== + * ZipEntry::LocalFileHeader + * =========================================================================== + */ + +/* + * Read a local file header. + * + * On entry, "fp" points to the signature at the start of the header. + * On exit, "fp" points to the start of data. + */ +status_t ZipEntry::LocalFileHeader::read(FILE* fp) +{ + status_t result = NO_ERROR; + unsigned char buf[kLFHLen]; + + assert(mFileName == NULL); + assert(mExtraField == NULL); + + if (fread(buf, 1, kLFHLen, fp) != kLFHLen) { + result = UNKNOWN_ERROR; + goto bail; + } + + if (ZipEntry::getLongLE(&buf[0x00]) != kSignature) { + ALOGD("whoops: didn't find expected signature\n"); + result = UNKNOWN_ERROR; + goto bail; + } + + mVersionToExtract = ZipEntry::getShortLE(&buf[0x04]); + mGPBitFlag = ZipEntry::getShortLE(&buf[0x06]); + mCompressionMethod = ZipEntry::getShortLE(&buf[0x08]); + mLastModFileTime = ZipEntry::getShortLE(&buf[0x0a]); + mLastModFileDate = ZipEntry::getShortLE(&buf[0x0c]); + mCRC32 = ZipEntry::getLongLE(&buf[0x0e]); + mCompressedSize = ZipEntry::getLongLE(&buf[0x12]); + mUncompressedSize = ZipEntry::getLongLE(&buf[0x16]); + mFileNameLength = ZipEntry::getShortLE(&buf[0x1a]); + mExtraFieldLength = ZipEntry::getShortLE(&buf[0x1c]); + + // TODO: validate sizes + + /* grab filename */ + if (mFileNameLength != 0) { + mFileName = new unsigned char[mFileNameLength+1]; + if (mFileName == NULL) { + result = NO_MEMORY; + goto bail; + } + if (fread(mFileName, 1, mFileNameLength, fp) != mFileNameLength) { + result = UNKNOWN_ERROR; + goto bail; + } + mFileName[mFileNameLength] = '\0'; + } + + /* grab extra field */ + if (mExtraFieldLength != 0) { + mExtraField = new unsigned char[mExtraFieldLength+1]; + if (mExtraField == NULL) { + result = NO_MEMORY; + goto bail; + } + if (fread(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) { + result = UNKNOWN_ERROR; + goto bail; + } + mExtraField[mExtraFieldLength] = '\0'; + } + +bail: + return result; +} + +/* + * Write a local file header. + */ +status_t ZipEntry::LocalFileHeader::write(FILE* fp) +{ + unsigned char buf[kLFHLen]; + + ZipEntry::putLongLE(&buf[0x00], kSignature); + ZipEntry::putShortLE(&buf[0x04], mVersionToExtract); + ZipEntry::putShortLE(&buf[0x06], mGPBitFlag); + ZipEntry::putShortLE(&buf[0x08], mCompressionMethod); + ZipEntry::putShortLE(&buf[0x0a], mLastModFileTime); + ZipEntry::putShortLE(&buf[0x0c], mLastModFileDate); + ZipEntry::putLongLE(&buf[0x0e], mCRC32); + ZipEntry::putLongLE(&buf[0x12], mCompressedSize); + ZipEntry::putLongLE(&buf[0x16], mUncompressedSize); + ZipEntry::putShortLE(&buf[0x1a], mFileNameLength); + ZipEntry::putShortLE(&buf[0x1c], mExtraFieldLength); + + if (fwrite(buf, 1, kLFHLen, fp) != kLFHLen) + return UNKNOWN_ERROR; + + /* write filename */ + if (mFileNameLength != 0) { + if (fwrite(mFileName, 1, mFileNameLength, fp) != mFileNameLength) + return UNKNOWN_ERROR; + } + + /* write "extra field" */ + if (mExtraFieldLength != 0) { + if (fwrite(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) + return UNKNOWN_ERROR; + } + + return NO_ERROR; +} + + +/* + * Dump the contents of a LocalFileHeader object. + */ +void ZipEntry::LocalFileHeader::dump(void) const +{ + ALOGD(" LocalFileHeader contents:\n"); + ALOGD(" versToExt=%u gpBits=0x%04x compression=%u\n", + mVersionToExtract, mGPBitFlag, mCompressionMethod); + ALOGD(" modTime=0x%04x modDate=0x%04x crc32=0x%08lx\n", + mLastModFileTime, mLastModFileDate, mCRC32); + ALOGD(" compressedSize=%lu uncompressedSize=%lu\n", + mCompressedSize, mUncompressedSize); + ALOGD(" filenameLen=%u extraLen=%u\n", + mFileNameLength, mExtraFieldLength); + if (mFileName != NULL) + ALOGD(" filename: '%s'\n", mFileName); +} + + +/* + * =========================================================================== + * ZipEntry::CentralDirEntry + * =========================================================================== + */ + +/* + * Read the central dir entry that appears next in the file. + * + * On entry, "fp" should be positioned on the signature bytes for the + * entry. On exit, "fp" will point at the signature word for the next + * entry or for the EOCD. + */ +status_t ZipEntry::CentralDirEntry::read(FILE* fp) +{ + status_t result = NO_ERROR; + unsigned char buf[kCDELen]; + + /* no re-use */ + assert(mFileName == NULL); + assert(mExtraField == NULL); + assert(mFileComment == NULL); + + if (fread(buf, 1, kCDELen, fp) != kCDELen) { + result = UNKNOWN_ERROR; + goto bail; + } + + if (ZipEntry::getLongLE(&buf[0x00]) != kSignature) { + ALOGD("Whoops: didn't find expected signature\n"); + result = UNKNOWN_ERROR; + goto bail; + } + + mVersionMadeBy = ZipEntry::getShortLE(&buf[0x04]); + mVersionToExtract = ZipEntry::getShortLE(&buf[0x06]); + mGPBitFlag = ZipEntry::getShortLE(&buf[0x08]); + mCompressionMethod = ZipEntry::getShortLE(&buf[0x0a]); + mLastModFileTime = ZipEntry::getShortLE(&buf[0x0c]); + mLastModFileDate = ZipEntry::getShortLE(&buf[0x0e]); + mCRC32 = ZipEntry::getLongLE(&buf[0x10]); + mCompressedSize = ZipEntry::getLongLE(&buf[0x14]); + mUncompressedSize = ZipEntry::getLongLE(&buf[0x18]); + mFileNameLength = ZipEntry::getShortLE(&buf[0x1c]); + mExtraFieldLength = ZipEntry::getShortLE(&buf[0x1e]); + mFileCommentLength = ZipEntry::getShortLE(&buf[0x20]); + mDiskNumberStart = ZipEntry::getShortLE(&buf[0x22]); + mInternalAttrs = ZipEntry::getShortLE(&buf[0x24]); + mExternalAttrs = ZipEntry::getLongLE(&buf[0x26]); + mLocalHeaderRelOffset = ZipEntry::getLongLE(&buf[0x2a]); + + // TODO: validate sizes and offsets + + /* grab filename */ + if (mFileNameLength != 0) { + mFileName = new unsigned char[mFileNameLength+1]; + if (mFileName == NULL) { + result = NO_MEMORY; + goto bail; + } + if (fread(mFileName, 1, mFileNameLength, fp) != mFileNameLength) { + result = UNKNOWN_ERROR; + goto bail; + } + mFileName[mFileNameLength] = '\0'; + } + + /* read "extra field" */ + if (mExtraFieldLength != 0) { + mExtraField = new unsigned char[mExtraFieldLength+1]; + if (mExtraField == NULL) { + result = NO_MEMORY; + goto bail; + } + if (fread(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) { + result = UNKNOWN_ERROR; + goto bail; + } + mExtraField[mExtraFieldLength] = '\0'; + } + + + /* grab comment, if any */ + if (mFileCommentLength != 0) { + mFileComment = new unsigned char[mFileCommentLength+1]; + if (mFileComment == NULL) { + result = NO_MEMORY; + goto bail; + } + if (fread(mFileComment, 1, mFileCommentLength, fp) != mFileCommentLength) + { + result = UNKNOWN_ERROR; + goto bail; + } + mFileComment[mFileCommentLength] = '\0'; + } + +bail: + return result; +} + +/* + * Write a central dir entry. + */ +status_t ZipEntry::CentralDirEntry::write(FILE* fp) +{ + unsigned char buf[kCDELen]; + + ZipEntry::putLongLE(&buf[0x00], kSignature); + ZipEntry::putShortLE(&buf[0x04], mVersionMadeBy); + ZipEntry::putShortLE(&buf[0x06], mVersionToExtract); + ZipEntry::putShortLE(&buf[0x08], mGPBitFlag); + ZipEntry::putShortLE(&buf[0x0a], mCompressionMethod); + ZipEntry::putShortLE(&buf[0x0c], mLastModFileTime); + ZipEntry::putShortLE(&buf[0x0e], mLastModFileDate); + ZipEntry::putLongLE(&buf[0x10], mCRC32); + ZipEntry::putLongLE(&buf[0x14], mCompressedSize); + ZipEntry::putLongLE(&buf[0x18], mUncompressedSize); + ZipEntry::putShortLE(&buf[0x1c], mFileNameLength); + ZipEntry::putShortLE(&buf[0x1e], mExtraFieldLength); + ZipEntry::putShortLE(&buf[0x20], mFileCommentLength); + ZipEntry::putShortLE(&buf[0x22], mDiskNumberStart); + ZipEntry::putShortLE(&buf[0x24], mInternalAttrs); + ZipEntry::putLongLE(&buf[0x26], mExternalAttrs); + ZipEntry::putLongLE(&buf[0x2a], mLocalHeaderRelOffset); + + if (fwrite(buf, 1, kCDELen, fp) != kCDELen) + return UNKNOWN_ERROR; + + /* write filename */ + if (mFileNameLength != 0) { + if (fwrite(mFileName, 1, mFileNameLength, fp) != mFileNameLength) + return UNKNOWN_ERROR; + } + + /* write "extra field" */ + if (mExtraFieldLength != 0) { + if (fwrite(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) + return UNKNOWN_ERROR; + } + + /* write comment */ + if (mFileCommentLength != 0) { + if (fwrite(mFileComment, 1, mFileCommentLength, fp) != mFileCommentLength) + return UNKNOWN_ERROR; + } + + return NO_ERROR; +} + +/* + * Dump the contents of a CentralDirEntry object. + */ +void ZipEntry::CentralDirEntry::dump(void) const +{ + ALOGD(" CentralDirEntry contents:\n"); + ALOGD(" versMadeBy=%u versToExt=%u gpBits=0x%04x compression=%u\n", + mVersionMadeBy, mVersionToExtract, mGPBitFlag, mCompressionMethod); + ALOGD(" modTime=0x%04x modDate=0x%04x crc32=0x%08lx\n", + mLastModFileTime, mLastModFileDate, mCRC32); + ALOGD(" compressedSize=%lu uncompressedSize=%lu\n", + mCompressedSize, mUncompressedSize); + ALOGD(" filenameLen=%u extraLen=%u commentLen=%u\n", + mFileNameLength, mExtraFieldLength, mFileCommentLength); + ALOGD(" diskNumStart=%u intAttr=0x%04x extAttr=0x%08lx relOffset=%lu\n", + mDiskNumberStart, mInternalAttrs, mExternalAttrs, + mLocalHeaderRelOffset); + + if (mFileName != NULL) + ALOGD(" filename: '%s'\n", mFileName); + if (mFileComment != NULL) + ALOGD(" comment: '%s'\n", mFileComment); +} + diff --git a/tools/aapt/ZipEntry.h b/tools/aapt/ZipEntry.h new file mode 100644 index 000000000000..c2f3227cf230 --- /dev/null +++ b/tools/aapt/ZipEntry.h @@ -0,0 +1,345 @@ +/* + * 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. + */ + +// +// Zip archive entries. +// +// The ZipEntry class is tightly meshed with the ZipFile class. +// +#ifndef __LIBS_ZIPENTRY_H +#define __LIBS_ZIPENTRY_H + +#include <utils/Errors.h> + +#include <stdlib.h> +#include <stdio.h> + +namespace android { + +class ZipFile; + +/* + * ZipEntry objects represent a single entry in a Zip archive. + * + * You can use one of these to get or set information about an entry, but + * there are no functions here for accessing the data itself. (We could + * tuck a pointer to the ZipFile in here for convenience, but that raises + * the likelihood of using ZipEntry objects after discarding the ZipFile.) + * + * File information is stored in two places: next to the file data (the Local + * File Header, and possibly a Data Descriptor), and at the end of the file + * (the Central Directory Entry). The two must be kept in sync. + */ +class ZipEntry { +public: + friend class ZipFile; + + ZipEntry(void) + : mDeleted(false), mMarked(false) + {} + ~ZipEntry(void) {} + + /* + * Returns "true" if the data is compressed. + */ + bool isCompressed(void) const { + return mCDE.mCompressionMethod != kCompressStored; + } + int getCompressionMethod(void) const { return mCDE.mCompressionMethod; } + + /* + * Return the uncompressed length. + */ + off_t getUncompressedLen(void) const { return mCDE.mUncompressedSize; } + + /* + * Return the compressed length. For uncompressed data, this returns + * the same thing as getUncompresesdLen(). + */ + off_t getCompressedLen(void) const { return mCDE.mCompressedSize; } + + /* + * Return the offset of the local file header. + */ + off_t getLFHOffset(void) const { return mCDE.mLocalHeaderRelOffset; } + + /* + * Return the absolute file offset of the start of the compressed or + * uncompressed data. + */ + off_t getFileOffset(void) const { + return mCDE.mLocalHeaderRelOffset + + LocalFileHeader::kLFHLen + + mLFH.mFileNameLength + + mLFH.mExtraFieldLength; + } + + /* + * Return the data CRC. + */ + unsigned long getCRC32(void) const { return mCDE.mCRC32; } + + /* + * Return file modification time in UNIX seconds-since-epoch. + */ + time_t getModWhen(void) const; + + /* + * Return the archived file name. + */ + const char* getFileName(void) const { return (const char*) mCDE.mFileName; } + + /* + * Application-defined "mark". Can be useful when synchronizing the + * contents of an archive with contents on disk. + */ + bool getMarked(void) const { return mMarked; } + void setMarked(bool val) { mMarked = val; } + + /* + * Some basic functions for raw data manipulation. "LE" means + * Little Endian. + */ + static inline unsigned short getShortLE(const unsigned char* buf) { + return buf[0] | (buf[1] << 8); + } + static inline unsigned long getLongLE(const unsigned char* buf) { + return buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24); + } + static inline void putShortLE(unsigned char* buf, short val) { + buf[0] = (unsigned char) val; + buf[1] = (unsigned char) (val >> 8); + } + static inline void putLongLE(unsigned char* buf, long val) { + buf[0] = (unsigned char) val; + buf[1] = (unsigned char) (val >> 8); + buf[2] = (unsigned char) (val >> 16); + buf[3] = (unsigned char) (val >> 24); + } + + /* defined for Zip archives */ + enum { + kCompressStored = 0, // no compression + // shrunk = 1, + // reduced 1 = 2, + // reduced 2 = 3, + // reduced 3 = 4, + // reduced 4 = 5, + // imploded = 6, + // tokenized = 7, + kCompressDeflated = 8, // standard deflate + // Deflate64 = 9, + // lib imploded = 10, + // reserved = 11, + // bzip2 = 12, + }; + + /* + * Deletion flag. If set, the entry will be removed on the next + * call to "flush". + */ + bool getDeleted(void) const { return mDeleted; } + +protected: + /* + * Initialize the structure from the file, which is pointing at + * our Central Directory entry. + */ + status_t initFromCDE(FILE* fp); + + /* + * Initialize the structure for a new file. We need the filename + * and comment so that we can properly size the LFH area. The + * filename is mandatory, the comment is optional. + */ + void initNew(const char* fileName, const char* comment); + + /* + * Initialize the structure with the contents of a ZipEntry from + * another file. + */ + status_t initFromExternal(const ZipFile* pZipFile, const ZipEntry* pEntry); + + /* + * Add some pad bytes to the LFH. We do this by adding or resizing + * the "extra" field. + */ + status_t addPadding(int padding); + + /* + * Set information about the data for this entry. + */ + void setDataInfo(long uncompLen, long compLen, unsigned long crc32, + int compressionMethod); + + /* + * Set the modification date. + */ + void setModWhen(time_t when); + + /* + * Set the offset of the local file header, relative to the start of + * the current file. + */ + void setLFHOffset(off_t offset) { + mCDE.mLocalHeaderRelOffset = (long) offset; + } + + /* mark for deletion; used by ZipFile::remove() */ + void setDeleted(void) { mDeleted = true; } + +private: + /* these are private and not defined */ + ZipEntry(const ZipEntry& src); + ZipEntry& operator=(const ZipEntry& src); + + /* returns "true" if the CDE and the LFH agree */ + bool compareHeaders(void) const; + void copyCDEtoLFH(void); + + bool mDeleted; // set if entry is pending deletion + bool mMarked; // app-defined marker + + /* + * Every entry in the Zip archive starts off with one of these. + */ + class LocalFileHeader { + public: + LocalFileHeader(void) : + mVersionToExtract(0), + mGPBitFlag(0), + mCompressionMethod(0), + mLastModFileTime(0), + mLastModFileDate(0), + mCRC32(0), + mCompressedSize(0), + mUncompressedSize(0), + mFileNameLength(0), + mExtraFieldLength(0), + mFileName(NULL), + mExtraField(NULL) + {} + virtual ~LocalFileHeader(void) { + delete[] mFileName; + delete[] mExtraField; + } + + status_t read(FILE* fp); + status_t write(FILE* fp); + + // unsigned long mSignature; + unsigned short mVersionToExtract; + unsigned short mGPBitFlag; + unsigned short mCompressionMethod; + unsigned short mLastModFileTime; + unsigned short mLastModFileDate; + unsigned long mCRC32; + unsigned long mCompressedSize; + unsigned long mUncompressedSize; + unsigned short mFileNameLength; + unsigned short mExtraFieldLength; + unsigned char* mFileName; + unsigned char* mExtraField; + + enum { + kSignature = 0x04034b50, + kLFHLen = 30, // LocalFileHdr len, excl. var fields + }; + + void dump(void) const; + }; + + /* + * Every entry in the Zip archive has one of these in the "central + * directory" at the end of the file. + */ + class CentralDirEntry { + public: + CentralDirEntry(void) : + mVersionMadeBy(0), + mVersionToExtract(0), + mGPBitFlag(0), + mCompressionMethod(0), + mLastModFileTime(0), + mLastModFileDate(0), + mCRC32(0), + mCompressedSize(0), + mUncompressedSize(0), + mFileNameLength(0), + mExtraFieldLength(0), + mFileCommentLength(0), + mDiskNumberStart(0), + mInternalAttrs(0), + mExternalAttrs(0), + mLocalHeaderRelOffset(0), + mFileName(NULL), + mExtraField(NULL), + mFileComment(NULL) + {} + virtual ~CentralDirEntry(void) { + delete[] mFileName; + delete[] mExtraField; + delete[] mFileComment; + } + + status_t read(FILE* fp); + status_t write(FILE* fp); + + // unsigned long mSignature; + unsigned short mVersionMadeBy; + unsigned short mVersionToExtract; + unsigned short mGPBitFlag; + unsigned short mCompressionMethod; + unsigned short mLastModFileTime; + unsigned short mLastModFileDate; + unsigned long mCRC32; + unsigned long mCompressedSize; + unsigned long mUncompressedSize; + unsigned short mFileNameLength; + unsigned short mExtraFieldLength; + unsigned short mFileCommentLength; + unsigned short mDiskNumberStart; + unsigned short mInternalAttrs; + unsigned long mExternalAttrs; + unsigned long mLocalHeaderRelOffset; + unsigned char* mFileName; + unsigned char* mExtraField; + unsigned char* mFileComment; + + void dump(void) const; + + enum { + kSignature = 0x02014b50, + kCDELen = 46, // CentralDirEnt len, excl. var fields + }; + }; + + enum { + //kDataDescriptorSignature = 0x08074b50, // currently unused + kDataDescriptorLen = 16, // four 32-bit fields + + kDefaultVersion = 20, // need deflate, nothing much else + kDefaultMadeBy = 0x0317, // 03=UNIX, 17=spec v2.3 + kUsesDataDescr = 0x0008, // GPBitFlag bit 3 + }; + + LocalFileHeader mLFH; + CentralDirEntry mCDE; +}; + +}; // namespace android + +#endif // __LIBS_ZIPENTRY_H diff --git a/tools/aapt/ZipFile.cpp b/tools/aapt/ZipFile.cpp new file mode 100644 index 000000000000..8057068dd685 --- /dev/null +++ b/tools/aapt/ZipFile.cpp @@ -0,0 +1,1297 @@ +/* + * 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. + */ + +// +// Access to Zip archives. +// + +#define LOG_TAG "zip" + +#include <androidfw/ZipUtils.h> +#include <utils/Log.h> + +#include "ZipFile.h" + +#include <zlib.h> +#define DEF_MEM_LEVEL 8 // normally in zutil.h? + +#include <memory.h> +#include <sys/stat.h> +#include <errno.h> +#include <assert.h> + +using namespace android; + +/* + * Some environments require the "b", some choke on it. + */ +#define FILE_OPEN_RO "rb" +#define FILE_OPEN_RW "r+b" +#define FILE_OPEN_RW_CREATE "w+b" + +/* should live somewhere else? */ +static status_t errnoToStatus(int err) +{ + if (err == ENOENT) + return NAME_NOT_FOUND; + else if (err == EACCES) + return PERMISSION_DENIED; + else + return UNKNOWN_ERROR; +} + +/* + * Open a file and parse its guts. + */ +status_t ZipFile::open(const char* zipFileName, int flags) +{ + bool newArchive = false; + + assert(mZipFp == NULL); // no reopen + + if ((flags & kOpenTruncate)) + flags |= kOpenCreate; // trunc implies create + + if ((flags & kOpenReadOnly) && (flags & kOpenReadWrite)) + return INVALID_OPERATION; // not both + if (!((flags & kOpenReadOnly) || (flags & kOpenReadWrite))) + return INVALID_OPERATION; // not neither + if ((flags & kOpenCreate) && !(flags & kOpenReadWrite)) + return INVALID_OPERATION; // create requires write + + if (flags & kOpenTruncate) { + newArchive = true; + } else { + newArchive = (access(zipFileName, F_OK) != 0); + if (!(flags & kOpenCreate) && newArchive) { + /* not creating, must already exist */ + ALOGD("File %s does not exist", zipFileName); + return NAME_NOT_FOUND; + } + } + + /* open the file */ + const char* openflags; + if (flags & kOpenReadWrite) { + if (newArchive) + openflags = FILE_OPEN_RW_CREATE; + else + openflags = FILE_OPEN_RW; + } else { + openflags = FILE_OPEN_RO; + } + mZipFp = fopen(zipFileName, openflags); + if (mZipFp == NULL) { + int err = errno; + ALOGD("fopen failed: %d\n", err); + return errnoToStatus(err); + } + + status_t result; + if (!newArchive) { + /* + * Load the central directory. If that fails, then this probably + * isn't a Zip archive. + */ + result = readCentralDir(); + } else { + /* + * Newly-created. The EndOfCentralDir constructor actually + * sets everything to be the way we want it (all zeroes). We + * set mNeedCDRewrite so that we create *something* if the + * caller doesn't add any files. (We could also just unlink + * the file if it's brand new and nothing was added, but that's + * probably doing more than we really should -- the user might + * have a need for empty zip files.) + */ + mNeedCDRewrite = true; + result = NO_ERROR; + } + + if (flags & kOpenReadOnly) + mReadOnly = true; + else + assert(!mReadOnly); + + return result; +} + +/* + * Return the Nth entry in the archive. + */ +ZipEntry* ZipFile::getEntryByIndex(int idx) const +{ + if (idx < 0 || idx >= (int) mEntries.size()) + return NULL; + + return mEntries[idx]; +} + +/* + * Find an entry by name. + */ +ZipEntry* ZipFile::getEntryByName(const char* fileName) const +{ + /* + * Do a stupid linear string-compare search. + * + * There are various ways to speed this up, especially since it's rare + * to intermingle changes to the archive with "get by name" calls. We + * don't want to sort the mEntries vector itself, however, because + * it's used to recreate the Central Directory. + * + * (Hash table works, parallel list of pointers in sorted order is good.) + */ + int idx; + + for (idx = mEntries.size()-1; idx >= 0; idx--) { + ZipEntry* pEntry = mEntries[idx]; + if (!pEntry->getDeleted() && + strcmp(fileName, pEntry->getFileName()) == 0) + { + return pEntry; + } + } + + return NULL; +} + +/* + * Empty the mEntries vector. + */ +void ZipFile::discardEntries(void) +{ + int count = mEntries.size(); + + while (--count >= 0) + delete mEntries[count]; + + mEntries.clear(); +} + + +/* + * Find the central directory and read the contents. + * + * The fun thing about ZIP archives is that they may or may not be + * readable from start to end. In some cases, notably for archives + * that were written to stdout, the only length information is in the + * central directory at the end of the file. + * + * Of course, the central directory can be followed by a variable-length + * comment field, so we have to scan through it backwards. The comment + * is at most 64K, plus we have 18 bytes for the end-of-central-dir stuff + * itself, plus apparently sometimes people throw random junk on the end + * just for the fun of it. + * + * This is all a little wobbly. If the wrong value ends up in the EOCD + * area, we're hosed. This appears to be the way that everbody handles + * it though, so we're in pretty good company if this fails. + */ +status_t ZipFile::readCentralDir(void) +{ + status_t result = NO_ERROR; + unsigned char* buf = NULL; + off_t fileLength, seekStart; + long readAmount; + int i; + + fseek(mZipFp, 0, SEEK_END); + fileLength = ftell(mZipFp); + rewind(mZipFp); + + /* too small to be a ZIP archive? */ + if (fileLength < EndOfCentralDir::kEOCDLen) { + ALOGD("Length is %ld -- too small\n", (long)fileLength); + result = INVALID_OPERATION; + goto bail; + } + + buf = new unsigned char[EndOfCentralDir::kMaxEOCDSearch]; + if (buf == NULL) { + ALOGD("Failure allocating %d bytes for EOCD search", + EndOfCentralDir::kMaxEOCDSearch); + result = NO_MEMORY; + goto bail; + } + + if (fileLength > EndOfCentralDir::kMaxEOCDSearch) { + seekStart = fileLength - EndOfCentralDir::kMaxEOCDSearch; + readAmount = EndOfCentralDir::kMaxEOCDSearch; + } else { + seekStart = 0; + readAmount = (long) fileLength; + } + if (fseek(mZipFp, seekStart, SEEK_SET) != 0) { + ALOGD("Failure seeking to end of zip at %ld", (long) seekStart); + result = UNKNOWN_ERROR; + goto bail; + } + + /* read the last part of the file into the buffer */ + if (fread(buf, 1, readAmount, mZipFp) != (size_t) readAmount) { + ALOGD("short file? wanted %ld\n", readAmount); + result = UNKNOWN_ERROR; + goto bail; + } + + /* find the end-of-central-dir magic */ + for (i = readAmount - 4; i >= 0; i--) { + if (buf[i] == 0x50 && + ZipEntry::getLongLE(&buf[i]) == EndOfCentralDir::kSignature) + { + ALOGV("+++ Found EOCD at buf+%d\n", i); + break; + } + } + if (i < 0) { + ALOGD("EOCD not found, not Zip\n"); + result = INVALID_OPERATION; + goto bail; + } + + /* extract eocd values */ + result = mEOCD.readBuf(buf + i, readAmount - i); + if (result != NO_ERROR) { + ALOGD("Failure reading %ld bytes of EOCD values", readAmount - i); + goto bail; + } + //mEOCD.dump(); + + if (mEOCD.mDiskNumber != 0 || mEOCD.mDiskWithCentralDir != 0 || + mEOCD.mNumEntries != mEOCD.mTotalNumEntries) + { + ALOGD("Archive spanning not supported\n"); + result = INVALID_OPERATION; + goto bail; + } + + /* + * So far so good. "mCentralDirSize" is the size in bytes of the + * central directory, so we can just seek back that far to find it. + * We can also seek forward mCentralDirOffset bytes from the + * start of the file. + * + * We're not guaranteed to have the rest of the central dir in the + * buffer, nor are we guaranteed that the central dir will have any + * sort of convenient size. We need to skip to the start of it and + * read the header, then the other goodies. + * + * The only thing we really need right now is the file comment, which + * we're hoping to preserve. + */ + if (fseek(mZipFp, mEOCD.mCentralDirOffset, SEEK_SET) != 0) { + ALOGD("Failure seeking to central dir offset %ld\n", + mEOCD.mCentralDirOffset); + result = UNKNOWN_ERROR; + goto bail; + } + + /* + * Loop through and read the central dir entries. + */ + ALOGV("Scanning %d entries...\n", mEOCD.mTotalNumEntries); + int entry; + for (entry = 0; entry < mEOCD.mTotalNumEntries; entry++) { + ZipEntry* pEntry = new ZipEntry; + + result = pEntry->initFromCDE(mZipFp); + if (result != NO_ERROR) { + ALOGD("initFromCDE failed\n"); + delete pEntry; + goto bail; + } + + mEntries.add(pEntry); + } + + + /* + * If all went well, we should now be back at the EOCD. + */ + { + unsigned char checkBuf[4]; + if (fread(checkBuf, 1, 4, mZipFp) != 4) { + ALOGD("EOCD check read failed\n"); + result = INVALID_OPERATION; + goto bail; + } + if (ZipEntry::getLongLE(checkBuf) != EndOfCentralDir::kSignature) { + ALOGD("EOCD read check failed\n"); + result = UNKNOWN_ERROR; + goto bail; + } + ALOGV("+++ EOCD read check passed\n"); + } + +bail: + delete[] buf; + return result; +} + + +/* + * Add a new file to the archive. + * + * This requires creating and populating a ZipEntry structure, and copying + * the data into the file at the appropriate position. The "appropriate + * position" is the current location of the central directory, which we + * casually overwrite (we can put it back later). + * + * If we were concerned about safety, we would want to make all changes + * in a temp file and then overwrite the original after everything was + * safely written. Not really a concern for us. + */ +status_t ZipFile::addCommon(const char* fileName, const void* data, size_t size, + const char* storageName, int sourceType, int compressionMethod, + ZipEntry** ppEntry) +{ + ZipEntry* pEntry = NULL; + status_t result = NO_ERROR; + long lfhPosn, startPosn, endPosn, uncompressedLen; + FILE* inputFp = NULL; + unsigned long crc; + time_t modWhen; + + if (mReadOnly) + return INVALID_OPERATION; + + assert(compressionMethod == ZipEntry::kCompressDeflated || + compressionMethod == ZipEntry::kCompressStored); + + /* make sure we're in a reasonable state */ + assert(mZipFp != NULL); + assert(mEntries.size() == mEOCD.mTotalNumEntries); + + /* make sure it doesn't already exist */ + if (getEntryByName(storageName) != NULL) + return ALREADY_EXISTS; + + if (!data) { + inputFp = fopen(fileName, FILE_OPEN_RO); + if (inputFp == NULL) + return errnoToStatus(errno); + } + + if (fseek(mZipFp, mEOCD.mCentralDirOffset, SEEK_SET) != 0) { + result = UNKNOWN_ERROR; + goto bail; + } + + pEntry = new ZipEntry; + pEntry->initNew(storageName, NULL); + + /* + * From here on out, failures are more interesting. + */ + mNeedCDRewrite = true; + + /* + * Write the LFH, even though it's still mostly blank. We need it + * as a place-holder. In theory the LFH isn't necessary, but in + * practice some utilities demand it. + */ + lfhPosn = ftell(mZipFp); + pEntry->mLFH.write(mZipFp); + startPosn = ftell(mZipFp); + + /* + * Copy the data in, possibly compressing it as we go. + */ + if (sourceType == ZipEntry::kCompressStored) { + if (compressionMethod == ZipEntry::kCompressDeflated) { + bool failed = false; + result = compressFpToFp(mZipFp, inputFp, data, size, &crc); + if (result != NO_ERROR) { + ALOGD("compression failed, storing\n"); + failed = true; + } else { + /* + * Make sure it has compressed "enough". This probably ought + * to be set through an API call, but I don't expect our + * criteria to change over time. + */ + long src = inputFp ? ftell(inputFp) : size; + long dst = ftell(mZipFp) - startPosn; + if (dst + (dst / 10) > src) { + ALOGD("insufficient compression (src=%ld dst=%ld), storing\n", + src, dst); + failed = true; + } + } + + if (failed) { + compressionMethod = ZipEntry::kCompressStored; + if (inputFp) rewind(inputFp); + fseek(mZipFp, startPosn, SEEK_SET); + /* fall through to kCompressStored case */ + } + } + /* handle "no compression" request, or failed compression from above */ + if (compressionMethod == ZipEntry::kCompressStored) { + if (inputFp) { + result = copyFpToFp(mZipFp, inputFp, &crc); + } else { + result = copyDataToFp(mZipFp, data, size, &crc); + } + if (result != NO_ERROR) { + // don't need to truncate; happens in CDE rewrite + ALOGD("failed copying data in\n"); + goto bail; + } + } + + // currently seeked to end of file + uncompressedLen = inputFp ? ftell(inputFp) : size; + } else if (sourceType == ZipEntry::kCompressDeflated) { + /* we should support uncompressed-from-compressed, but it's not + * important right now */ + assert(compressionMethod == ZipEntry::kCompressDeflated); + + bool scanResult; + int method; + long compressedLen; + + scanResult = ZipUtils::examineGzip(inputFp, &method, &uncompressedLen, + &compressedLen, &crc); + if (!scanResult || method != ZipEntry::kCompressDeflated) { + ALOGD("this isn't a deflated gzip file?"); + result = UNKNOWN_ERROR; + goto bail; + } + + result = copyPartialFpToFp(mZipFp, inputFp, compressedLen, NULL); + if (result != NO_ERROR) { + ALOGD("failed copying gzip data in\n"); + goto bail; + } + } else { + assert(false); + result = UNKNOWN_ERROR; + goto bail; + } + + /* + * We could write the "Data Descriptor", but there doesn't seem to + * be any point since we're going to go back and write the LFH. + * + * Update file offsets. + */ + endPosn = ftell(mZipFp); // seeked to end of compressed data + + /* + * Success! Fill out new values. + */ + pEntry->setDataInfo(uncompressedLen, endPosn - startPosn, crc, + compressionMethod); + modWhen = getModTime(inputFp ? fileno(inputFp) : fileno(mZipFp)); + pEntry->setModWhen(modWhen); + pEntry->setLFHOffset(lfhPosn); + mEOCD.mNumEntries++; + mEOCD.mTotalNumEntries++; + mEOCD.mCentralDirSize = 0; // mark invalid; set by flush() + mEOCD.mCentralDirOffset = endPosn; + + /* + * Go back and write the LFH. + */ + if (fseek(mZipFp, lfhPosn, SEEK_SET) != 0) { + result = UNKNOWN_ERROR; + goto bail; + } + pEntry->mLFH.write(mZipFp); + + /* + * Add pEntry to the list. + */ + mEntries.add(pEntry); + if (ppEntry != NULL) + *ppEntry = pEntry; + pEntry = NULL; + +bail: + if (inputFp != NULL) + fclose(inputFp); + delete pEntry; + return result; +} + +/* + * Add an entry by copying it from another zip file. If "padding" is + * nonzero, the specified number of bytes will be added to the "extra" + * field in the header. + * + * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. + */ +status_t ZipFile::add(const ZipFile* pSourceZip, const ZipEntry* pSourceEntry, + int padding, ZipEntry** ppEntry) +{ + ZipEntry* pEntry = NULL; + status_t result; + long lfhPosn, endPosn; + + if (mReadOnly) + return INVALID_OPERATION; + + /* make sure we're in a reasonable state */ + assert(mZipFp != NULL); + assert(mEntries.size() == mEOCD.mTotalNumEntries); + + if (fseek(mZipFp, mEOCD.mCentralDirOffset, SEEK_SET) != 0) { + result = UNKNOWN_ERROR; + goto bail; + } + + pEntry = new ZipEntry; + if (pEntry == NULL) { + result = NO_MEMORY; + goto bail; + } + + result = pEntry->initFromExternal(pSourceZip, pSourceEntry); + if (result != NO_ERROR) + goto bail; + if (padding != 0) { + result = pEntry->addPadding(padding); + if (result != NO_ERROR) + goto bail; + } + + /* + * From here on out, failures are more interesting. + */ + mNeedCDRewrite = true; + + /* + * Write the LFH. Since we're not recompressing the data, we already + * have all of the fields filled out. + */ + lfhPosn = ftell(mZipFp); + pEntry->mLFH.write(mZipFp); + + /* + * Copy the data over. + * + * If the "has data descriptor" flag is set, we want to copy the DD + * fields as well. This is a fixed-size area immediately following + * the data. + */ + if (fseek(pSourceZip->mZipFp, pSourceEntry->getFileOffset(), SEEK_SET) != 0) + { + result = UNKNOWN_ERROR; + goto bail; + } + + off_t copyLen; + copyLen = pSourceEntry->getCompressedLen(); + if ((pSourceEntry->mLFH.mGPBitFlag & ZipEntry::kUsesDataDescr) != 0) + copyLen += ZipEntry::kDataDescriptorLen; + + if (copyPartialFpToFp(mZipFp, pSourceZip->mZipFp, copyLen, NULL) + != NO_ERROR) + { + ALOGW("copy of '%s' failed\n", pEntry->mCDE.mFileName); + result = UNKNOWN_ERROR; + goto bail; + } + + /* + * Update file offsets. + */ + endPosn = ftell(mZipFp); + + /* + * Success! Fill out new values. + */ + pEntry->setLFHOffset(lfhPosn); // sets mCDE.mLocalHeaderRelOffset + mEOCD.mNumEntries++; + mEOCD.mTotalNumEntries++; + mEOCD.mCentralDirSize = 0; // mark invalid; set by flush() + mEOCD.mCentralDirOffset = endPosn; + + /* + * Add pEntry to the list. + */ + mEntries.add(pEntry); + if (ppEntry != NULL) + *ppEntry = pEntry; + pEntry = NULL; + + result = NO_ERROR; + +bail: + delete pEntry; + return result; +} + +/* + * Copy all of the bytes in "src" to "dst". + * + * On exit, "srcFp" will be seeked to the end of the file, and "dstFp" + * will be seeked immediately past the data. + */ +status_t ZipFile::copyFpToFp(FILE* dstFp, FILE* srcFp, unsigned long* pCRC32) +{ + unsigned char tmpBuf[32768]; + size_t count; + + *pCRC32 = crc32(0L, Z_NULL, 0); + + while (1) { + count = fread(tmpBuf, 1, sizeof(tmpBuf), srcFp); + if (ferror(srcFp) || ferror(dstFp)) + return errnoToStatus(errno); + if (count == 0) + break; + + *pCRC32 = crc32(*pCRC32, tmpBuf, count); + + if (fwrite(tmpBuf, 1, count, dstFp) != count) { + ALOGD("fwrite %d bytes failed\n", (int) count); + return UNKNOWN_ERROR; + } + } + + return NO_ERROR; +} + +/* + * Copy all of the bytes in "src" to "dst". + * + * On exit, "dstFp" will be seeked immediately past the data. + */ +status_t ZipFile::copyDataToFp(FILE* dstFp, + const void* data, size_t size, unsigned long* pCRC32) +{ + size_t count; + + *pCRC32 = crc32(0L, Z_NULL, 0); + if (size > 0) { + *pCRC32 = crc32(*pCRC32, (const unsigned char*)data, size); + if (fwrite(data, 1, size, dstFp) != size) { + ALOGD("fwrite %d bytes failed\n", (int) size); + return UNKNOWN_ERROR; + } + } + + return NO_ERROR; +} + +/* + * Copy some of the bytes in "src" to "dst". + * + * If "pCRC32" is NULL, the CRC will not be computed. + * + * On exit, "srcFp" will be seeked to the end of the file, and "dstFp" + * will be seeked immediately past the data just written. + */ +status_t ZipFile::copyPartialFpToFp(FILE* dstFp, FILE* srcFp, long length, + unsigned long* pCRC32) +{ + unsigned char tmpBuf[32768]; + size_t count; + + if (pCRC32 != NULL) + *pCRC32 = crc32(0L, Z_NULL, 0); + + while (length) { + long readSize; + + readSize = sizeof(tmpBuf); + if (readSize > length) + readSize = length; + + count = fread(tmpBuf, 1, readSize, srcFp); + if ((long) count != readSize) { // error or unexpected EOF + ALOGD("fread %d bytes failed\n", (int) readSize); + return UNKNOWN_ERROR; + } + + if (pCRC32 != NULL) + *pCRC32 = crc32(*pCRC32, tmpBuf, count); + + if (fwrite(tmpBuf, 1, count, dstFp) != count) { + ALOGD("fwrite %d bytes failed\n", (int) count); + return UNKNOWN_ERROR; + } + + length -= readSize; + } + + return NO_ERROR; +} + +/* + * Compress all of the data in "srcFp" and write it to "dstFp". + * + * On exit, "srcFp" will be seeked to the end of the file, and "dstFp" + * will be seeked immediately past the compressed data. + */ +status_t ZipFile::compressFpToFp(FILE* dstFp, FILE* srcFp, + const void* data, size_t size, unsigned long* pCRC32) +{ + status_t result = NO_ERROR; + const size_t kBufSize = 32768; + unsigned char* inBuf = NULL; + unsigned char* outBuf = NULL; + z_stream zstream; + bool atEof = false; // no feof() aviailable yet + unsigned long crc; + int zerr; + + /* + * Create an input buffer and an output buffer. + */ + inBuf = new unsigned char[kBufSize]; + outBuf = new unsigned char[kBufSize]; + if (inBuf == NULL || outBuf == NULL) { + result = NO_MEMORY; + goto bail; + } + + /* + * Initialize the zlib stream. + */ + memset(&zstream, 0, sizeof(zstream)); + zstream.zalloc = Z_NULL; + zstream.zfree = Z_NULL; + zstream.opaque = Z_NULL; + zstream.next_in = NULL; + zstream.avail_in = 0; + zstream.next_out = outBuf; + zstream.avail_out = kBufSize; + zstream.data_type = Z_UNKNOWN; + + zerr = deflateInit2(&zstream, Z_BEST_COMPRESSION, + Z_DEFLATED, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY); + if (zerr != Z_OK) { + result = UNKNOWN_ERROR; + if (zerr == Z_VERSION_ERROR) { + ALOGE("Installed zlib is not compatible with linked version (%s)\n", + ZLIB_VERSION); + } else { + ALOGD("Call to deflateInit2 failed (zerr=%d)\n", zerr); + } + goto bail; + } + + crc = crc32(0L, Z_NULL, 0); + + /* + * Loop while we have data. + */ + do { + size_t getSize; + int flush; + + /* only read if the input buffer is empty */ + if (zstream.avail_in == 0 && !atEof) { + ALOGV("+++ reading %d bytes\n", (int)kBufSize); + if (data) { + getSize = size > kBufSize ? kBufSize : size; + memcpy(inBuf, data, getSize); + data = ((const char*)data) + getSize; + size -= getSize; + } else { + getSize = fread(inBuf, 1, kBufSize, srcFp); + if (ferror(srcFp)) { + ALOGD("deflate read failed (errno=%d)\n", errno); + goto z_bail; + } + } + if (getSize < kBufSize) { + ALOGV("+++ got %d bytes, EOF reached\n", + (int)getSize); + atEof = true; + } + + crc = crc32(crc, inBuf, getSize); + + zstream.next_in = inBuf; + zstream.avail_in = getSize; + } + + if (atEof) + flush = Z_FINISH; /* tell zlib that we're done */ + else + flush = Z_NO_FLUSH; /* more to come! */ + + zerr = deflate(&zstream, flush); + if (zerr != Z_OK && zerr != Z_STREAM_END) { + ALOGD("zlib deflate call failed (zerr=%d)\n", zerr); + result = UNKNOWN_ERROR; + goto z_bail; + } + + /* write when we're full or when we're done */ + if (zstream.avail_out == 0 || + (zerr == Z_STREAM_END && zstream.avail_out != (uInt) kBufSize)) + { + ALOGV("+++ writing %d bytes\n", (int) (zstream.next_out - outBuf)); + if (fwrite(outBuf, 1, zstream.next_out - outBuf, dstFp) != + (size_t)(zstream.next_out - outBuf)) + { + ALOGD("write %d failed in deflate\n", + (int) (zstream.next_out - outBuf)); + goto z_bail; + } + + zstream.next_out = outBuf; + zstream.avail_out = kBufSize; + } + } while (zerr == Z_OK); + + assert(zerr == Z_STREAM_END); /* other errors should've been caught */ + + *pCRC32 = crc; + +z_bail: + deflateEnd(&zstream); /* free up any allocated structures */ + +bail: + delete[] inBuf; + delete[] outBuf; + + return result; +} + +/* + * Mark an entry as deleted. + * + * We will eventually need to crunch the file down, but if several files + * are being removed (perhaps as part of an "update" process) we can make + * things considerably faster by deferring the removal to "flush" time. + */ +status_t ZipFile::remove(ZipEntry* pEntry) +{ + /* + * Should verify that pEntry is actually part of this archive, and + * not some stray ZipEntry from a different file. + */ + + /* mark entry as deleted, and mark archive as dirty */ + pEntry->setDeleted(); + mNeedCDRewrite = true; + return NO_ERROR; +} + +/* + * Flush any pending writes. + * + * In particular, this will crunch out deleted entries, and write the + * Central Directory and EOCD if we have stomped on them. + */ +status_t ZipFile::flush(void) +{ + status_t result = NO_ERROR; + long eocdPosn; + int i, count; + + if (mReadOnly) + return INVALID_OPERATION; + if (!mNeedCDRewrite) + return NO_ERROR; + + assert(mZipFp != NULL); + + result = crunchArchive(); + if (result != NO_ERROR) + return result; + + if (fseek(mZipFp, mEOCD.mCentralDirOffset, SEEK_SET) != 0) + return UNKNOWN_ERROR; + + count = mEntries.size(); + for (i = 0; i < count; i++) { + ZipEntry* pEntry = mEntries[i]; + pEntry->mCDE.write(mZipFp); + } + + eocdPosn = ftell(mZipFp); + mEOCD.mCentralDirSize = eocdPosn - mEOCD.mCentralDirOffset; + + mEOCD.write(mZipFp); + + /* + * If we had some stuff bloat up during compression and get replaced + * with plain files, or if we deleted some entries, there's a lot + * of wasted space at the end of the file. Remove it now. + */ + if (ftruncate(fileno(mZipFp), ftell(mZipFp)) != 0) { + ALOGW("ftruncate failed %ld: %s\n", ftell(mZipFp), strerror(errno)); + // not fatal + } + + /* should we clear the "newly added" flag in all entries now? */ + + mNeedCDRewrite = false; + return NO_ERROR; +} + +/* + * Crunch deleted files out of an archive by shifting the later files down. + * + * Because we're not using a temp file, we do the operation inside the + * current file. + */ +status_t ZipFile::crunchArchive(void) +{ + status_t result = NO_ERROR; + int i, count; + long delCount, adjust; + +#if 0 + printf("CONTENTS:\n"); + for (i = 0; i < (int) mEntries.size(); i++) { + printf(" %d: lfhOff=%ld del=%d\n", + i, mEntries[i]->getLFHOffset(), mEntries[i]->getDeleted()); + } + printf(" END is %ld\n", (long) mEOCD.mCentralDirOffset); +#endif + + /* + * Roll through the set of files, shifting them as appropriate. We + * could probably get a slight performance improvement by sliding + * multiple files down at once (because we could use larger reads + * when operating on batches of small files), but it's not that useful. + */ + count = mEntries.size(); + delCount = adjust = 0; + for (i = 0; i < count; i++) { + ZipEntry* pEntry = mEntries[i]; + long span; + + if (pEntry->getLFHOffset() != 0) { + long nextOffset; + + /* Get the length of this entry by finding the offset + * of the next entry. Directory entries don't have + * file offsets, so we need to find the next non-directory + * entry. + */ + nextOffset = 0; + for (int ii = i+1; nextOffset == 0 && ii < count; ii++) + nextOffset = mEntries[ii]->getLFHOffset(); + if (nextOffset == 0) + nextOffset = mEOCD.mCentralDirOffset; + span = nextOffset - pEntry->getLFHOffset(); + + assert(span >= ZipEntry::LocalFileHeader::kLFHLen); + } else { + /* This is a directory entry. It doesn't have + * any actual file contents, so there's no need to + * move anything. + */ + span = 0; + } + + //printf("+++ %d: off=%ld span=%ld del=%d [count=%d]\n", + // i, pEntry->getLFHOffset(), span, pEntry->getDeleted(), count); + + if (pEntry->getDeleted()) { + adjust += span; + delCount++; + + delete pEntry; + mEntries.removeAt(i); + + /* adjust loop control */ + count--; + i--; + } else if (span != 0 && adjust > 0) { + /* shuffle this entry back */ + //printf("+++ Shuffling '%s' back %ld\n", + // pEntry->getFileName(), adjust); + result = filemove(mZipFp, pEntry->getLFHOffset() - adjust, + pEntry->getLFHOffset(), span); + if (result != NO_ERROR) { + /* this is why you use a temp file */ + ALOGE("error during crunch - archive is toast\n"); + return result; + } + + pEntry->setLFHOffset(pEntry->getLFHOffset() - adjust); + } + } + + /* + * Fix EOCD info. We have to wait until the end to do some of this + * because we use mCentralDirOffset to determine "span" for the + * last entry. + */ + mEOCD.mCentralDirOffset -= adjust; + mEOCD.mNumEntries -= delCount; + mEOCD.mTotalNumEntries -= delCount; + mEOCD.mCentralDirSize = 0; // mark invalid; set by flush() + + assert(mEOCD.mNumEntries == mEOCD.mTotalNumEntries); + assert(mEOCD.mNumEntries == count); + + return result; +} + +/* + * Works like memmove(), but on pieces of a file. + */ +status_t ZipFile::filemove(FILE* fp, off_t dst, off_t src, size_t n) +{ + if (dst == src || n <= 0) + return NO_ERROR; + + unsigned char readBuf[32768]; + + if (dst < src) { + /* shift stuff toward start of file; must read from start */ + while (n != 0) { + size_t getSize = sizeof(readBuf); + if (getSize > n) + getSize = n; + + if (fseek(fp, (long) src, SEEK_SET) != 0) { + ALOGD("filemove src seek %ld failed\n", (long) src); + return UNKNOWN_ERROR; + } + + if (fread(readBuf, 1, getSize, fp) != getSize) { + ALOGD("filemove read %ld off=%ld failed\n", + (long) getSize, (long) src); + return UNKNOWN_ERROR; + } + + if (fseek(fp, (long) dst, SEEK_SET) != 0) { + ALOGD("filemove dst seek %ld failed\n", (long) dst); + return UNKNOWN_ERROR; + } + + if (fwrite(readBuf, 1, getSize, fp) != getSize) { + ALOGD("filemove write %ld off=%ld failed\n", + (long) getSize, (long) dst); + return UNKNOWN_ERROR; + } + + src += getSize; + dst += getSize; + n -= getSize; + } + } else { + /* shift stuff toward end of file; must read from end */ + assert(false); // write this someday, maybe + return UNKNOWN_ERROR; + } + + return NO_ERROR; +} + + +/* + * Get the modification time from a file descriptor. + */ +time_t ZipFile::getModTime(int fd) +{ + struct stat sb; + + if (fstat(fd, &sb) < 0) { + ALOGD("HEY: fstat on fd %d failed\n", fd); + return (time_t) -1; + } + + return sb.st_mtime; +} + + +#if 0 /* this is a bad idea */ +/* + * Get a copy of the Zip file descriptor. + * + * We don't allow this if the file was opened read-write because we tend + * to leave the file contents in an uncertain state between calls to + * flush(). The duplicated file descriptor should only be valid for reads. + */ +int ZipFile::getZipFd(void) const +{ + if (!mReadOnly) + return INVALID_OPERATION; + assert(mZipFp != NULL); + + int fd; + fd = dup(fileno(mZipFp)); + if (fd < 0) { + ALOGD("didn't work, errno=%d\n", errno); + } + + return fd; +} +#endif + + +#if 0 +/* + * Expand data. + */ +bool ZipFile::uncompress(const ZipEntry* pEntry, void* buf) const +{ + return false; +} +#endif + +// free the memory when you're done +void* ZipFile::uncompress(const ZipEntry* entry) +{ + size_t unlen = entry->getUncompressedLen(); + size_t clen = entry->getCompressedLen(); + + void* buf = malloc(unlen); + if (buf == NULL) { + return NULL; + } + + fseek(mZipFp, 0, SEEK_SET); + + off_t offset = entry->getFileOffset(); + if (fseek(mZipFp, offset, SEEK_SET) != 0) { + goto bail; + } + + switch (entry->getCompressionMethod()) + { + case ZipEntry::kCompressStored: { + ssize_t amt = fread(buf, 1, unlen, mZipFp); + if (amt != (ssize_t)unlen) { + goto bail; + } +#if 0 + printf("data...\n"); + const unsigned char* p = (unsigned char*)buf; + const unsigned char* end = p+unlen; + for (int i=0; i<32 && p < end; i++) { + printf("0x%08x ", (int)(offset+(i*0x10))); + for (int j=0; j<0x10 && p < end; j++) { + printf(" %02x", *p); + p++; + } + printf("\n"); + } +#endif + + } + break; + case ZipEntry::kCompressDeflated: { + if (!ZipUtils::inflateToBuffer(mZipFp, buf, unlen, clen)) { + goto bail; + } + } + break; + default: + goto bail; + } + return buf; + +bail: + free(buf); + return NULL; +} + + +/* + * =========================================================================== + * ZipFile::EndOfCentralDir + * =========================================================================== + */ + +/* + * Read the end-of-central-dir fields. + * + * "buf" should be positioned at the EOCD signature, and should contain + * the entire EOCD area including the comment. + */ +status_t ZipFile::EndOfCentralDir::readBuf(const unsigned char* buf, int len) +{ + /* don't allow re-use */ + assert(mComment == NULL); + + if (len < kEOCDLen) { + /* looks like ZIP file got truncated */ + ALOGD(" Zip EOCD: expected >= %d bytes, found %d\n", + kEOCDLen, len); + return INVALID_OPERATION; + } + + /* this should probably be an assert() */ + if (ZipEntry::getLongLE(&buf[0x00]) != kSignature) + return UNKNOWN_ERROR; + + mDiskNumber = ZipEntry::getShortLE(&buf[0x04]); + mDiskWithCentralDir = ZipEntry::getShortLE(&buf[0x06]); + mNumEntries = ZipEntry::getShortLE(&buf[0x08]); + mTotalNumEntries = ZipEntry::getShortLE(&buf[0x0a]); + mCentralDirSize = ZipEntry::getLongLE(&buf[0x0c]); + mCentralDirOffset = ZipEntry::getLongLE(&buf[0x10]); + mCommentLen = ZipEntry::getShortLE(&buf[0x14]); + + // TODO: validate mCentralDirOffset + + if (mCommentLen > 0) { + if (kEOCDLen + mCommentLen > len) { + ALOGD("EOCD(%d) + comment(%d) exceeds len (%d)\n", + kEOCDLen, mCommentLen, len); + return UNKNOWN_ERROR; + } + mComment = new unsigned char[mCommentLen]; + memcpy(mComment, buf + kEOCDLen, mCommentLen); + } + + return NO_ERROR; +} + +/* + * Write an end-of-central-directory section. + */ +status_t ZipFile::EndOfCentralDir::write(FILE* fp) +{ + unsigned char buf[kEOCDLen]; + + ZipEntry::putLongLE(&buf[0x00], kSignature); + ZipEntry::putShortLE(&buf[0x04], mDiskNumber); + ZipEntry::putShortLE(&buf[0x06], mDiskWithCentralDir); + ZipEntry::putShortLE(&buf[0x08], mNumEntries); + ZipEntry::putShortLE(&buf[0x0a], mTotalNumEntries); + ZipEntry::putLongLE(&buf[0x0c], mCentralDirSize); + ZipEntry::putLongLE(&buf[0x10], mCentralDirOffset); + ZipEntry::putShortLE(&buf[0x14], mCommentLen); + + if (fwrite(buf, 1, kEOCDLen, fp) != kEOCDLen) + return UNKNOWN_ERROR; + if (mCommentLen > 0) { + assert(mComment != NULL); + if (fwrite(mComment, mCommentLen, 1, fp) != mCommentLen) + return UNKNOWN_ERROR; + } + + return NO_ERROR; +} + +/* + * Dump the contents of an EndOfCentralDir object. + */ +void ZipFile::EndOfCentralDir::dump(void) const +{ + ALOGD(" EndOfCentralDir contents:\n"); + ALOGD(" diskNum=%u diskWCD=%u numEnt=%u totalNumEnt=%u\n", + mDiskNumber, mDiskWithCentralDir, mNumEntries, mTotalNumEntries); + ALOGD(" centDirSize=%lu centDirOff=%lu commentLen=%u\n", + mCentralDirSize, mCentralDirOffset, mCommentLen); +} + diff --git a/tools/aapt/ZipFile.h b/tools/aapt/ZipFile.h new file mode 100644 index 000000000000..78775502884b --- /dev/null +++ b/tools/aapt/ZipFile.h @@ -0,0 +1,270 @@ +/* + * 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. + */ + +// +// General-purpose Zip archive access. This class allows both reading and +// writing to Zip archives, including deletion of existing entries. +// +#ifndef __LIBS_ZIPFILE_H +#define __LIBS_ZIPFILE_H + +#include <utils/Vector.h> +#include <utils/Errors.h> +#include <stdio.h> + +#include "ZipEntry.h" + +namespace android { + +/* + * Manipulate a Zip archive. + * + * Some changes will not be visible in the until until "flush" is called. + * + * The correct way to update a file archive is to make all changes to a + * copy of the archive in a temporary file, and then unlink/rename over + * the original after everything completes. Because we're only interested + * in using this for packaging, we don't worry about such things. Crashing + * after making changes and before flush() completes could leave us with + * an unusable Zip archive. + */ +class ZipFile { +public: + ZipFile(void) + : mZipFp(NULL), mReadOnly(false), mNeedCDRewrite(false) + {} + ~ZipFile(void) { + if (!mReadOnly) + flush(); + if (mZipFp != NULL) + fclose(mZipFp); + discardEntries(); + } + + /* + * Open a new or existing archive. + */ + enum { + kOpenReadOnly = 0x01, + kOpenReadWrite = 0x02, + kOpenCreate = 0x04, // create if it doesn't exist + kOpenTruncate = 0x08, // if it exists, empty it + }; + status_t open(const char* zipFileName, int flags); + + /* + * Add a file to the end of the archive. Specify whether you want the + * library to try to store it compressed. + * + * If "storageName" is specified, the archive will use that instead + * of "fileName". + * + * If there is already an entry with the same name, the call fails. + * Existing entries with the same name must be removed first. + * + * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. + */ + status_t add(const char* fileName, int compressionMethod, + ZipEntry** ppEntry) + { + return add(fileName, fileName, compressionMethod, ppEntry); + } + status_t add(const char* fileName, const char* storageName, + int compressionMethod, ZipEntry** ppEntry) + { + return addCommon(fileName, NULL, 0, storageName, + ZipEntry::kCompressStored, + compressionMethod, ppEntry); + } + + /* + * Add a file that is already compressed with gzip. + * + * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. + */ + status_t addGzip(const char* fileName, const char* storageName, + ZipEntry** ppEntry) + { + return addCommon(fileName, NULL, 0, storageName, + ZipEntry::kCompressDeflated, + ZipEntry::kCompressDeflated, ppEntry); + } + + /* + * Add a file from an in-memory data buffer. + * + * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. + */ + status_t add(const void* data, size_t size, const char* storageName, + int compressionMethod, ZipEntry** ppEntry) + { + return addCommon(NULL, data, size, storageName, + ZipEntry::kCompressStored, + compressionMethod, ppEntry); + } + + /* + * Add an entry by copying it from another zip file. If "padding" is + * nonzero, the specified number of bytes will be added to the "extra" + * field in the header. + * + * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. + */ + status_t add(const ZipFile* pSourceZip, const ZipEntry* pSourceEntry, + int padding, ZipEntry** ppEntry); + + /* + * Mark an entry as having been removed. It is not actually deleted + * from the archive or our internal data structures until flush() is + * called. + */ + status_t remove(ZipEntry* pEntry); + + /* + * Flush changes. If mNeedCDRewrite is set, this writes the central dir. + */ + status_t flush(void); + + /* + * Expand the data into the buffer provided. The buffer must hold + * at least <uncompressed len> bytes. Variation expands directly + * to a file. + * + * Returns "false" if an error was encountered in the compressed data. + */ + //bool uncompress(const ZipEntry* pEntry, void* buf) const; + //bool uncompress(const ZipEntry* pEntry, FILE* fp) const; + void* uncompress(const ZipEntry* pEntry); + + /* + * Get an entry, by name. Returns NULL if not found. + * + * Does not return entries pending deletion. + */ + ZipEntry* getEntryByName(const char* fileName) const; + + /* + * Get the Nth entry in the archive. + * + * This will return an entry that is pending deletion. + */ + int getNumEntries(void) const { return mEntries.size(); } + ZipEntry* getEntryByIndex(int idx) const; + +private: + /* these are private and not defined */ + ZipFile(const ZipFile& src); + ZipFile& operator=(const ZipFile& src); + + class EndOfCentralDir { + public: + EndOfCentralDir(void) : + mDiskNumber(0), + mDiskWithCentralDir(0), + mNumEntries(0), + mTotalNumEntries(0), + mCentralDirSize(0), + mCentralDirOffset(0), + mCommentLen(0), + mComment(NULL) + {} + virtual ~EndOfCentralDir(void) { + delete[] mComment; + } + + status_t readBuf(const unsigned char* buf, int len); + status_t write(FILE* fp); + + //unsigned long mSignature; + unsigned short mDiskNumber; + unsigned short mDiskWithCentralDir; + unsigned short mNumEntries; + unsigned short mTotalNumEntries; + unsigned long mCentralDirSize; + unsigned long mCentralDirOffset; // offset from first disk + unsigned short mCommentLen; + unsigned char* mComment; + + enum { + kSignature = 0x06054b50, + kEOCDLen = 22, // EndOfCentralDir len, excl. comment + + kMaxCommentLen = 65535, // longest possible in ushort + kMaxEOCDSearch = kMaxCommentLen + EndOfCentralDir::kEOCDLen, + + }; + + void dump(void) const; + }; + + + /* read all entries in the central dir */ + status_t readCentralDir(void); + + /* crunch deleted entries out */ + status_t crunchArchive(void); + + /* clean up mEntries */ + void discardEntries(void); + + /* common handler for all "add" functions */ + status_t addCommon(const char* fileName, const void* data, size_t size, + const char* storageName, int sourceType, int compressionMethod, + ZipEntry** ppEntry); + + /* copy all of "srcFp" into "dstFp" */ + status_t copyFpToFp(FILE* dstFp, FILE* srcFp, unsigned long* pCRC32); + /* copy all of "data" into "dstFp" */ + status_t copyDataToFp(FILE* dstFp, + const void* data, size_t size, unsigned long* pCRC32); + /* copy some of "srcFp" into "dstFp" */ + status_t copyPartialFpToFp(FILE* dstFp, FILE* srcFp, long length, + unsigned long* pCRC32); + /* like memmove(), but on parts of a single file */ + status_t filemove(FILE* fp, off_t dest, off_t src, size_t n); + /* compress all of "srcFp" into "dstFp", using Deflate */ + status_t compressFpToFp(FILE* dstFp, FILE* srcFp, + const void* data, size_t size, unsigned long* pCRC32); + + /* get modification date from a file descriptor */ + time_t getModTime(int fd); + + /* + * We use stdio FILE*, which gives us buffering but makes dealing + * with files >2GB awkward. Until we support Zip64, we're fine. + */ + FILE* mZipFp; // Zip file pointer + + /* one of these per file */ + EndOfCentralDir mEOCD; + + /* did we open this read-only? */ + bool mReadOnly; + + /* set this when we trash the central dir */ + bool mNeedCDRewrite; + + /* + * One ZipEntry per entry in the zip file. I'm using pointers instead + * of objects because it's easier than making operator= work for the + * classes and sub-classes. + */ + Vector<ZipEntry*> mEntries; +}; + +}; // namespace android + +#endif // __LIBS_ZIPFILE_H diff --git a/tools/aapt/printapk.cpp b/tools/aapt/printapk.cpp new file mode 100644 index 000000000000..4cf73d81294b --- /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/pseudolocalize.cpp b/tools/aapt/pseudolocalize.cpp new file mode 100644 index 000000000000..9e50c5ac5eb1 --- /dev/null +++ b/tools/aapt/pseudolocalize.cpp @@ -0,0 +1,119 @@ +#include "pseudolocalize.h" + +using namespace std; + +static const char* +pseudolocalize_char(char c) +{ + switch (c) { + case 'a': return "\xc4\x83"; + case 'b': return "\xcf\x84"; + case 'c': return "\xc4\x8b"; + case 'd': return "\xc4\x8f"; + case 'e': return "\xc4\x99"; + case 'f': return "\xc6\x92"; + case 'g': return "\xc4\x9d"; + case 'h': return "\xd1\x9b"; + case 'i': return "\xcf\x8a"; + case 'j': return "\xc4\xb5"; + case 'k': return "\xc4\xb8"; + case 'l': return "\xc4\xba"; + case 'm': return "\xe1\xb8\xbf"; + case 'n': return "\xd0\xb8"; + case 'o': return "\xcf\x8c"; + case 'p': return "\xcf\x81"; + case 'q': return "\x51"; + case 'r': return "\xd2\x91"; + case 's': return "\xc5\xa1"; + case 't': return "\xd1\x82"; + case 'u': return "\xce\xb0"; + case 'v': return "\x56"; + case 'w': return "\xe1\xba\x85"; + case 'x': return "\xd1\x85"; + case 'y': return "\xe1\xbb\xb3"; + case 'z': return "\xc5\xba"; + case 'A': return "\xc3\x85"; + case 'B': return "\xce\xb2"; + case 'C': return "\xc4\x88"; + case 'D': return "\xc4\x90"; + case 'E': return "\xd0\x84"; + case 'F': return "\xce\x93"; + case 'G': return "\xc4\x9e"; + case 'H': return "\xc4\xa6"; + case 'I': return "\xd0\x87"; + case 'J': return "\xc4\xb5"; + case 'K': return "\xc4\xb6"; + case 'L': return "\xc5\x81"; + case 'M': return "\xe1\xb8\xbe"; + case 'N': return "\xc5\x83"; + case 'O': return "\xce\x98"; + case 'P': return "\xcf\x81"; + case 'Q': return "\x71"; + case 'R': return "\xd0\xaf"; + case 'S': return "\xc8\x98"; + case 'T': return "\xc5\xa6"; + case 'U': return "\xc5\xa8"; + case 'V': return "\xce\xbd"; + case 'W': return "\xe1\xba\x84"; + case 'X': return "\xc3\x97"; + case 'Y': return "\xc2\xa5"; + case 'Z': return "\xc5\xbd"; + default: return NULL; + } +} + +/** + * Converts characters so they look like they've been localized. + * + * Note: This leaves escape sequences untouched so they can later be + * processed by ResTable::collectString in the normal way. + */ +string +pseudolocalize_string(const string& source) +{ + const char* s = source.c_str(); + string result; + const size_t I = source.length(); + for (size_t i=0; i<I; i++) { + char c = s[i]; + if (c == '\\') { + if (i<I-1) { + result += '\\'; + i++; + c = s[i]; + switch (c) { + case 'u': + // this one takes up 5 chars + result += string(s+i, 5); + i += 4; + break; + case 't': + case 'n': + case '#': + case '@': + case '?': + case '"': + case '\'': + case '\\': + default: + result += c; + break; + } + } else { + result += c; + } + } else { + const char* p = pseudolocalize_char(c); + if (p != NULL) { + result += p; + } else { + result += c; + } + } + } + + //printf("result=\'%s\'\n", result.c_str()); + return result; +} + + diff --git a/tools/aapt/pseudolocalize.h b/tools/aapt/pseudolocalize.h new file mode 100644 index 000000000000..94cb034ac5ea --- /dev/null +++ b/tools/aapt/pseudolocalize.h @@ -0,0 +1,9 @@ +#ifndef HOST_PSEUDOLOCALIZE_H +#define HOST_PSEUDOLOCALIZE_H + +#include <string> + +std::string pseudolocalize_string(const std::string& source); + +#endif // HOST_PSEUDOLOCALIZE_H + diff --git a/tools/aapt/qsort_r_compat.c b/tools/aapt/qsort_r_compat.c new file mode 100644 index 000000000000..2a8dbe85f4c9 --- /dev/null +++ b/tools/aapt/qsort_r_compat.c @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2012 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. + */ + +#include <stdlib.h> +#include "qsort_r_compat.h" + +/* + * Note: This code is only used on the host, and is primarily here for + * Mac OS compatibility. Apparently, glibc and Apple's libc disagree on + * the parameter order for qsort_r. + */ + +#if HAVE_BSD_QSORT_R + +/* + * BSD qsort_r parameter order is as we have defined here. + */ + +void qsort_r_compat(void* base, size_t nel, size_t width, void* thunk, + int (*compar)(void*, const void* , const void*)) { + qsort_r(base, nel, width, thunk, compar); +} + +#elif HAVE_GNU_QSORT_R + +/* + * GNU qsort_r parameter order places the thunk parameter last. + */ + +struct compar_data { + void* thunk; + int (*compar)(void*, const void* , const void*); +}; + +static int compar_wrapper(const void* a, const void* b, void* data) { + struct compar_data* compar_data = (struct compar_data*)data; + return compar_data->compar(compar_data->thunk, a, b); +} + +void qsort_r_compat(void* base, size_t nel, size_t width, void* thunk, + int (*compar)(void*, const void* , const void*)) { + struct compar_data compar_data; + compar_data.thunk = thunk; + compar_data.compar = compar; + qsort_r(base, nel, width, compar_wrapper, &compar_data); +} + +#else + +/* + * Emulate qsort_r using thread local storage to access the thunk data. + */ + +#include <cutils/threads.h> + +static thread_store_t compar_data_key = THREAD_STORE_INITIALIZER; + +struct compar_data { + void* thunk; + int (*compar)(void*, const void* , const void*); +}; + +static int compar_wrapper(const void* a, const void* b) { + struct compar_data* compar_data = (struct compar_data*)thread_store_get(&compar_data_key); + return compar_data->compar(compar_data->thunk, a, b); +} + +void qsort_r_compat(void* base, size_t nel, size_t width, void* thunk, + int (*compar)(void*, const void* , const void*)) { + struct compar_data compar_data; + compar_data.thunk = thunk; + compar_data.compar = compar; + thread_store_set(&compar_data_key, &compar_data, NULL); + qsort(base, nel, width, compar_wrapper); +} + +#endif diff --git a/tools/aapt/qsort_r_compat.h b/tools/aapt/qsort_r_compat.h new file mode 100644 index 000000000000..e14f999f736a --- /dev/null +++ b/tools/aapt/qsort_r_compat.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 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. + */ + +/* + * Provides a portable version of qsort_r, called qsort_r_compat, which is a + * reentrant variant of qsort that passes a user data pointer to its comparator. + * This implementation follows the BSD parameter convention. + */ + +#ifndef ___QSORT_R_COMPAT_H +#define ___QSORT_R_COMPAT_H + +#include <stdlib.h> + +#ifdef __cplusplus +extern "C" { +#endif + +void qsort_r_compat(void* base, size_t nel, size_t width, void* thunk, + int (*compar)(void*, const void* , const void* )); + +#ifdef __cplusplus +} +#endif + +#endif // ___QSORT_R_COMPAT_H diff --git a/tools/aapt/tests/CrunchCache_test.cpp b/tools/aapt/tests/CrunchCache_test.cpp new file mode 100644 index 000000000000..20b5022b5257 --- /dev/null +++ b/tools/aapt/tests/CrunchCache_test.cpp @@ -0,0 +1,97 @@ +// +// Copyright 2011 The Android Open Source Project +// +#include <utils/String8.h> +#include <iostream> +#include <errno.h> + +#include "CrunchCache.h" +#include "FileFinder.h" +#include "MockFileFinder.h" +#include "CacheUpdater.h" +#include "MockCacheUpdater.h" + +using namespace android; +using std::cout; +using std::endl; + +void expectEqual(int got, int expected, const char* desc) { + cout << "Checking " << desc << ": "; + cout << "Got " << got << ", expected " << expected << "..."; + cout << ( (got == expected) ? "PASSED" : "FAILED") << endl; + errno += ((got == expected) ? 0 : 1); +} + +int main() { + + errno = 0; + + String8 source("res"); + String8 dest("res2"); + + // Create data for MockFileFinder to feed to the cache + KeyedVector<String8, time_t> sourceData; + // This shouldn't be updated + sourceData.add(String8("res/drawable/hello.png"),3); + // This should be updated + sourceData.add(String8("res/drawable/world.png"),5); + // This should cause make directory to be called + sourceData.add(String8("res/drawable-cool/hello.png"),3); + + KeyedVector<String8, time_t> destData; + destData.add(String8("res2/drawable/hello.png"),3); + destData.add(String8("res2/drawable/world.png"),3); + // this should call delete + destData.add(String8("res2/drawable/dead.png"),3); + + // Package up data and create mock file finder + KeyedVector<String8, KeyedVector<String8,time_t> > data; + data.add(source,sourceData); + data.add(dest,destData); + FileFinder* ff = new MockFileFinder(data); + CrunchCache cc(source,dest,ff); + + MockCacheUpdater* mcu = new MockCacheUpdater(); + CacheUpdater* cu(mcu); + + cout << "Running Crunch..."; + int result = cc.crunch(cu); + cout << ((result > 0) ? "PASSED" : "FAILED") << endl; + errno += ((result > 0) ? 0 : 1); + + const int EXPECTED_RESULT = 2; + expectEqual(result, EXPECTED_RESULT, "number of files touched"); + + cout << "Checking calls to deleteFile and processImage:" << endl; + const int EXPECTED_DELETES = 1; + const int EXPECTED_PROCESSED = 2; + // Deletes + expectEqual(mcu->deleteCount, EXPECTED_DELETES, "deleteFile"); + // processImage + expectEqual(mcu->processCount, EXPECTED_PROCESSED, "processImage"); + + const int EXPECTED_OVERWRITES = 3; + result = cc.crunch(cu, true); + expectEqual(result, EXPECTED_OVERWRITES, "number of files touched with overwrite"); + \ + + if (errno == 0) + cout << "ALL TESTS PASSED!" << endl; + else + cout << errno << " TESTS FAILED" << endl; + + delete ff; + delete cu; + + // TESTS BELOW WILL GO AWAY SOON + + String8 source2("ApiDemos/res"); + String8 dest2("ApiDemos/res2"); + + FileFinder* sff = new SystemFileFinder(); + CacheUpdater* scu = new SystemCacheUpdater(); + + CrunchCache scc(source2,dest2,sff); + + scc.crunch(scu); +}
\ No newline at end of file diff --git a/tools/aapt/tests/FileFinder_test.cpp b/tools/aapt/tests/FileFinder_test.cpp new file mode 100644 index 000000000000..07bd665bcdb5 --- /dev/null +++ b/tools/aapt/tests/FileFinder_test.cpp @@ -0,0 +1,101 @@ +// +// Copyright 2011 The Android Open Source Project +// +#include <utils/Vector.h> +#include <utils/KeyedVector.h> +#include <iostream> +#include <cassert> +#include <utils/String8.h> +#include <utility> + +#include "DirectoryWalker.h" +#include "MockDirectoryWalker.h" +#include "FileFinder.h" + +using namespace android; + +using std::pair; +using std::cout; +using std::endl; + + + +int main() +{ + + cout << "\n\n STARTING FILE FINDER TESTS" << endl; + String8 path("ApiDemos"); + + // Storage to pass to findFiles() + KeyedVector<String8,time_t> testStorage; + + // Mock Directory Walker initialization. First data, then sdw + Vector< pair<String8,time_t> > data; + data.push( pair<String8,time_t>(String8("hello.png"),3) ); + data.push( pair<String8,time_t>(String8("world.PNG"),3) ); + data.push( pair<String8,time_t>(String8("foo.pNg"),3) ); + // Neither of these should be found + data.push( pair<String8,time_t>(String8("hello.jpg"),3) ); + data.push( pair<String8,time_t>(String8(".hidden.png"),3)); + + DirectoryWalker* sdw = new StringDirectoryWalker(path,data); + + // Extensions to look for + Vector<String8> exts; + exts.push(String8(".png")); + + errno = 0; + + // Make sure we get a valid mock directory walker + // Make sure we finish without errors + cout << "Checking DirectoryWalker..."; + assert(sdw != NULL); + cout << "PASSED" << endl; + + // Make sure we finish without errors + cout << "Running findFiles()..."; + bool findStatus = FileFinder::findFiles(path,exts, testStorage, sdw); + assert(findStatus); + cout << "PASSED" << endl; + + const size_t SIZE_EXPECTED = 3; + // Check to make sure we have the right number of things in our storage + cout << "Running size comparison: Size is " << testStorage.size() << ", "; + cout << "Expected " << SIZE_EXPECTED << "..."; + if(testStorage.size() == SIZE_EXPECTED) + cout << "PASSED" << endl; + else { + cout << "FAILED" << endl; + errno++; + } + + // Check to make sure that each of our found items has the right extension + cout << "Checking Returned Extensions..."; + bool extsOkay = true; + String8 wrongExts; + for (size_t i = 0; i < SIZE_EXPECTED; ++i) { + String8 testExt(testStorage.keyAt(i).getPathExtension()); + testExt.toLower(); + if (testExt != ".png") { + wrongExts += testStorage.keyAt(i); + wrongExts += "\n"; + extsOkay = false; + } + } + if (extsOkay) + cout << "PASSED" << endl; + else { + cout << "FAILED" << endl; + cout << "The following extensions didn't check out" << endl << wrongExts; + } + + // Clean up + delete sdw; + + if(errno == 0) { + cout << "ALL TESTS PASSED" << endl; + } else { + cout << errno << " TESTS FAILED" << endl; + } + return errno; +}
\ No newline at end of file diff --git a/tools/aapt/tests/MockCacheUpdater.h b/tools/aapt/tests/MockCacheUpdater.h new file mode 100644 index 000000000000..c7f4bd7359a1 --- /dev/null +++ b/tools/aapt/tests/MockCacheUpdater.h @@ -0,0 +1,40 @@ +// +// Copyright 2011 The Android Open Source Project +// +#ifndef MOCKCACHEUPDATER_H +#define MOCKCACHEUPDATER_H + +#include <utils/String8.h> +#include "CacheUpdater.h" + +using namespace android; + +class MockCacheUpdater : public CacheUpdater { +public: + + MockCacheUpdater() + : deleteCount(0), processCount(0) { }; + + // Make sure all the directories along this path exist + virtual void ensureDirectoriesExist(String8 path) + { + // Nothing to do + }; + + // Delete a file + virtual void deleteFile(String8 path) { + deleteCount++; + }; + + // Process an image from source out to dest + virtual void processImage(String8 source, String8 dest) { + processCount++; + }; + + // DATA MEMBERS + int deleteCount; + int processCount; +private: +}; + +#endif // MOCKCACHEUPDATER_H
\ No newline at end of file diff --git a/tools/aapt/tests/MockDirectoryWalker.h b/tools/aapt/tests/MockDirectoryWalker.h new file mode 100644 index 000000000000..5900cf3d8b0a --- /dev/null +++ b/tools/aapt/tests/MockDirectoryWalker.h @@ -0,0 +1,85 @@ +// +// Copyright 2011 The Android Open Source Project +// +#ifndef MOCKDIRECTORYWALKER_H +#define MOCKDIRECTORYWALKER_H + +#include <utils/Vector.h> +#include <utils/String8.h> +#include <utility> +#include "DirectoryWalker.h" + +using namespace android; +using std::pair; + +// String8 Directory Walker +// This is an implementation of the Directory Walker abstraction that is built +// for testing. +// Instead of system calls it queries a private data structure for the directory +// entries. It takes a path and a map of filenames and their modification times. +// functions are inlined since they are short and simple + +class StringDirectoryWalker : public DirectoryWalker { +public: + StringDirectoryWalker(String8& path, Vector< pair<String8,time_t> >& data) + : mPos(0), mBasePath(path), mData(data) { + //fprintf(stdout,"StringDW built to mimic %s with %d files\n", + // mBasePath.string()); + }; + // Default copy constructor, and destructor are fine + + virtual bool openDir(String8 path) { + // If the user is trying to query the "directory" that this + // walker was initialized with, then return success. Else fail. + return path == mBasePath; + }; + virtual bool openDir(const char* path) { + String8 p(path); + openDir(p); + return true; + }; + // Advance to next entry in the Vector + virtual struct dirent* nextEntry() { + // Advance position and check to see if we're done + if (mPos >= mData.size()) + return NULL; + + // Place data in the entry descriptor. This class only returns files. + mEntry.d_type = DT_REG; + mEntry.d_ino = mPos; + // Copy chars from the string name to the entry name + size_t i = 0; + for (i; i < mData[mPos].first.size(); ++i) + mEntry.d_name[i] = mData[mPos].first[i]; + mEntry.d_name[i] = '\0'; + + // Place data in stats + mStats.st_ino = mPos; + mStats.st_mtime = mData[mPos].second; + + // Get ready to move to the next entry + mPos++; + + return &mEntry; + }; + // Get the stats for the current entry + virtual struct stat* entryStats() { + return &mStats; + }; + // Nothing to do in clean up + virtual void closeDir() { + // Nothing to do + }; + virtual DirectoryWalker* clone() { + return new StringDirectoryWalker(*this); + }; +private: + // Current position in the Vector + size_t mPos; + // Base path + String8 mBasePath; + // Data to simulate a directory full of files. + Vector< pair<String8,time_t> > mData; +}; + +#endif // MOCKDIRECTORYWALKER_H
\ No newline at end of file diff --git a/tools/aapt/tests/MockFileFinder.h b/tools/aapt/tests/MockFileFinder.h new file mode 100644 index 000000000000..da5ea4fcd3a2 --- /dev/null +++ b/tools/aapt/tests/MockFileFinder.h @@ -0,0 +1,55 @@ +// +// Copyright 2011 The Android Open Source Project +// + +#ifndef MOCKFILEFINDER_H +#define MOCKFILEFINDER_H + +#include <utils/Vector.h> +#include <utils/KeyedVector.h> +#include <utils/String8.h> + +#include "DirectoryWalker.h" + +using namespace android; + +class MockFileFinder : public FileFinder { +public: + MockFileFinder (KeyedVector<String8, KeyedVector<String8,time_t> >& files) + : mFiles(files) + { + // Nothing left to do + }; + + /** + * findFiles implementation for the abstraction. + * PRECONDITIONS: + * No checking is done, so there MUST be an entry in mFiles with + * path matching basePath. + * + * POSTCONDITIONS: + * fileStore is filled with a copy of the data in mFiles corresponding + * to the basePath. + */ + + virtual bool findFiles(String8 basePath, Vector<String8>& extensions, + KeyedVector<String8,time_t>& fileStore, + DirectoryWalker* dw) + { + const KeyedVector<String8,time_t>* payload(&mFiles.valueFor(basePath)); + // Since KeyedVector doesn't implement swap + // (who doesn't use swap??) we loop and add one at a time. + for (size_t i = 0; i < payload->size(); ++i) { + fileStore.add(payload->keyAt(i),payload->valueAt(i)); + } + return true; + } + +private: + // Virtual mapping between "directories" and the "files" contained + // in them + KeyedVector<String8, KeyedVector<String8,time_t> > mFiles; +}; + + +#endif // MOCKFILEFINDER_H
\ No newline at end of file diff --git a/tools/aapt/tests/plurals/AndroidManifest.xml b/tools/aapt/tests/plurals/AndroidManifest.xml new file mode 100644 index 000000000000..c721dee98256 --- /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 000000000000..1c1fc19ebf95 --- /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 000000000000..4d39e10bd313 --- /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 100644 index 000000000000..bfa67656b323 --- /dev/null +++ b/tools/aidl/AST.cpp @@ -0,0 +1,912 @@ +#include "AST.h" +#include "Type.h" + +void +WriteModifiers(FILE* to, int mod, int mask) +{ + int m = mod & mask; + + if (m & OVERRIDE) { + fprintf(to, "@Override "); + } + + 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 | OVERRIDE); + 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()); +} + +StringLiteralExpression::StringLiteralExpression(const string& v) + :value(v) +{ +} + +StringLiteralExpression::~StringLiteralExpression() +{ +} + +void +StringLiteralExpression::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(const string& n, int argc = 0, ...) + :obj(NULL), + clazz(NULL), + name(n) +{ + va_list args; + va_start(args, argc); + init(argc, args); + va_end(args); +} + +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(Type* t, int argc = 0, ...) + :type(t) +{ + va_list args; + va_start(args, argc); + init(argc, args); + va_end(args); +} + +NewExpression::~NewExpression() +{ +} + +void +NewExpression::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 +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"); +} + +Break::Break() +{ +} + +Break::~Break() +{ +} + +void +Break::Write(FILE* to) +{ + fprintf(to, "break;\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 | ABSTRACT | FINAL | OVERRIDE); + + 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()); + } + + 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 100644 index 000000000000..ead5e7ae3439 --- /dev/null +++ b/tools/aidl/AST.h @@ -0,0 +1,371 @@ +#ifndef AIDL_AST_H +#define AIDL_AST_H + +#include <string> +#include <vector> +#include <set> +#include <stdarg.h> +#include <stdio.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, + + OVERRIDE = 0x00000100, + + 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); +}; + +// TODO: also escape the contents. not needed for now +struct StringLiteralExpression : public Expression +{ + string value; + + StringLiteralExpression(const string& value); + virtual ~StringLiteralExpression(); + 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 : public Statement +{ + 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(const string& name, int argc, ...); + 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); + NewExpression(Type* type, int argc, ...); + virtual ~NewExpression(); + virtual void Write(FILE* to); + +private: + void init(int n, va_list args); +}; + +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 Break : public Statement +{ + Break(); + virtual ~Break(); + 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 000000000000..77d46abf4881 --- /dev/null +++ b/tools/aidl/Android.mk @@ -0,0 +1,29 @@ +# Copyright 2007 The Android Open Source Project +# +# Copies files into the directory structure described by a manifest + +# This tool is prebuilt if we're doing an app-only build. +ifeq ($(TARGET_BUILD_APPS),) + +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 \ + generate_java_binder.cpp \ + generate_java_rpc.cpp + +LOCAL_CFLAGS := -g +LOCAL_MODULE := aidl + +include $(BUILD_HOST_EXECUTABLE) + +endif # TARGET_BUILD_APPS diff --git a/tools/aidl/NOTICE b/tools/aidl/NOTICE new file mode 100644 index 000000000000..c5b1efa7aac7 --- /dev/null +++ b/tools/aidl/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-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. + + 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. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/tools/aidl/Type.cpp b/tools/aidl/Type.cpp new file mode 100644 index 000000000000..d572af6d2aab --- /dev/null +++ b/tools/aidl/Type.cpp @@ -0,0 +1,1440 @@ +#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* OBJECT_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* CONTEXT_TYPE; +Type* MAP_TYPE; +Type* LIST_TYPE; +Type* CLASSLOADER_TYPE; +Type* RPC_DATA_TYPE; +Type* RPC_ERROR_TYPE; +Type* EVENT_FAKE_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", + "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", + "putByte", "getByte", "putByteArray", "createByteArray", "getByteArray"); + NAMES.Add(BYTE_TYPE); + + CHAR_TYPE = new CharType(); + NAMES.Add(CHAR_TYPE); + + INT_TYPE = new BasicType("int", + "writeInt", "readInt", "writeIntArray", "createIntArray", "readIntArray", + "putInteger", "getInteger", "putIntegerArray", "createIntegerArray", "getIntegerArray"); + NAMES.Add(INT_TYPE); + + LONG_TYPE = new BasicType("long", + "writeLong", "readLong", "writeLongArray", "createLongArray", "readLongArray", + "putLong", "getLong", "putLongArray", "createLongArray", "getLongArray"); + NAMES.Add(LONG_TYPE); + + FLOAT_TYPE = new BasicType("float", + "writeFloat", "readFloat", "writeFloatArray", "createFloatArray", "readFloatArray", + "putFloat", "getFloat", "putFloatArray", "createFloatArray", "getFloatArray"); + NAMES.Add(FLOAT_TYPE); + + DOUBLE_TYPE = new BasicType("double", + "writeDouble", "readDouble", "writeDoubleArray", "createDoubleArray", "readDoubleArray", + "putDouble", "getDouble", "putDoubleArray", "createDoubleArray", "getDoubleArray"); + NAMES.Add(DOUBLE_TYPE); + + STRING_TYPE = new StringType(); + NAMES.Add(STRING_TYPE); + + OBJECT_TYPE = new Type("java.lang", "Object", Type::BUILT_IN, false, false, false); + NAMES.Add(OBJECT_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, 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); + + CONTEXT_TYPE = new Type("android.content", "Context", Type::BUILT_IN, false, false, false); + NAMES.Add(CONTEXT_TYPE); + + RPC_DATA_TYPE = new RpcDataType(); + NAMES.Add(RPC_DATA_TYPE); + + RPC_ERROR_TYPE = new UserDataType("android.support.place.rpc", "RpcError", + true, __FILE__, __LINE__); + NAMES.Add(RPC_ERROR_TYPE); + + EVENT_FAKE_TYPE = new Type("event", Type::BUILT_IN, false, false, false); + NAMES.Add(EVENT_FAKE_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 canWriteToRpcData, + bool canBeOut) + :m_package(), + m_name(name), + m_declFile(""), + m_declLine(-1), + m_kind(kind), + m_canWriteToParcel(canWriteToParcel), + m_canWriteToRpcData(canWriteToRpcData), + m_canBeOut(canBeOut) +{ + m_qualifiedName = name; +} + +Type::Type(const string& package, const string& name, + int kind, bool canWriteToParcel, bool canWriteToRpcData, + 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_canWriteToRpcData(canWriteToRpcData), + 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::RpcCreatorName() 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, Variable**) +{ + 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, Variable**) +{ + 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, Variable**) +{ + 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, Variable**) +{ + 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::WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags) +{ + fprintf(stderr, "aidl:internal error %s:%d qualifiedName=%s\n", + __FILE__, __LINE__, m_qualifiedName.c_str()); + addTo->Add(new LiteralExpression("/* WriteToRpcData error " + + m_qualifiedName + " */")); +} + +void +Type::CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, Variable* data, + Variable** cl) +{ + fprintf(stderr, "aidl:internal error %s:%d qualifiedName=%s\n", + __FILE__, __LINE__, m_qualifiedName.c_str()); + addTo->Add(new LiteralExpression("/* ReadFromRpcData 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& marshallParcel, + const string& unmarshallParcel, const string& writeArrayParcel, + const string& createArrayParcel, const string& readArrayParcel, + const string& marshallRpc, const string& unmarshallRpc, + const string& writeArrayRpc, const string& createArrayRpc, const string& readArrayRpc) + :Type(name, BUILT_IN, true, true, false), + m_marshallParcel(marshallParcel), + m_unmarshallParcel(unmarshallParcel), + m_writeArrayParcel(writeArrayParcel), + m_createArrayParcel(createArrayParcel), + m_readArrayParcel(readArrayParcel), + m_marshallRpc(marshallRpc), + m_unmarshallRpc(unmarshallRpc), + m_writeArrayRpc(writeArrayRpc), + m_createArrayRpc(createArrayRpc), + m_readArrayRpc(readArrayRpc) +{ +} + +void +BasicType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, m_marshallParcel, 1, v)); +} + +void +BasicType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel, Variable**) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, m_unmarshallParcel))); +} + +bool +BasicType::CanBeArray() const +{ + return true; +} + +void +BasicType::WriteArrayToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, m_writeArrayParcel, 1, v)); +} + +void +BasicType::CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable**) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, m_createArrayParcel))); +} + +void +BasicType::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel, Variable**) +{ + addTo->Add(new MethodCall(parcel, m_readArrayParcel, 1, v)); +} + +void +BasicType::WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags) +{ + addTo->Add(new MethodCall(data, m_marshallRpc, 2, k, v)); +} + +void +BasicType::CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, Variable* data, + Variable** cl) +{ + addTo->Add(new Assignment(v, new MethodCall(data, m_unmarshallRpc, 1, k))); +} + +// ================================================================ + +BooleanType::BooleanType() + :Type("boolean", BUILT_IN, true, 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, Variable**) +{ + 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, Variable**) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, "createBooleanArray"))); +} + +void +BooleanType::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel, Variable**) +{ + addTo->Add(new MethodCall(parcel, "readBooleanArray", 1, v)); +} + +void +BooleanType::WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags) +{ + addTo->Add(new MethodCall(data, "putBoolean", 2, k, v)); +} + +void +BooleanType::CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, Variable* data, + Variable** cl) +{ + addTo->Add(new Assignment(v, new MethodCall(data, "getBoolean", 1, k))); +} + +// ================================================================ + +CharType::CharType() + :Type("char", BUILT_IN, true, 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, Variable**) +{ + 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, Variable**) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, "createCharArray"))); +} + +void +CharType::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel, Variable**) +{ + addTo->Add(new MethodCall(parcel, "readCharArray", 1, v)); +} + +void +CharType::WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags) +{ + addTo->Add(new MethodCall(data, "putChar", 2, k, v)); +} + +void +CharType::CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, Variable* data, + Variable** cl) +{ + addTo->Add(new Assignment(v, new MethodCall(data, "getChar", 1, k))); +} + +// ================================================================ + +StringType::StringType() + :Type("java.lang", "String", BUILT_IN, true, 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, Variable**) +{ + 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, Variable**) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, "createStringArray"))); +} + +void +StringType::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel, Variable**) +{ + addTo->Add(new MethodCall(parcel, "readStringArray", 1, v)); +} + +void +StringType::WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags) +{ + addTo->Add(new MethodCall(data, "putString", 2, k, v)); +} + +void +StringType::CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, Variable**) +{ + addTo->Add(new Assignment(v, new MethodCall(data, "getString", 1, k))); +} + +// ================================================================ + +CharSequenceType::CharSequenceType() + :Type("java.lang", "CharSequence", BUILT_IN, true, 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, Variable**) +{ + // 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, 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, Variable**) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +// ================================================================ + +RuntimeExceptionType::RuntimeExceptionType() + :Type("java.lang", "RuntimeException", BUILT_IN, false, 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, Variable**) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + + +// ================================================================ + +IBinderType::IBinderType() + :Type("android.os", "IBinder", BUILT_IN, true, false, 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, Variable**) +{ + 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, Variable**) +{ + addTo->Add(new Assignment(v, new MethodCall(parcel, "createBinderArray"))); +} + +void +IBinderType::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel, Variable**) +{ + addTo->Add(new MethodCall(parcel, "readBinderArray", 1, v)); +} + + +// ================================================================ + +IInterfaceType::IInterfaceType() + :Type("android.os", "IInterface", BUILT_IN, false, 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, Variable**) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + + +// ================================================================ + +BinderType::BinderType() + :Type("android.os", "Binder", BUILT_IN, false, 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, Variable**) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + + +// ================================================================ + +BinderProxyType::BinderProxyType() + :Type("android.os", "BinderProxy", BUILT_IN, false, 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, Variable**) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + + +// ================================================================ + +ParcelType::ParcelType() + :Type("android.os", "Parcel", BUILT_IN, false, 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, Variable**) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +// ================================================================ + +ParcelableInterfaceType::ParcelableInterfaceType() + :Type("android.os", "Parcelable", BUILT_IN, false, 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, Variable**) +{ + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, __LINE__); +} + +// ================================================================ + +MapType::MapType() + :Type("java.util", "Map", BUILT_IN, true, false, true) +{ +} + +void +MapType::WriteToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeMap", 1, v)); +} + +static void EnsureClassLoader(StatementBlock* addTo, Variable** cl) +{ + // We don't want to look up the class loader once for every + // collection argument, so ensure we do it at most once per method. + if (*cl == NULL) { + *cl = new Variable(CLASSLOADER_TYPE, "cl"); + addTo->Add(new VariableDeclaration(*cl, + new LiteralExpression("this.getClass().getClassLoader()"), + CLASSLOADER_TYPE)); + } +} + +void +MapType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel, Variable** cl) +{ + EnsureClassLoader(addTo, cl); + addTo->Add(new Assignment(v, new MethodCall(parcel, "readHashMap", 1, *cl))); +} + +void +MapType::ReadFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel, Variable** cl) +{ + EnsureClassLoader(addTo, cl); + addTo->Add(new MethodCall(parcel, "readMap", 2, v, *cl)); +} + + +// ================================================================ + +ListType::ListType() + :Type("java.util", "List", BUILT_IN, true, 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) +{ + EnsureClassLoader(addTo, cl); + addTo->Add(new Assignment(v, new MethodCall(parcel, "readArrayList", 1, *cl))); +} + +void +ListType::ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl) +{ + EnsureClassLoader(addTo, cl); + addTo->Add(new MethodCall(parcel, "readList", 2, v, *cl)); +} + +void +ListType::WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags) +{ + addTo->Add(new MethodCall(data, "putList", 2, k, v)); +} + +void +ListType::CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, Variable* data, + Variable** cl) +{ + addTo->Add(new Assignment(v, new MethodCall(data, "getList", 1, k))); +} + +// ================================================================ + +UserDataType::UserDataType(const string& package, const string& name, + bool builtIn, bool canWriteToParcel, bool canWriteToRpcData, + const string& declFile, int declLine) + :Type(package, name, builtIn ? BUILT_IN : USERDATA, canWriteToParcel, canWriteToRpcData, + true, declFile, declLine) +{ +} + +string +UserDataType::CreatorName() const +{ + return QualifiedName() + ".CREATOR"; +} + +string +UserDataType::RpcCreatorName() const +{ + return QualifiedName() + ".RPC_CREATOR"; +} + +void +UserDataType::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 +UserDataType::CreateFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel, Variable**) +{ + // 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 +UserDataType::ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable**) +{ + // 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 +UserDataType::CanBeArray() const +{ + return true; +} + +void +UserDataType::WriteArrayToParcel(StatementBlock* addTo, Variable* v, Variable* parcel, int flags) +{ + addTo->Add(new MethodCall(parcel, "writeTypedArray", 2, v, + BuildWriteToParcelFlags(flags))); +} + +void +UserDataType::CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable**) +{ + string creator = v->type->QualifiedName() + ".CREATOR"; + addTo->Add(new Assignment(v, new MethodCall(parcel, + "createTypedArray", 1, new LiteralExpression(creator)))); +} + +void +UserDataType::ReadArrayFromParcel(StatementBlock* addTo, Variable* v, Variable* parcel, Variable**) +{ + string creator = v->type->QualifiedName() + ".CREATOR"; + addTo->Add(new MethodCall(parcel, "readTypedArray", 2, + v, new LiteralExpression(creator))); +} + +void +UserDataType::WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags) +{ + // data.putFlattenable(k, v); + addTo->Add(new MethodCall(data, "putFlattenable", 2, k, v)); +} + +void +UserDataType::CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, Variable** cl) +{ + // data.getFlattenable(k, CLASS.RPC_CREATOR); + addTo->Add(new Assignment(v, new MethodCall(data, "getFlattenable", 2, k, + new FieldVariable(v->type, "RPC_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, 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, Variable**) +{ + // 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, 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); +} + +const vector<Type*>& +GenericType::GenericArgumentTypes() const +{ + return m_args; +} + +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, Variable**) +{ + fprintf(stderr, "implement GenericType::CreateFromParcel\n"); +} + +void +GenericType::ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable**) +{ + 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, Variable**) +{ + 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, Variable**) +{ + 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))); + } +} + +void +GenericListType::WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags) +{ + Type* generic = GenericArgumentTypes()[0]; + if (generic == RPC_DATA_TYPE) { + addTo->Add(new MethodCall(data, "putRpcDataList", 2, k, v)); + } else if (generic->RpcCreatorName() != "") { + addTo->Add(new MethodCall(data, "putFlattenableList", 2, k, v)); + } else { + addTo->Add(new MethodCall(data, "putList", 2, k, v)); + } +} + +void +GenericListType::CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, Variable** cl) +{ + Type* generic = GenericArgumentTypes()[0]; + if (generic == RPC_DATA_TYPE) { + addTo->Add(new Assignment(v, new MethodCall(data, "getRpcDataList", 2, k))); + } else if (generic->RpcCreatorName() != "") { + addTo->Add(new Assignment(v, new MethodCall(data, "getFlattenableList", 2, k, + new LiteralExpression(generic->RpcCreatorName())))); + } else { + string classArg = GenericArgumentTypes()[0]->QualifiedName(); + classArg += ".class"; + addTo->Add(new Assignment(v, new MethodCall(data, "getList", 2, k, + new LiteralExpression(classArg)))); + } +} + + +// ================================================================ + +RpcDataType::RpcDataType() + :UserDataType("android.support.place.rpc", "RpcData", true, true, true) +{ +} + +void +RpcDataType::WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags) +{ + addTo->Add(new MethodCall(data, "putRpcData", 2, k, v)); +} + +void +RpcDataType::CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, Variable* data, + Variable** cl) +{ + addTo->Add(new Assignment(v, new MethodCall(data, "getRpcData", 1, k))); +} + + +// ================================================================ + +ClassLoaderType::ClassLoaderType() + :Type("java.lang", "ClassLoader", BUILT_IN, false, 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 100644 index 000000000000..ae12720142e8 --- /dev/null +++ b/tools/aidl/Type.h @@ -0,0 +1,542 @@ +#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, + USERDATA, + INTERFACE, + GENERATED + }; + + // WriteToParcel flags + enum { + PARCELABLE_WRITE_RETURN_VALUE = 0x0001 + }; + + Type(const string& name, int kind, bool canWriteToParcel, + bool canWriteToRpcData, bool canBeOut); + Type(const string& package, const string& name, + int kind, bool canWriteToParcel, bool canWriteToRpcData, 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 CanWriteToParcel() const { return m_canWriteToParcel; } + inline bool CanWriteToRpcData() const { return m_canWriteToRpcData; } + inline bool CanBeOutParameter() const { return m_canBeOut; } + + virtual string ImportType() const; + virtual string CreatorName() const; + virtual string RpcCreatorName() 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, Variable** cl); + virtual void ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + + virtual bool CanBeArray() const; + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + + virtual void WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags); + virtual void CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, Variable** cl); + +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_canWriteToRpcData; + bool m_canBeOut; +}; + +class BasicType : public Type +{ +public: + BasicType(const string& name, + const string& marshallParcel, + const string& unmarshallParcel, + const string& writeArrayParcel, + const string& createArrayParcel, + const string& readArrayParcel, + const string& marshallRpc, + const string& unmarshallRpc, + const string& writeArrayRpc, + const string& createArrayRpc, + const string& readArrayRpc); + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + + virtual bool CanBeArray() const; + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + + virtual void WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags); + virtual void CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, Variable** cl); + +private: + string m_marshallParcel; + string m_unmarshallParcel; + string m_writeArrayParcel; + string m_createArrayParcel; + string m_readArrayParcel; + string m_marshallRpc; + string m_unmarshallRpc; + string m_writeArrayRpc; + string m_createArrayRpc; + string m_readArrayRpc; +}; + +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, Variable** cl); + + virtual bool CanBeArray() const; + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + + virtual void WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags); + virtual void CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, Variable** cl); +}; + +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, Variable** cl); + + virtual bool CanBeArray() const; + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + + virtual void WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags); + virtual void CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, Variable** cl); +}; + + +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, Variable** cl); + + virtual bool CanBeArray() const; + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + + virtual void WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags); + virtual void CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, Variable** cl); +}; + +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, Variable** cl); +}; + +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, Variable** cl); +}; + +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, Variable** cl); +}; + +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, Variable** cl); + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); +}; + +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, Variable** cl); +}; + +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, Variable** cl); +}; + +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, Variable** cl); +}; + +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, Variable** cl); +}; + +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, Variable** cl); +}; + +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, Variable** cl); + virtual void ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); +}; + +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, Variable** cl); + virtual void ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + + virtual void WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags); + virtual void CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, Variable** cl); +}; + +class UserDataType : public Type +{ +public: + UserDataType(const string& package, const string& name, + bool builtIn, bool canWriteToParcel, bool canWriteToRpcData, + const string& declFile = "", int declLine = -1); + + virtual string CreatorName() const; + virtual string RpcCreatorName() const; + + virtual void WriteToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + virtual void ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + + virtual bool CanBeArray() const; + + virtual void WriteArrayToParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, int flags); + virtual void CreateArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + virtual void ReadArrayFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + + virtual void WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags); + virtual void CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, Variable** cl); +}; + +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, Variable** cl); + +private: + bool m_oneway; +}; + + +class GenericType : public Type +{ +public: + GenericType(const string& package, const string& name, + const vector<Type*>& args); + + const vector<Type*>& GenericArgumentTypes() const; + 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, Variable** cl); + virtual void ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + +private: + string m_genericArguments; + string m_importName; + vector<Type*> m_args; +}; + +class RpcDataType : public UserDataType +{ +public: + RpcDataType(); + + virtual void WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags); + virtual void CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, Variable** cl); +}; + +class ClassLoaderType : public Type +{ +public: + ClassLoaderType(); +}; + +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, Variable** cl); + virtual void ReadFromParcel(StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl); + + virtual void WriteToRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, int flags); + virtual void CreateFromRpcData(StatementBlock* addTo, Expression* k, Variable* v, + Variable* data, Variable** cl); + +private: + string m_creator; +}; + +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* BYTE_TYPE; +extern Type* CHAR_TYPE; +extern Type* INT_TYPE; +extern Type* LONG_TYPE; +extern Type* FLOAT_TYPE; +extern Type* DOUBLE_TYPE; +extern Type* OBJECT_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 Type* CONTEXT_TYPE; + +extern Type* RPC_DATA_TYPE; +extern Type* RPC_ERROR_TYPE; +extern Type* RPC_CONTEXT_TYPE; +extern Type* EVENT_FAKE_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 000000000000..b8a48033ede5 --- /dev/null +++ b/tools/aidl/aidl.cpp @@ -0,0 +1,1155 @@ + +#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 + +// The following are gotten as the offset from the allowable id's between +// android.os.IBinder.FIRST_CALL_TRANSACTION=1 and +// android.os.IBinder.LAST_CALL_TRANSACTION=16777215 +#define MIN_USER_SET_METHOD_ID 0 +#define MAX_USER_SET_METHOD_ID 16777214 + +using namespace std; + +static void +test_document(document_item_type* d) +{ + while (d) { + if (d->item_type == INTERFACE_TYPE_BINDER) { + 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 == USER_DATA_TYPE) { + user_data_type* b = (user_data_type*)d; + if ((b->flattening_methods & PARCELABLE_DATA) != 0) { + printf("parcelable %s %s;\n", b->package, b->name.data); + } + if ((b->flattening_methods & RPC_DATA) != 0) { + printf("flattenable %s %s;\n", b->package, b->name.data); + } + } + else { + printf("UNKNOWN d=0x%08lx d->item_type=%d\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 == USER_DATA_TYPE) { + user_data_type* p = (user_data_type*)items; + err |= check_filename(filename, p->package, &p->name); + } + else if (items->item_type == INTERFACE_TYPE_BINDER + || items->item_type == INTERFACE_TYPE_RPC) { + 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::USERDATA: + return "a user data"; + 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 == USER_DATA_TYPE) { + user_data_type* p = (user_data_type*)items; + type = new UserDataType(p->package ? p->package : "", p->name.data, + false, ((p->flattening_methods & PARCELABLE_DATA) != 0), + ((p->flattening_methods & RPC_DATA) != 0), filename, p->name.lineno); + } + else if (items->item_type == INTERFACE_TYPE_BINDER + || items->item_type == INTERFACE_TYPE_RPC) { + 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_BINDER) { + // 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, 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, false, + filename, c->name.lineno); + NAMES.Add(proxy); + } + else if (items->item_type == INTERFACE_TYPE_RPC) { + // for interfaces, also add the service base type, 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 += ".ServiceBase"; + Type* base = new Type(c->package ? c->package : "", + name, Type::GENERATED, false, false, false, + filename, c->name.lineno); + NAMES.Add(base); + } + } 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, int kind, 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 == EVENT_FAKE_TYPE) { + if (kind != INTERFACE_TYPE_RPC) { + fprintf(stderr, "%s:%d event methods only supported for rpc interfaces\n", + filename, m->type.type.lineno); + err = 1; + } + } else { + if (!(kind == INTERFACE_TYPE_BINDER ? returnType->CanWriteToParcel() + : returnType->CanWriteToRpcData())) { + 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 == EVENT_FAKE_TYPE) { + fprintf(stderr, "%s:%d parameter %s (%d) event can not be used as a parameter %s\n", + filename, m->type.type.lineno, arg->name.data, index, + arg->type.type.data); + err = 1; + goto next; + } + + if (!(kind == INTERFACE_TYPE_BINDER ? t->CanWriteToParcel() : t->CanWriteToRpcData())) { + 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 (returnType == EVENT_FAKE_TYPE + && convert_direction(arg->direction.data) != IN_PARAMETER) { + fprintf(stderr, "%s:%d parameter %d: '%s %s' All paremeters on events must be 'in'.\n", + filename, m->type.type.lineno, index, + arg->type.type.data, arg->name.data); + err = 1; + goto next; + } + + 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 or aidl 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 USER_DATA_TYPE) + if (items->item_type == INTERFACE_TYPE_BINDER + || items->item_type == INTERFACE_TYPE_RPC) { + 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, items->item_type, 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; + // Allow parcelables to skip the "one-only" rule. + if (items->next != NULL && next->item_type != USER_DATA_TYPE) { + int lineno = -1; + if (next->item_type == INTERFACE_TYPE_BINDER) { + lineno = ((interface_type*)next)->interface_token.lineno; + } + else if (next->item_type == INTERFACE_TYPE_RPC) { + lineno = ((interface_type*)next)->interface_token.lineno; + } + fprintf(stderr, "%s:%d aidl can only handle one interface per file\n", + filename, lineno); + return 1; + } + + if (items->item_type == USER_DATA_TYPE) { + *onlyParcelable = true; + if (options.failOnParcelable) { + fprintf(stderr, "%s:%d aidl can only generate code for interfaces, not" + " parcelables or flattenables,\n", filename, + ((user_data_type*)items)->keyword_token.lineno); + fprintf(stderr, "%s:%d .aidl files that only declare parcelables or flattenables" + "may not go in the Makefile.\n", filename, + ((user_data_type*)items)->keyword_token.lineno); + return 1; + } + } else { + *onlyParcelable = false; + } + + return 0; +} + +// ========================================================== +void +generate_dep_file(const Options& options, const document_item_type* items) +{ + /* we open the file in binary mode to ensure that the same output is + * generated on all platforms !! + */ + FILE* to = NULL; + if (options.autoDepFile) { + string fileName = options.outputFileName + ".d"; + to = fopen(fileName.c_str(), "wb"); + } else { + to = fopen(options.depFileName.c_str(), "wb"); + } + + if (to == NULL) { + return; + } + + const char* slash = "\\"; + import_info* import = g_imports; + if (import == NULL) { + slash = ""; + } + + if (items->item_type == INTERFACE_TYPE_BINDER || items->item_type == INTERFACE_TYPE_RPC) { + fprintf(to, "%s: \\\n", options.outputFileName.c_str()); + } else { + // parcelable: there's no output file. + fprintf(to, " : \\\n"); + } + 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"); + + // Output "<imported_file>: " so make won't fail if the imported file has + // been deleted, moved or renamed in incremental build. + import = g_imports; + while (import) { + if (import->filename) { + fprintf(to, "%s :\n", import->filename); + } + import = import->next; + } + + fclose(to); +} + +// ========================================================== +static string +generate_outputFileName2(const Options& options, const buffer_type& name, const char* package) +{ + string result; + + // create the path to the destination folder based on the + // interface package name + result = options.outputBaseFolder; + result += OS_PATH_SEPARATOR; + + string packageStr = package; + size_t len = packageStr.length(); + for (size_t i=0; i<len; i++) { + if (packageStr[i] == '.') { + packageStr[i] = OS_PATH_SEPARATOR; + } + } + + result += packageStr; + + // add the filename by replacing the .aidl extension to .java + const char* p = strchr(name.data, '.'); + len = p ? p-name.data : strlen(name.data); + + result += OS_PATH_SEPARATOR; + result.append(name.data, len); + result += ".java"; + + return result; +} + +// ========================================================== +static string +generate_outputFileName(const Options& options, const document_item_type* items) +{ + // items has already been checked to have only one interface. + if (items->item_type == INTERFACE_TYPE_BINDER || items->item_type == INTERFACE_TYPE_RPC) { + interface_type* type = (interface_type*)items; + + return generate_outputFileName2(options, type->name, type->package); + } else if (items->item_type == USER_DATA_TYPE) { + user_data_type* type = (user_data_type*)items; + return generate_outputFileName2(options, type->name, type->package); + } + + // I don't think we can come here, but safer than returning NULL. + string result; + return result; +} + + + +// ========================================================== +static void +check_outputFilePath(const string& path) { + size_t len = path.length(); + for (size_t i=0; i<len ; i++) { + if (path[i] == OS_PATH_SEPARATOR) { + string p = path.substr(0, i); + if (access(path.data(), F_OK) != 0) { +#ifdef HAVE_MS_C_RUNTIME + _mkdir(p.data()); +#else + mkdir(p.data(), S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP); +#endif + } + } + } +} + + +// ========================================================== +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 preprocessed 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)) { + user_data_type* parcl = (user_data_type*)malloc( + sizeof(user_data_type)); + memset(parcl, 0, sizeof(user_data_type)); + parcl->document_item.item_type = USER_DATA_TYPE; + parcl->keyword_token.lineno = lineno; + parcl->keyword_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(";"); + parcl->flattening_methods = PARCELABLE_DATA; + doc = (document_item_type*)parcl; + } + else if (0 == strcmp("flattenable", type)) { + user_data_type* parcl = (user_data_type*)malloc( + sizeof(user_data_type)); + memset(parcl, 0, sizeof(user_data_type)); + parcl->document_item.item_type = USER_DATA_TYPE; + parcl->keyword_token.lineno = lineno; + parcl->keyword_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(";"); + parcl->flattening_methods = RPC_DATA; + 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_BINDER; + 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 +check_and_assign_method_ids(const char * filename, interface_item_type* first_item) +{ + // Check whether there are any methods with manually assigned id's and any that are not. + // Either all method id's must be manually assigned or all of them must not. + // Also, check for duplicates of user set id's and that the id's are within the proper bounds. + set<int> usedIds; + interface_item_type* item = first_item; + bool hasUnassignedIds = false; + bool hasAssignedIds = false; + while (item != NULL) { + if (item->item_type == METHOD_TYPE) { + method_type* method_item = (method_type*)item; + if (method_item->hasId) { + hasAssignedIds = true; + method_item->assigned_id = atoi(method_item->id.data); + // Ensure that the user set id is not duplicated. + if (usedIds.find(method_item->assigned_id) != usedIds.end()) { + // We found a duplicate id, so throw an error. + fprintf(stderr, + "%s:%d Found duplicate method id (%d) for method: %s\n", + filename, method_item->id.lineno, + method_item->assigned_id, method_item->name.data); + return 1; + } + // Ensure that the user set id is within the appropriate limits + if (method_item->assigned_id < MIN_USER_SET_METHOD_ID || + method_item->assigned_id > MAX_USER_SET_METHOD_ID) { + fprintf(stderr, "%s:%d Found out of bounds id (%d) for method: %s\n", + filename, method_item->id.lineno, + method_item->assigned_id, method_item->name.data); + fprintf(stderr, " Value for id must be between %d and %d inclusive.\n", + MIN_USER_SET_METHOD_ID, MAX_USER_SET_METHOD_ID); + return 1; + } + usedIds.insert(method_item->assigned_id); + } else { + hasUnassignedIds = true; + } + if (hasAssignedIds && hasUnassignedIds) { + fprintf(stderr, + "%s: You must either assign id's to all methods or to none of them.\n", + filename); + return 1; + } + } + item = item->next; + } + + // In the case that all methods have unassigned id's, set a unique id for them. + if (hasUnassignedIds) { + int newId = 0; + item = first_item; + while (item != NULL) { + if (item->item_type == METHOD_TYPE) { + method_type* method_item = (method_type*)item; + method_item->assigned_id = newId++; + } + item = item->next; + } + } + + // success + return 0; +} + +// ========================================================== +static int +compile_aidl(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); + + // If this includes an interface definition, then assign method ids and validate. + if (!onlyParcelable) { + err |= check_and_assign_method_ids(options.inputFileName.c_str(), + ((interface_type*)mainDoc)->interface_items); + } + + // after this, there shouldn't be any more errors because of the + // input. + if (err != 0 || mainDoc == NULL) { + return 1; + } + + // if needed, generate the outputFileName from the outputBaseFolder + if (options.outputFileName.length() == 0 && + options.outputBaseFolder.length() > 0) { + options.outputFileName = generate_outputFileName(options, mainDoc); + } + + // if we were asked to, generate a make dependency file + // unless it's a parcelable *and* it's supposed to fail on parcelable + if ((options.autoDepFile || options.depFileName != "") && + !(onlyParcelable && options.failOnParcelable)) { + // make sure the folders of the output file all exists + check_outputFilePath(options.outputFileName); + generate_dep_file(options, mainDoc); + } + + // they didn't ask to fail on parcelables, so just exit quietly. + if (onlyParcelable && !options.failOnParcelable) { + return 0; + } + + // make sure the folders of the output file all exists + check_outputFilePath(options.outputFileName); + + 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 == USER_DATA_TYPE) { + user_data_type* parcelable = (user_data_type*)doc; + if ((parcelable->flattening_methods & PARCELABLE_DATA) != 0) { + line = "parcelable "; + } + if ((parcelable->flattening_methods & RPC_DATA) != 0) { + line = "flattenable "; + } + 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) +{ + 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 000000000000..cd6a3bd5dfc1 --- /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 000000000000..de1370c086f5 --- /dev/null +++ b/tools/aidl/aidl_language.h @@ -0,0 +1,172 @@ +#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; + bool hasId; + buffer_type equals_token; + buffer_type id; + // XXX missing comments/copy text here + buffer_type semicolon_token; + buffer_type* comments_token; // points into this structure, DO NOT DELETE + int assigned_id; +} method_type; + +enum { + USER_DATA_TYPE = 12, + INTERFACE_TYPE_BINDER, + INTERFACE_TYPE_RPC +}; + +typedef struct document_item_type { + unsigned item_type; + struct document_item_type* next; +} document_item_type; + + +// for user_data_type.flattening_methods +enum { + PARCELABLE_DATA = 0x1, + RPC_DATA = 0x2 +}; + +typedef struct user_data_type { + document_item_type document_item; + buffer_type keyword_token; // only the first one + char* package; + buffer_type name; + buffer_type semicolon_token; + int flattening_methods; +} user_data_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; + user_data_type* user_data; + 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 000000000000..3d33e7a14913 --- /dev/null +++ b/tools/aidl/aidl_language_l.l @@ -0,0 +1,214 @@ +%{ +#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}?\] +idvalue (0|[1-9][0-9]*) + +%% + + +\%\%\{ { 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 ','; } += { SET_BUFFER('='); return '='; } + + /* keywords */ +parcelable { SET_BUFFER(PARCELABLE); return PARCELABLE; } +interface { SET_BUFFER(INTERFACE); return INTERFACE; } +flattenable { SET_BUFFER(FLATTENABLE); return FLATTENABLE; } +rpc { SET_BUFFER(INTERFACE); return RPC; } +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; } +{idvalue} { SET_BUFFER(IDVALUE); return IDVALUE; } +{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 000000000000..9b40d28ad598 --- /dev/null +++ b/tools/aidl/aidl_language_y.y @@ -0,0 +1,373 @@ +%{ +#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 IDVALUE +%token GENERIC +%token ARRAY +%token PARCELABLE +%token INTERFACE +%token FLATTENABLE +%token RPC +%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.user_data; } + | interface_decl { $$.document_item = (document_item_type*)$1.interface_item; } + ; + +parcelable_decl: + PARCELABLE IDENTIFIER ';' { + user_data_type* b = (user_data_type*)malloc(sizeof(user_data_type)); + b->document_item.item_type = USER_DATA_TYPE; + b->document_item.next = NULL; + b->keyword_token = $1.buffer; + b->name = $2.buffer; + b->package = g_currentPackage ? strdup(g_currentPackage) : NULL; + b->semicolon_token = $3.buffer; + b->flattening_methods = PARCELABLE_DATA; + $$.user_data = b; + } + | PARCELABLE ';' { + fprintf(stderr, "%s:%d syntax error in parcelable declaration. Expected type name.\n", + g_currentFilename, $1.buffer.lineno); + $$.user_data = 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); + $$.user_data = NULL; + } + | FLATTENABLE IDENTIFIER ';' { + user_data_type* b = (user_data_type*)malloc(sizeof(user_data_type)); + b->document_item.item_type = USER_DATA_TYPE; + b->document_item.next = NULL; + b->keyword_token = $1.buffer; + b->name = $2.buffer; + b->package = g_currentPackage ? strdup(g_currentPackage) : NULL; + b->semicolon_token = $3.buffer; + b->flattening_methods = PARCELABLE_DATA | RPC_DATA; + $$.user_data = b; + } + | FLATTENABLE ';' { + fprintf(stderr, "%s:%d syntax error in flattenable declaration. Expected type name.\n", + g_currentFilename, $1.buffer.lineno); + $$.user_data = NULL; + } + | FLATTENABLE error ';' { + fprintf(stderr, "%s:%d syntax error in flattenable declaration. Expected type name, saw \"%s\".\n", + g_currentFilename, $2.buffer.lineno, $2.buffer.data); + $$.user_data = NULL; + } + + ; + +interface_header: + INTERFACE { + interface_type* c = (interface_type*)malloc(sizeof(interface_type)); + c->document_item.item_type = INTERFACE_TYPE_BINDER; + c->document_item.next = NULL; + 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->document_item.item_type = INTERFACE_TYPE_BINDER; + c->document_item.next = NULL; + c->interface_token = $2.buffer; + c->oneway = true; + c->oneway_token = $1.buffer; + c->comments_token = &c->oneway_token; + $$.interface_obj = c; + } + | RPC { + interface_type* c = (interface_type*)malloc(sizeof(interface_type)); + c->document_item.item_type = INTERFACE_TYPE_RPC; + c->document_item.next = NULL; + 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; + } + ; + +interface_keywords: + INTERFACE + | RPC + ; + +interface_decl: + interface_header IDENTIFIER '{' interface_items '}' { + interface_type* c = $1.interface_obj; + 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_keywords 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_keywords 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->oneway = false; + method->type = $1.type; + 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->hasId = false; + memset(&method->equals_token, 0, sizeof(buffer_type)); + memset(&method->id, 0, sizeof(buffer_type)); + 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->hasId = false; + memset(&method->equals_token, 0, sizeof(buffer_type)); + memset(&method->id, 0, sizeof(buffer_type)); + method->semicolon_token = $7.buffer; + method->comments_token = &method->oneway_token; + $$.method = method; + } + | type IDENTIFIER '(' arg_list ')' '=' IDVALUE ';' { + method_type *method = (method_type*)malloc(sizeof(method_type)); + method->interface_item.item_type = METHOD_TYPE; + method->interface_item.next = NULL; + method->oneway = false; + memset(&method->oneway_token, 0, sizeof(buffer_type)); + method->type = $1.type; + method->name = $2.buffer; + method->open_paren_token = $3.buffer; + method->args = $4.arg; + method->close_paren_token = $5.buffer; + method->hasId = true; + method->equals_token = $6.buffer; + method->id = $7.buffer; + method->semicolon_token = $8.buffer; + method->comments_token = &method->type.type; + $$.method = method; + } + | ONEWAY type IDENTIFIER '(' arg_list ')' '=' IDVALUE ';' { + 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->hasId = true; + method->equals_token = $7.buffer; + method->id = $8.buffer; + method->semicolon_token = $9.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 000000000000..9e57407e772f --- /dev/null +++ b/tools/aidl/generate_java.cpp @@ -0,0 +1,99 @@ +#include "generate_java.h" +#include "Type.h" +#include <string.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +// ================================================= +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]; +} + +// ================================================= +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; +} + +string +append(const char* a, const char* b) +{ + string s = a; + s += b; + return s; +} + +// ================================================= +int +generate_java(const string& filename, const string& originalSrc, + interface_type* iface) +{ + Class* cl; + + if (iface->document_item.item_type == INTERFACE_TYPE_BINDER) { + cl = generate_binder_interface_class(iface); + } + else if (iface->document_item.item_type == INTERFACE_TYPE_RPC) { + cl = generate_rpc_interface_class(iface); + } + + Document* document = new Document; + document->comment = ""; + if (iface->package) document->package = iface->package; + document->originalSrc = originalSrc; + document->classes.push_back(cl); + +// 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 000000000000..4bfcfeba07c8 --- /dev/null +++ b/tools/aidl/generate_java.h @@ -0,0 +1,33 @@ +#ifndef GENERATE_JAVA_H +#define GENERATE_JAVA_H + +#include "aidl_language.h" +#include "AST.h" + +#include <string> + +using namespace std; + +int generate_java(const string& filename, const string& originalSrc, + interface_type* iface); + +Class* generate_binder_interface_class(const interface_type* iface); +Class* generate_rpc_interface_class(const interface_type* iface); + +string gather_comments(extra_text_type* extra); +string append(const char* a, const char* b); + +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; +}; + +#endif // GENERATE_JAVA_H + diff --git a/tools/aidl/generate_java_binder.cpp b/tools/aidl/generate_java_binder.cpp new file mode 100644 index 000000000000..f291ceb2b09f --- /dev/null +++ b/tools/aidl/generate_java_binder.cpp @@ -0,0 +1,560 @@ +#include "generate_java.h" +#include "Type.h" +#include <string.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +// ================================================= +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 | OVERRIDE; + 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 | OVERRIDE; + 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->QualifiedName(); + 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, NULL); + 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 | OVERRIDE; + 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 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, Variable** cl) +{ + if (v->dimension == 0) { + t->CreateFromParcel(addTo, v, parcel, cl); + } + if (v->dimension == 1) { + t->CreateArrayFromParcel(addTo, v, parcel, cl); + } +} + +static void +generate_read_from_parcel(Type* t, StatementBlock* addTo, Variable* v, + Variable* parcel, Variable** cl) +{ + if (v->dimension == 0) { + t->ReadFromParcel(addTo, v, parcel, cl); + } + if (v->dimension == 1) { + t->ReadArrayFromParcel(addTo, v, parcel, cl); + } +} + + +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[60]; + sprintf(transactCodeValue, "(android.os.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 + Variable* cl = NULL; + 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, &cl); + } 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 | OVERRIDE; + 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 ? "android.os.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, &cl); + } + + // 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, &cl); + } + 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); +} + +Class* +generate_binder_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) { + method_type * method_item = (method_type*) item; + generate_method(method_item, interface, stub, proxy, method_item->assigned_id); + } + item = item->next; + index++; + } + + return interface; +} + diff --git a/tools/aidl/generate_java_rpc.cpp b/tools/aidl/generate_java_rpc.cpp new file mode 100644 index 000000000000..5e4daccf6334 --- /dev/null +++ b/tools/aidl/generate_java_rpc.cpp @@ -0,0 +1,1001 @@ +#include "generate_java.h" +#include "Type.h" +#include <string.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +Type* SERVICE_CONTEXT_TYPE = new Type("android.content", + "Context", Type::BUILT_IN, false, false, false); +Type* PRESENTER_BASE_TYPE = new Type("android.support.place.connector", + "EventListener", Type::BUILT_IN, false, false, false); +Type* PRESENTER_LISTENER_BASE_TYPE = new Type("android.support.place.connector", + "EventListener.Listener", Type::BUILT_IN, false, false, false); +Type* RPC_BROKER_TYPE = new Type("android.support.place.connector", "Broker", + Type::BUILT_IN, false, false, false); +Type* RPC_CONTAINER_TYPE = new Type("com.android.athome.connector", "ConnectorContainer", + Type::BUILT_IN, false, false, false); +Type* PLACE_INFO_TYPE = new Type("android.support.place.connector", "PlaceInfo", + Type::BUILT_IN, false, false, false); +// TODO: Just use Endpoint, so this works for all endpoints. +Type* RPC_CONNECTOR_TYPE = new Type("android.support.place.connector", "Connector", + Type::BUILT_IN, false, false, false); +Type* RPC_ENDPOINT_INFO_TYPE = new UserDataType("android.support.place.rpc", + "EndpointInfo", true, __FILE__, __LINE__); +Type* RPC_RESULT_HANDLER_TYPE = new UserDataType("android.support.place.rpc", "RpcResultHandler", + true, __FILE__, __LINE__); +Type* RPC_ERROR_LISTENER_TYPE = new Type("android.support.place.rpc", "RpcErrorHandler", + Type::BUILT_IN, false, false, false); +Type* RPC_CONTEXT_TYPE = new UserDataType("android.support.place.rpc", "RpcContext", true, + __FILE__, __LINE__); + +static void generate_create_from_data(Type* t, StatementBlock* addTo, const string& key, + Variable* v, Variable* data, Variable** cl); +static void generate_new_array(Type* t, StatementBlock* addTo, Variable* v, Variable* from); +static void generate_write_to_data(Type* t, StatementBlock* addTo, Expression* k, Variable* v, + Variable* data); + +static string +format_int(int n) +{ + char str[20]; + sprintf(str, "%d", n); + return string(str); +} + +static string +class_name_leaf(const string& str) +{ + string::size_type pos = str.rfind('.'); + if (pos == string::npos) { + return str; + } else { + return string(str, pos+1); + } +} + +static string +results_class_name(const string& n) +{ + string str = n; + str[0] = toupper(str[0]); + str.insert(0, "On"); + return str; +} + +static string +results_method_name(const string& n) +{ + string str = n; + str[0] = toupper(str[0]); + str.insert(0, "on"); + return str; +} + +static string +push_method_name(const string& n) +{ + string str = n; + str[0] = toupper(str[0]); + str.insert(0, "push"); + return str; +} + +// ================================================= +class DispatcherClass : public Class +{ +public: + DispatcherClass(const interface_type* iface, Expression* target); + virtual ~DispatcherClass(); + + void AddMethod(const method_type* method); + void DoneWithMethods(); + + Method* processMethod; + Variable* actionParam; + Variable* requestParam; + Variable* rpcContextParam; + Variable* errorParam; + Variable* requestData; + Variable* resultData; + IfStatement* dispatchIfStatement; + Expression* targetExpression; + +private: + void generate_process(); +}; + +DispatcherClass::DispatcherClass(const interface_type* iface, Expression* target) + :Class(), + dispatchIfStatement(NULL), + targetExpression(target) +{ + generate_process(); +} + +DispatcherClass::~DispatcherClass() +{ +} + +void +DispatcherClass::generate_process() +{ + // byte[] process(String action, byte[] params, RpcContext context, RpcError status) + this->processMethod = new Method; + this->processMethod->modifiers = PUBLIC; + this->processMethod->returnType = BYTE_TYPE; + this->processMethod->returnTypeDimension = 1; + this->processMethod->name = "process"; + this->processMethod->statements = new StatementBlock; + + this->actionParam = new Variable(STRING_TYPE, "action"); + this->processMethod->parameters.push_back(this->actionParam); + + this->requestParam = new Variable(BYTE_TYPE, "requestParam", 1); + this->processMethod->parameters.push_back(this->requestParam); + + this->rpcContextParam = new Variable(RPC_CONTEXT_TYPE, "context", 0); + this->processMethod->parameters.push_back(this->rpcContextParam); + + this->errorParam = new Variable(RPC_ERROR_TYPE, "errorParam", 0); + this->processMethod->parameters.push_back(this->errorParam); + + this->requestData = new Variable(RPC_DATA_TYPE, "request"); + this->processMethod->statements->Add(new VariableDeclaration(requestData, + new NewExpression(RPC_DATA_TYPE, 1, this->requestParam))); + + this->resultData = new Variable(RPC_DATA_TYPE, "resultData"); + this->processMethod->statements->Add(new VariableDeclaration(this->resultData, + NULL_VALUE)); +} + +void +DispatcherClass::AddMethod(const method_type* method) +{ + arg_type* arg; + + // The if/switch statement + IfStatement* ifs = new IfStatement(); + ifs->expression = new MethodCall(new StringLiteralExpression(method->name.data), "equals", + 1, this->actionParam); + StatementBlock* block = ifs->statements = new StatementBlock; + if (this->dispatchIfStatement == NULL) { + this->dispatchIfStatement = ifs; + this->processMethod->statements->Add(dispatchIfStatement); + } else { + this->dispatchIfStatement->elseif = ifs; + this->dispatchIfStatement = ifs; + } + + // The call to decl (from above) + MethodCall* realCall = new MethodCall(this->targetExpression, method->name.data); + + // args + Variable* classLoader = NULL; + 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; + + // Unmarshall the parameter + block->Add(new VariableDeclaration(v)); + if (convert_direction(arg->direction.data) & IN_PARAMETER) { + generate_create_from_data(t, block, arg->name.data, v, + this->requestData, &classLoader); + } else { + if (arg->type.dimension == 0) { + block->Add(new Assignment(v, new NewExpression(v->type))); + } + else if (arg->type.dimension == 1) { + generate_new_array(v->type, block, v, this->requestData); + } + else { + fprintf(stderr, "aidl:internal error %s:%d\n", __FILE__, + __LINE__); + } + } + + // Add that parameter to the method call + realCall->arguments.push_back(v); + + arg = arg->next; + } + + // Add a final parameter: RpcContext. Contains data about + // incoming request (e.g., certificate) + realCall->arguments.push_back(new Variable(RPC_CONTEXT_TYPE, "context", 0)); + + Type* returnType = NAMES.Search(method->type.type.data); + if (returnType == EVENT_FAKE_TYPE) { + returnType = VOID_TYPE; + } + + // the real call + bool first = true; + Variable* _result = NULL; + if (returnType == VOID_TYPE) { + block->Add(realCall); + } else { + _result = new Variable(returnType, "_result", + method->type.dimension); + block->Add(new VariableDeclaration(_result, realCall)); + + // need the result RpcData + if (first) { + block->Add(new Assignment(this->resultData, + new NewExpression(RPC_DATA_TYPE))); + first = false; + } + + // marshall the return value + generate_write_to_data(returnType, block, + new StringLiteralExpression("_result"), _result, this->resultData); + } + + // out parameters + int 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) { + // need the result RpcData + if (first) { + block->Add(new Assignment(this->resultData, new NewExpression(RPC_DATA_TYPE))); + first = false; + } + + generate_write_to_data(t, block, new StringLiteralExpression(arg->name.data), + v, this->resultData); + } + + arg = arg->next; + } +} + +void +DispatcherClass::DoneWithMethods() +{ + if (this->dispatchIfStatement == NULL) { + return; + } + + this->elements.push_back(this->processMethod); + + IfStatement* fallthrough = new IfStatement(); + fallthrough->statements = new StatementBlock; + fallthrough->statements->Add(new ReturnStatement( + new MethodCall(SUPER_VALUE, "process", 4, + this->actionParam, this->requestParam, + this->rpcContextParam, + this->errorParam))); + this->dispatchIfStatement->elseif = fallthrough; + IfStatement* s = new IfStatement; + s->statements = new StatementBlock; + this->processMethod->statements->Add(s); + s->expression = new Comparison(this->resultData, "!=", NULL_VALUE); + s->statements->Add(new ReturnStatement(new MethodCall(this->resultData, "serialize"))); + s->elseif = new IfStatement; + s = s->elseif; + s->statements->Add(new ReturnStatement(NULL_VALUE)); +} + +// ================================================= +class RpcProxyClass : public Class +{ +public: + RpcProxyClass(const interface_type* iface, InterfaceType* interfaceType); + virtual ~RpcProxyClass(); + + Variable* endpoint; + Variable* broker; + +private: + void generate_ctor(); + void generate_get_endpoint_info(); +}; + +RpcProxyClass::RpcProxyClass(const interface_type* iface, InterfaceType* interfaceType) + :Class() +{ + this->comment = gather_comments(iface->comments_token->extra); + this->modifiers = PUBLIC; + this->what = Class::CLASS; + this->type = interfaceType; + + // broker + this->broker = new Variable(RPC_BROKER_TYPE, "_broker"); + this->elements.push_back(new Field(PRIVATE, this->broker)); + // endpoint + this->endpoint = new Variable(RPC_ENDPOINT_INFO_TYPE, "_endpoint"); + this->elements.push_back(new Field(PRIVATE, this->endpoint)); + + // methods + generate_ctor(); + generate_get_endpoint_info(); +} + +RpcProxyClass::~RpcProxyClass() +{ +} + +void +RpcProxyClass::generate_ctor() +{ + Variable* broker = new Variable(RPC_BROKER_TYPE, "broker"); + Variable* endpoint = new Variable(RPC_ENDPOINT_INFO_TYPE, "endpoint"); + Method* ctor = new Method; + ctor->modifiers = PUBLIC; + ctor->name = class_name_leaf(this->type->Name()); + ctor->statements = new StatementBlock; + ctor->parameters.push_back(broker); + ctor->parameters.push_back(endpoint); + this->elements.push_back(ctor); + + ctor->statements->Add(new Assignment(this->broker, broker)); + ctor->statements->Add(new Assignment(this->endpoint, endpoint)); +} + +void +RpcProxyClass::generate_get_endpoint_info() +{ + Method* get = new Method; + get->modifiers = PUBLIC; + get->returnType = RPC_ENDPOINT_INFO_TYPE; + get->name = "getEndpointInfo"; + get->statements = new StatementBlock; + this->elements.push_back(get); + + get->statements->Add(new ReturnStatement(this->endpoint)); +} + +// ================================================= +class EventListenerClass : public DispatcherClass +{ +public: + EventListenerClass(const interface_type* iface, Type* listenerType); + virtual ~EventListenerClass(); + + Variable* _listener; + +private: + void generate_ctor(); +}; + +Expression* +generate_get_listener_expression(Type* cast) +{ + return new Cast(cast, new MethodCall(THIS_VALUE, "getView")); +} + +EventListenerClass::EventListenerClass(const interface_type* iface, Type* listenerType) + :DispatcherClass(iface, new FieldVariable(THIS_VALUE, "_listener")) +{ + this->modifiers = PRIVATE; + this->what = Class::CLASS; + this->type = new Type(iface->package ? iface->package : "", + append(iface->name.data, ".Presenter"), + Type::GENERATED, false, false, false); + this->extends = PRESENTER_BASE_TYPE; + + this->_listener = new Variable(listenerType, "_listener"); + this->elements.push_back(new Field(PRIVATE, this->_listener)); + + // methods + generate_ctor(); +} + +EventListenerClass::~EventListenerClass() +{ +} + +void +EventListenerClass::generate_ctor() +{ + Variable* broker = new Variable(RPC_BROKER_TYPE, "broker"); + Variable* listener = new Variable(this->_listener->type, "listener"); + Method* ctor = new Method; + ctor->modifiers = PUBLIC; + ctor->name = class_name_leaf(this->type->Name()); + ctor->statements = new StatementBlock; + ctor->parameters.push_back(broker); + ctor->parameters.push_back(listener); + this->elements.push_back(ctor); + + ctor->statements->Add(new MethodCall("super", 2, broker, listener)); + ctor->statements->Add(new Assignment(this->_listener, listener)); +} + +// ================================================= +class ListenerClass : public Class +{ +public: + ListenerClass(const interface_type* iface); + virtual ~ListenerClass(); + + bool needed; + +private: + void generate_ctor(); +}; + +ListenerClass::ListenerClass(const interface_type* iface) + :Class(), + needed(false) +{ + this->comment = "/** Extend this to listen to the events from this class. */"; + this->modifiers = STATIC | PUBLIC ; + this->what = Class::CLASS; + this->type = new Type(iface->package ? iface->package : "", + append(iface->name.data, ".Listener"), + Type::GENERATED, false, false, false); + this->extends = PRESENTER_LISTENER_BASE_TYPE; +} + +ListenerClass::~ListenerClass() +{ +} + +// ================================================= +class EndpointBaseClass : public DispatcherClass +{ +public: + EndpointBaseClass(const interface_type* iface); + virtual ~EndpointBaseClass(); + + bool needed; + +private: + void generate_ctor(); +}; + +EndpointBaseClass::EndpointBaseClass(const interface_type* iface) + :DispatcherClass(iface, THIS_VALUE), + needed(false) +{ + this->comment = "/** Extend this to implement a link service. */"; + this->modifiers = STATIC | PUBLIC | ABSTRACT; + this->what = Class::CLASS; + this->type = new Type(iface->package ? iface->package : "", + append(iface->name.data, ".EndpointBase"), + Type::GENERATED, false, false, false); + this->extends = RPC_CONNECTOR_TYPE; + + // methods + generate_ctor(); +} + +EndpointBaseClass::~EndpointBaseClass() +{ +} + +void +EndpointBaseClass::generate_ctor() +{ + Variable* container = new Variable(RPC_CONTAINER_TYPE, "container"); + Variable* broker = new Variable(RPC_BROKER_TYPE, "broker"); + Variable* place = new Variable(PLACE_INFO_TYPE, "placeInfo"); + Method* ctor = new Method; + ctor->modifiers = PUBLIC; + ctor->name = class_name_leaf(this->type->Name()); + ctor->statements = new StatementBlock; + ctor->parameters.push_back(container); + ctor->parameters.push_back(broker); + ctor->parameters.push_back(place); + this->elements.push_back(ctor); + + ctor->statements->Add(new MethodCall("super", 3, container, broker, place)); +} + +// ================================================= +class ResultDispatcherClass : public Class +{ +public: + ResultDispatcherClass(); + virtual ~ResultDispatcherClass(); + + void AddMethod(int index, const string& name, Method** method, Variable** param); + + bool needed; + Variable* methodId; + Variable* callback; + Method* onResultMethod; + Variable* resultParam; + SwitchStatement* methodSwitch; + +private: + void generate_ctor(); + void generate_onResult(); +}; + +ResultDispatcherClass::ResultDispatcherClass() + :Class(), + needed(false) +{ + this->modifiers = PRIVATE | FINAL; + this->what = Class::CLASS; + this->type = new Type("_ResultDispatcher", Type::GENERATED, false, false, false); + this->interfaces.push_back(RPC_RESULT_HANDLER_TYPE); + + // methodId + this->methodId = new Variable(INT_TYPE, "methodId"); + this->elements.push_back(new Field(PRIVATE, this->methodId)); + this->callback = new Variable(OBJECT_TYPE, "callback"); + this->elements.push_back(new Field(PRIVATE, this->callback)); + + // methods + generate_ctor(); + generate_onResult(); +} + +ResultDispatcherClass::~ResultDispatcherClass() +{ +} + +void +ResultDispatcherClass::generate_ctor() +{ + Variable* methodIdParam = new Variable(INT_TYPE, "methId"); + Variable* callbackParam = new Variable(OBJECT_TYPE, "cbObj"); + Method* ctor = new Method; + ctor->modifiers = PUBLIC; + ctor->name = class_name_leaf(this->type->Name()); + ctor->statements = new StatementBlock; + ctor->parameters.push_back(methodIdParam); + ctor->parameters.push_back(callbackParam); + this->elements.push_back(ctor); + + ctor->statements->Add(new Assignment(this->methodId, methodIdParam)); + ctor->statements->Add(new Assignment(this->callback, callbackParam)); +} + +void +ResultDispatcherClass::generate_onResult() +{ + this->onResultMethod = new Method; + this->onResultMethod->modifiers = PUBLIC; + this->onResultMethod->returnType = VOID_TYPE; + this->onResultMethod->returnTypeDimension = 0; + this->onResultMethod->name = "onResult"; + this->onResultMethod->statements = new StatementBlock; + this->elements.push_back(this->onResultMethod); + + this->resultParam = new Variable(BYTE_TYPE, "result", 1); + this->onResultMethod->parameters.push_back(this->resultParam); + + this->methodSwitch = new SwitchStatement(this->methodId); + this->onResultMethod->statements->Add(this->methodSwitch); +} + +void +ResultDispatcherClass::AddMethod(int index, const string& name, Method** method, Variable** param) +{ + Method* m = new Method; + m->modifiers = PUBLIC; + m->returnType = VOID_TYPE; + m->returnTypeDimension = 0; + m->name = name; + m->statements = new StatementBlock; + *param = new Variable(BYTE_TYPE, "result", 1); + m->parameters.push_back(*param); + this->elements.push_back(m); + *method = m; + + Case* c = new Case(format_int(index)); + c->statements->Add(new MethodCall(new LiteralExpression("this"), name, 1, this->resultParam)); + c->statements->Add(new Break()); + + this->methodSwitch->cases.push_back(c); +} + +// ================================================= +static void +generate_new_array(Type* t, StatementBlock* addTo, Variable* v, Variable* from) +{ + fprintf(stderr, "aidl: implement generate_new_array %s:%d\n", __FILE__, __LINE__); + exit(1); +} + +static void +generate_create_from_data(Type* t, StatementBlock* addTo, const string& key, Variable* v, + Variable* data, Variable** cl) +{ + Expression* k = new StringLiteralExpression(key); + if (v->dimension == 0) { + t->CreateFromRpcData(addTo, k, v, data, cl); + } + if (v->dimension == 1) { + //t->ReadArrayFromRpcData(addTo, v, data, cl); + fprintf(stderr, "aidl: implement generate_create_from_data for arrays%s:%d\n", + __FILE__, __LINE__); + } +} + +static void +generate_write_to_data(Type* t, StatementBlock* addTo, Expression* k, Variable* v, Variable* data) +{ + if (v->dimension == 0) { + t->WriteToRpcData(addTo, k, v, data, 0); + } + if (v->dimension == 1) { + //t->WriteArrayToParcel(addTo, v, data); + fprintf(stderr, "aidl: implement generate_write_to_data for arrays%s:%d\n", + __FILE__, __LINE__); + } +} + +// ================================================= +static Type* +generate_results_method(const method_type* method, RpcProxyClass* proxyClass) +{ + arg_type* arg; + + string resultsMethodName = results_method_name(method->name.data); + Type* resultsInterfaceType = new Type(results_class_name(method->name.data), + Type::GENERATED, false, false, false); + + if (!method->oneway) { + Class* resultsClass = new Class; + resultsClass->modifiers = STATIC | PUBLIC; + resultsClass->what = Class::INTERFACE; + resultsClass->type = resultsInterfaceType; + + Method* resultMethod = new Method; + resultMethod->comment = gather_comments(method->comments_token->extra); + resultMethod->modifiers = PUBLIC; + resultMethod->returnType = VOID_TYPE; + resultMethod->returnTypeDimension = 0; + resultMethod->name = resultsMethodName; + if (0 != strcmp("void", method->type.type.data)) { + resultMethod->parameters.push_back(new Variable(NAMES.Search(method->type.type.data), + "_result", method->type.dimension)); + } + arg = method->args; + while (arg != NULL) { + if (convert_direction(arg->direction.data) & OUT_PARAMETER) { + resultMethod->parameters.push_back(new Variable( + NAMES.Search(arg->type.type.data), arg->name.data, + arg->type.dimension)); + } + arg = arg->next; + } + resultsClass->elements.push_back(resultMethod); + + if (resultMethod->parameters.size() > 0) { + proxyClass->elements.push_back(resultsClass); + return resultsInterfaceType; + } + } + //delete resultsInterfaceType; + return NULL; +} + +static void +generate_proxy_method(const method_type* method, RpcProxyClass* proxyClass, + ResultDispatcherClass* resultsDispatcherClass, Type* resultsInterfaceType, int index) +{ + arg_type* arg; + Method* proxyMethod = new Method; + proxyMethod->comment = gather_comments(method->comments_token->extra); + proxyMethod->modifiers = PUBLIC; + proxyMethod->returnType = VOID_TYPE; + proxyMethod->returnTypeDimension = 0; + proxyMethod->name = method->name.data; + proxyMethod->statements = new StatementBlock; + proxyClass->elements.push_back(proxyMethod); + + // The local variables + Variable* _data = new Variable(RPC_DATA_TYPE, "_data"); + proxyMethod->statements->Add(new VariableDeclaration(_data, new NewExpression(RPC_DATA_TYPE))); + + // Add the arguments + arg = method->args; + while (arg != NULL) { + if (convert_direction(arg->direction.data) & IN_PARAMETER) { + // Function signature + Type* t = NAMES.Search(arg->type.type.data); + Variable* v = new Variable(t, arg->name.data, arg->type.dimension); + proxyMethod->parameters.push_back(v); + + // Input parameter marshalling + generate_write_to_data(t, proxyMethod->statements, + new StringLiteralExpression(arg->name.data), v, _data); + } + arg = arg->next; + } + + // If there is a results interface for this class + Expression* resultParameter; + if (resultsInterfaceType != NULL) { + // Result interface parameter + Variable* resultListener = new Variable(resultsInterfaceType, "_result"); + proxyMethod->parameters.push_back(resultListener); + + // Add the results dispatcher callback + resultsDispatcherClass->needed = true; + resultParameter = new NewExpression(resultsDispatcherClass->type, 2, + new LiteralExpression(format_int(index)), resultListener); + } else { + resultParameter = NULL_VALUE; + } + + // All proxy methods take an error parameter + Variable* errorListener = new Variable(RPC_ERROR_LISTENER_TYPE, "_errors"); + proxyMethod->parameters.push_back(errorListener); + + // Call the broker + proxyMethod->statements->Add(new MethodCall(new FieldVariable(THIS_VALUE, "_broker"), + "sendRpc", 5, + proxyClass->endpoint, + new StringLiteralExpression(method->name.data), + new MethodCall(_data, "serialize"), + resultParameter, + errorListener)); +} + +static void +generate_result_dispatcher_method(const method_type* method, + ResultDispatcherClass* resultsDispatcherClass, Type* resultsInterfaceType, int index) +{ + arg_type* arg; + Method* dispatchMethod; + Variable* dispatchParam; + resultsDispatcherClass->AddMethod(index, method->name.data, &dispatchMethod, &dispatchParam); + + Variable* classLoader = NULL; + Variable* resultData = new Variable(RPC_DATA_TYPE, "resultData"); + dispatchMethod->statements->Add(new VariableDeclaration(resultData, + new NewExpression(RPC_DATA_TYPE, 1, dispatchParam))); + + // The callback method itself + MethodCall* realCall = new MethodCall( + new Cast(resultsInterfaceType, new FieldVariable(THIS_VALUE, "callback")), + results_method_name(method->name.data)); + + // The return value + { + Type* t = NAMES.Search(method->type.type.data); + if (t != VOID_TYPE) { + Variable* rv = new Variable(t, "rv"); + dispatchMethod->statements->Add(new VariableDeclaration(rv)); + generate_create_from_data(t, dispatchMethod->statements, "_result", rv, + resultData, &classLoader); + realCall->arguments.push_back(rv); + } + } + + VariableFactory stubArgs("arg"); + arg = method->args; + while (arg != NULL) { + if (convert_direction(arg->direction.data) & OUT_PARAMETER) { + // Unmarshall the results + Type* t = NAMES.Search(arg->type.type.data); + Variable* v = stubArgs.Get(t); + dispatchMethod->statements->Add(new VariableDeclaration(v)); + + generate_create_from_data(t, dispatchMethod->statements, arg->name.data, v, + resultData, &classLoader); + + // Add the argument to the callback + realCall->arguments.push_back(v); + } + arg = arg->next; + } + + // Call the callback method + IfStatement* ifst = new IfStatement; + ifst->expression = new Comparison(new FieldVariable(THIS_VALUE, "callback"), "!=", NULL_VALUE); + dispatchMethod->statements->Add(ifst); + ifst->statements->Add(realCall); +} + +static void +generate_regular_method(const method_type* method, RpcProxyClass* proxyClass, + EndpointBaseClass* serviceBaseClass, ResultDispatcherClass* resultsDispatcherClass, + int index) +{ + arg_type* arg; + + // == the callback interface for results ================================ + // the service base class + Type* resultsInterfaceType = generate_results_method(method, proxyClass); + + // == the method in the proxy class ===================================== + generate_proxy_method(method, proxyClass, resultsDispatcherClass, resultsInterfaceType, index); + + // == the method in the result dispatcher class ========================= + if (resultsInterfaceType != NULL) { + generate_result_dispatcher_method(method, resultsDispatcherClass, resultsInterfaceType, + index); + } + + // == The abstract method that the service developers implement ========== + Method* decl = new Method; + decl->comment = gather_comments(method->comments_token->extra); + decl->modifiers = PUBLIC | ABSTRACT; + 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; + } + + // Add the default RpcContext param to all methods + decl->parameters.push_back(new Variable(RPC_CONTEXT_TYPE, "context", 0)); + + serviceBaseClass->elements.push_back(decl); + + + // == the dispatch method in the service base class ====================== + serviceBaseClass->AddMethod(method); +} + +static void +generate_event_method(const method_type* method, RpcProxyClass* proxyClass, + EndpointBaseClass* serviceBaseClass, ListenerClass* listenerClass, + EventListenerClass* presenterClass, int index) +{ + arg_type* arg; + listenerClass->needed = true; + + // == the push method in the service base class ========================= + Method* push = new Method; + push->modifiers = PUBLIC; + push->name = push_method_name(method->name.data); + push->statements = new StatementBlock; + push->returnType = VOID_TYPE; + serviceBaseClass->elements.push_back(push); + + // The local variables + Variable* _data = new Variable(RPC_DATA_TYPE, "_data"); + push->statements->Add(new VariableDeclaration(_data, new NewExpression(RPC_DATA_TYPE))); + + // Add the arguments + arg = method->args; + while (arg != NULL) { + // Function signature + Type* t = NAMES.Search(arg->type.type.data); + Variable* v = new Variable(t, arg->name.data, arg->type.dimension); + push->parameters.push_back(v); + + // Input parameter marshalling + generate_write_to_data(t, push->statements, + new StringLiteralExpression(arg->name.data), v, _data); + + arg = arg->next; + } + + // Send the notifications + push->statements->Add(new MethodCall("pushEvent", 2, + new StringLiteralExpression(method->name.data), + new MethodCall(_data, "serialize"))); + + // == the event callback dispatcher method ==================================== + presenterClass->AddMethod(method); + + // == the event method in the listener base class ===================== + Method* event = new Method; + event->modifiers = PUBLIC; + event->name = method->name.data; + event->statements = new StatementBlock; + event->returnType = VOID_TYPE; + listenerClass->elements.push_back(event); + arg = method->args; + while (arg != NULL) { + event->parameters.push_back(new Variable( + NAMES.Search(arg->type.type.data), arg->name.data, + arg->type.dimension)); + arg = arg->next; + } + + // Add a final parameter: RpcContext. Contains data about + // incoming request (e.g., certificate) + event->parameters.push_back(new Variable(RPC_CONTEXT_TYPE, "context", 0)); +} + +static void +generate_listener_methods(RpcProxyClass* proxyClass, Type* presenterType, Type* listenerType) +{ + // AndroidAtHomePresenter _presenter; + // void startListening(Listener listener) { + // stopListening(); + // _presenter = new Presenter(_broker, listener); + // _presenter.startListening(_endpoint); + // } + // void stopListening() { + // if (_presenter != null) { + // _presenter.stopListening(); + // } + // } + + Variable* _presenter = new Variable(presenterType, "_presenter"); + proxyClass->elements.push_back(new Field(PRIVATE, _presenter)); + + Variable* listener = new Variable(listenerType, "listener"); + + Method* startListeningMethod = new Method; + startListeningMethod->modifiers = PUBLIC; + startListeningMethod->returnType = VOID_TYPE; + startListeningMethod->name = "startListening"; + startListeningMethod->statements = new StatementBlock; + startListeningMethod->parameters.push_back(listener); + proxyClass->elements.push_back(startListeningMethod); + + startListeningMethod->statements->Add(new MethodCall(THIS_VALUE, "stopListening")); + startListeningMethod->statements->Add(new Assignment(_presenter, + new NewExpression(presenterType, 2, proxyClass->broker, listener))); + startListeningMethod->statements->Add(new MethodCall(_presenter, + "startListening", 1, proxyClass->endpoint)); + + Method* stopListeningMethod = new Method; + stopListeningMethod->modifiers = PUBLIC; + stopListeningMethod->returnType = VOID_TYPE; + stopListeningMethod->name = "stopListening"; + stopListeningMethod->statements = new StatementBlock; + proxyClass->elements.push_back(stopListeningMethod); + + IfStatement* ifst = new IfStatement; + ifst->expression = new Comparison(_presenter, "!=", NULL_VALUE); + stopListeningMethod->statements->Add(ifst); + + ifst->statements->Add(new MethodCall(_presenter, "stopListening")); + ifst->statements->Add(new Assignment(_presenter, NULL_VALUE)); +} + +Class* +generate_rpc_interface_class(const interface_type* iface) +{ + // the proxy class + InterfaceType* interfaceType = static_cast<InterfaceType*>( + NAMES.Find(iface->package, iface->name.data)); + RpcProxyClass* proxy = new RpcProxyClass(iface, interfaceType); + + // the listener class + ListenerClass* listener = new ListenerClass(iface); + + // the presenter class + EventListenerClass* presenter = new EventListenerClass(iface, listener->type); + + // the service base class + EndpointBaseClass* base = new EndpointBaseClass(iface); + proxy->elements.push_back(base); + + // the result dispatcher + ResultDispatcherClass* results = new ResultDispatcherClass(); + + // all the declared methods of the proxy + int index = 0; + interface_item_type* item = iface->interface_items; + while (item != NULL) { + if (item->item_type == METHOD_TYPE) { + if (NAMES.Search(((method_type*)item)->type.type.data) == EVENT_FAKE_TYPE) { + generate_event_method((method_type*)item, proxy, base, listener, presenter, index); + } else { + generate_regular_method((method_type*)item, proxy, base, results, index); + } + } + item = item->next; + index++; + } + presenter->DoneWithMethods(); + base->DoneWithMethods(); + + // only add this if there are methods with results / out parameters + if (results->needed) { + proxy->elements.push_back(results); + } + if (listener->needed) { + proxy->elements.push_back(listener); + proxy->elements.push_back(presenter); + generate_listener_methods(proxy, presenter->type, listener->type); + } + + return proxy; +} diff --git a/tools/aidl/options.cpp b/tools/aidl/options.cpp new file mode 100644 index 000000000000..7b2daebec09e --- /dev/null +++ b/tools/aidl/options.cpp @@ -0,0 +1,154 @@ + +#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" + " -a generate dependency file next to the output file with the name based on the input file.\n" + " -p<FILE> file created by --preprocess to import.\n" + " -o<FOLDER> base output folder for generated files.\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.\n" + " If omitted and the -o option is not used, the input filename is used, with the .aidl extension changed to a .java extension.\n" + " If the -o option is used, the generated files will be placed in the base output folder, under their package folder\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->autoDepFile = 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] == 'a') { + options->autoDepFile = true; + } + 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 (s[1] == 'o') { + if (len > 2) { + options->outputBaseFolder = s+2; + } else { + fprintf(stderr, "-o option (%d) requires a path.\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 if (options->outputBaseFolder.length() == 0) { + // 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 000000000000..387e37d08732 --- /dev/null +++ b/tools/aidl/options.h @@ -0,0 +1,36 @@ +#ifndef DEVICE_TOOLS_AIDL_H +#define DEVICE_TOOLS_AIDL_H + +#include <string.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 outputBaseFolder; + string depFileName; + bool autoDepFile; + + 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 000000000000..bd106ce54f2d --- /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 000000000000..ffb6cb2932e4 --- /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 000000000000..2bf94b12bbf1 --- /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/.gitignore b/tools/layoutlib/.gitignore new file mode 100644 index 000000000000..c5e82d74585d --- /dev/null +++ b/tools/layoutlib/.gitignore @@ -0,0 +1 @@ +bin
\ No newline at end of file diff --git a/tools/layoutlib/Android.mk b/tools/layoutlib/Android.mk new file mode 100644 index 000000000000..4e735686d4fd --- /dev/null +++ b/tools/layoutlib/Android.mk @@ -0,0 +1,65 @@ +# +# 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 java-lib-deps,framework-base) +built_framework_classes := $(call java-lib-files,framework-base) + +built_core_dep := $(call java-lib-deps,core) +built_core_classes := $(call java-lib-files,core) + +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) + $(hide) echo "host layoutlib_create: $@" + $(hide) mkdir -p $(dir $@) + $(hide) rm -f $@ + $(hide) ls -l $(built_framework_classes) + $(hide) java -jar $(built_layoutlib_create_jar) \ + $@ \ + $(built_core_classes) \ + $(built_framework_classes) + $(hide) ls -l $(built_framework_classes) + + +# +# Include the subdir makefiles. +# +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tools/layoutlib/README b/tools/layoutlib/README new file mode 100644 index 000000000000..0fea9bd9d9ac --- /dev/null +++ b/tools/layoutlib/README @@ -0,0 +1,4 @@ +Layoutlib is a custom version of the android View framework designed to run inside Eclipse. +The goal of the library is to provide layout rendering in Eclipse that are very very close to their rendering on devices. + +None of the com.android.* or android.* classes in layoutlib run on devices.
\ No newline at end of file diff --git a/tools/layoutlib/bridge/.classpath b/tools/layoutlib/bridge/.classpath new file mode 100644 index 000000000000..3c124d90aaae --- /dev/null +++ b/tools/layoutlib/bridge/.classpath @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry excluding="org/kxml2/io/" kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry kind="var" path="ANDROID_PLAT_SRC/prebuilts/misc/common/layoutlib_api/layoutlib_api-prebuilt.jar"/> + <classpathentry kind="var" path="ANDROID_PLAT_SRC/prebuilts/misc/common/kxml2/kxml2-2.3.0.jar" sourcepath="/ANDROID_PLAT_SRC/dalvik/libcore/xml/src/main/java"/> + <classpathentry kind="var" path="ANDROID_PLAT_SRC/out/host/common/obj/JAVA_LIBRARIES/temp_layoutlib_intermediates/javalib.jar" sourcepath="/ANDROID_PLAT_SRC/frameworks/base"/> + <classpathentry kind="var" path="ANDROID_PLAT_SRC/prebuilts/misc/common/ninepatch/ninepatch-prebuilt.jar"/> + <classpathentry kind="var" path="ANDROID_PLAT_SRC/prebuilts/misc/common/tools-common/tools-common-prebuilt.jar"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/tools/layoutlib/bridge/.project b/tools/layoutlib/bridge/.project new file mode 100644 index 000000000000..e36e71b23ff9 --- /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/.settings/README.txt b/tools/layoutlib/bridge/.settings/README.txt new file mode 100644 index 000000000000..9120b20710a3 --- /dev/null +++ b/tools/layoutlib/bridge/.settings/README.txt @@ -0,0 +1,2 @@ +Copy this in eclipse project as a .settings folder at the root. +This ensure proper compilation compliance and warning/error levels.
\ No newline at end of file diff --git a/tools/layoutlib/bridge/.settings/org.eclipse.jdt.core.prefs b/tools/layoutlib/bridge/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000000..5381a0e16c7d --- /dev/null +++ b/tools/layoutlib/bridge/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,93 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled +org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullReference=error +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/tools/layoutlib/bridge/Android.mk b/tools/layoutlib/bridge/Android.mk new file mode 100644 index 000000000000..687a91feeb6f --- /dev/null +++ b/tools/layoutlib/bridge/Android.mk @@ -0,0 +1,38 @@ +# +# 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_RESOURCE_DIRS := resources + + +LOCAL_JAVA_LIBRARIES := \ + kxml2-2.3.0 \ + layoutlib_api-prebuilt \ + tools-common-prebuilt + +LOCAL_STATIC_JAVA_LIBRARIES := \ + temp_layoutlib \ + ninepatch-prebuilt + +LOCAL_MODULE := layoutlib + +include $(BUILD_HOST_JAVA_LIBRARY) + +# Build all sub-directories +include $(call all-makefiles-under,$(LOCAL_PATH)) + diff --git a/tools/layoutlib/bridge/resources/bars/action_bar.xml b/tools/layoutlib/bridge/resources/bars/action_bar.xml new file mode 100644 index 000000000000..7adc5af44b75 --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/action_bar.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge xmlns:android="http://schemas.android.com/apk/res/android"> + <include layout="@android:layout/action_bar_home" /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> +</merge> diff --git a/tools/layoutlib/bridge/resources/bars/hdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/hdpi/ic_sysbar_back.png Binary files differnew file mode 100644 index 000000000000..84e6bc89c082 --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/hdpi/ic_sysbar_back.png diff --git a/tools/layoutlib/bridge/resources/bars/hdpi/ic_sysbar_home.png b/tools/layoutlib/bridge/resources/bars/hdpi/ic_sysbar_home.png Binary files differnew file mode 100644 index 000000000000..38e4f45719e7 --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/hdpi/ic_sysbar_home.png diff --git a/tools/layoutlib/bridge/resources/bars/hdpi/ic_sysbar_recent.png b/tools/layoutlib/bridge/resources/bars/hdpi/ic_sysbar_recent.png Binary files differnew file mode 100644 index 000000000000..bf9f3009c32c --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/hdpi/ic_sysbar_recent.png diff --git a/tools/layoutlib/bridge/resources/bars/hdpi/stat_sys_wifi_signal_4_fully.png b/tools/layoutlib/bridge/resources/bars/hdpi/stat_sys_wifi_signal_4_fully.png Binary files differnew file mode 100644 index 000000000000..bd44b529ee74 --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/hdpi/stat_sys_wifi_signal_4_fully.png diff --git a/tools/layoutlib/bridge/resources/bars/hdpi/status_bar_background.9.png b/tools/layoutlib/bridge/resources/bars/hdpi/status_bar_background.9.png Binary files differnew file mode 100644 index 000000000000..a4be29879663 --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/hdpi/status_bar_background.9.png diff --git a/tools/layoutlib/bridge/resources/bars/mdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/mdpi/ic_sysbar_back.png Binary files differnew file mode 100644 index 000000000000..a00bc5b5f33b --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/mdpi/ic_sysbar_back.png diff --git a/tools/layoutlib/bridge/resources/bars/mdpi/ic_sysbar_home.png b/tools/layoutlib/bridge/resources/bars/mdpi/ic_sysbar_home.png Binary files differnew file mode 100644 index 000000000000..dc3183bf640f --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/mdpi/ic_sysbar_home.png diff --git a/tools/layoutlib/bridge/resources/bars/mdpi/ic_sysbar_recent.png b/tools/layoutlib/bridge/resources/bars/mdpi/ic_sysbar_recent.png Binary files differnew file mode 100644 index 000000000000..b07f611ab98c --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/mdpi/ic_sysbar_recent.png diff --git a/tools/layoutlib/bridge/resources/bars/mdpi/stat_sys_wifi_signal_4_fully.png b/tools/layoutlib/bridge/resources/bars/mdpi/stat_sys_wifi_signal_4_fully.png Binary files differnew file mode 100644 index 000000000000..c629387c20b8 --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/mdpi/stat_sys_wifi_signal_4_fully.png diff --git a/tools/layoutlib/bridge/resources/bars/mdpi/status_bar_background.9.png b/tools/layoutlib/bridge/resources/bars/mdpi/status_bar_background.9.png Binary files differnew file mode 100644 index 000000000000..eb7c1a4d7819 --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/mdpi/status_bar_background.9.png diff --git a/tools/layoutlib/bridge/resources/bars/navigation_bar.xml b/tools/layoutlib/bridge/resources/bars/navigation_bar.xml new file mode 100644 index 000000000000..599ca08bf02c --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/navigation_bar.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1"/> + <ImageView + android:layout_height="wrap_content" + android:layout_width="wrap_content"/> + <ImageView + android:layout_height="wrap_content" + android:layout_width="wrap_content"/> + <ImageView + android:layout_height="wrap_content" + android:layout_width="wrap_content"/> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1"/> +</merge> diff --git a/tools/layoutlib/bridge/resources/bars/status_bar.xml b/tools/layoutlib/bridge/resources/bars/status_bar.xml new file mode 100644 index 000000000000..d3c492eacf43 --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/status_bar.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1"/> + <ImageView + android:layout_height="wrap_content" + android:layout_width="wrap_content"/> + <ImageView + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_marginLeft="3dip" + android:layout_marginRight="5dip"/> +</merge> diff --git a/tools/layoutlib/bridge/resources/bars/title_bar.xml b/tools/layoutlib/bridge/resources/bars/title_bar.xml new file mode 100644 index 000000000000..76d78d9169d4 --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/title_bar.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge xmlns:android="http://schemas.android.com/apk/res/android"> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> +</merge> diff --git a/tools/layoutlib/bridge/resources/bars/xhdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/xhdpi/ic_sysbar_back.png Binary files differnew file mode 100644 index 000000000000..bd60cd65878a --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/xhdpi/ic_sysbar_back.png diff --git a/tools/layoutlib/bridge/resources/bars/xhdpi/ic_sysbar_home.png b/tools/layoutlib/bridge/resources/bars/xhdpi/ic_sysbar_home.png Binary files differnew file mode 100644 index 000000000000..c5bc5c96ef05 --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/xhdpi/ic_sysbar_home.png diff --git a/tools/layoutlib/bridge/resources/bars/xhdpi/ic_sysbar_recent.png b/tools/layoutlib/bridge/resources/bars/xhdpi/ic_sysbar_recent.png Binary files differnew file mode 100644 index 000000000000..f621d9cc7242 --- /dev/null +++ b/tools/layoutlib/bridge/resources/bars/xhdpi/ic_sysbar_recent.png diff --git a/tools/layoutlib/bridge/src/android/animation/AnimationThread.java b/tools/layoutlib/bridge/src/android/animation/AnimationThread.java new file mode 100644 index 000000000000..b10ec9fec2ad --- /dev/null +++ b/tools/layoutlib/bridge/src/android/animation/AnimationThread.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +import com.android.ide.common.rendering.api.IAnimationListener; +import com.android.ide.common.rendering.api.RenderSession; +import com.android.ide.common.rendering.api.Result; +import com.android.ide.common.rendering.api.Result.Status; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.RenderSessionImpl; + +import android.os.Handler; +import android.os.Handler_Delegate; +import android.os.Handler_Delegate.IHandlerCallback; +import android.os.Message; + +import java.util.PriorityQueue; +import java.util.Queue; + +/** + * Abstract animation thread. + * <p/> + * This does not actually start an animation, instead it fakes a looper that will play whatever + * animation is sending messages to its own {@link Handler}. + * <p/> + * Classes should implement {@link #preAnimation()} and {@link #postAnimation()}. + * <p/> + * If {@link #preAnimation()} does not start an animation somehow then the thread doesn't do + * anything. + * + */ +public abstract class AnimationThread extends Thread { + + private static class MessageBundle implements Comparable<MessageBundle> { + final Handler mTarget; + final Message mMessage; + final long mUptimeMillis; + + MessageBundle(Handler target, Message message, long uptimeMillis) { + mTarget = target; + mMessage = message; + mUptimeMillis = uptimeMillis; + } + + @Override + public int compareTo(MessageBundle bundle) { + if (mUptimeMillis < bundle.mUptimeMillis) { + return -1; + } + return 1; + } + } + + private final RenderSessionImpl mSession; + + private Queue<MessageBundle> mQueue = new PriorityQueue<MessageBundle>(); + private final IAnimationListener mListener; + + public AnimationThread(RenderSessionImpl scene, String threadName, + IAnimationListener listener) { + super(threadName); + mSession = scene; + mListener = listener; + } + + public abstract Result preAnimation(); + public abstract void postAnimation(); + + @Override + public void run() { + Bridge.prepareThread(); + try { + /* FIXME: The ANIMATION_FRAME message no longer exists. Instead, the + * animation timing loop is completely based on a Choreographer objects + * that schedules animation and drawing frames. The animation handler is + * no longer even a handler; it is just a Runnable enqueued on the Choreographer. + Handler_Delegate.setCallback(new IHandlerCallback() { + @Override + public void sendMessageAtTime(Handler handler, Message msg, long uptimeMillis) { + if (msg.what == ValueAnimator.ANIMATION_START || + msg.what == ValueAnimator.ANIMATION_FRAME) { + mQueue.add(new MessageBundle(handler, msg, uptimeMillis)); + } else { + // just ignore. + } + } + }); + */ + + // call out to the pre-animation work, which should start an animation or more. + Result result = preAnimation(); + if (result.isSuccess() == false) { + mListener.done(result); + } + + // loop the animation + RenderSession session = mSession.getSession(); + do { + // check early. + if (mListener.isCanceled()) { + break; + } + + // get the next message. + MessageBundle bundle = mQueue.poll(); + if (bundle == null) { + break; + } + + // sleep enough for this bundle to be on time + long currentTime = System.currentTimeMillis(); + if (currentTime < bundle.mUptimeMillis) { + try { + sleep(bundle.mUptimeMillis - currentTime); + } catch (InterruptedException e) { + // FIXME log/do something/sleep again? + e.printStackTrace(); + } + } + + // check after sleeping. + if (mListener.isCanceled()) { + break; + } + + // ready to do the work, acquire the scene. + result = mSession.acquire(250); + if (result.isSuccess() == false) { + mListener.done(result); + return; + } + + // process the bundle. If the animation is not finished, this will enqueue + // the next message, so mQueue will have another one. + try { + // check after acquiring in case it took a while. + if (mListener.isCanceled()) { + break; + } + + bundle.mTarget.handleMessage(bundle.mMessage); + if (mSession.render(false /*freshRender*/).isSuccess()) { + mListener.onNewFrame(session); + } + } finally { + mSession.release(); + } + } while (mListener.isCanceled() == false && mQueue.size() > 0); + + mListener.done(Status.SUCCESS.createResult()); + + } catch (Throwable throwable) { + // can't use Bridge.getLog() as the exception might be thrown outside + // of an acquire/release block. + mListener.done(Status.ERROR_UNKNOWN.createResult("Error playing animation", throwable)); + + } finally { + postAnimation(); + Handler_Delegate.setCallback(null); + Bridge.cleanupThread(); + } + } +} diff --git a/tools/layoutlib/bridge/src/android/animation/PropertyValuesHolder_Delegate.java b/tools/layoutlib/bridge/src/android/animation/PropertyValuesHolder_Delegate.java new file mode 100644 index 000000000000..7b444aadb303 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/animation/PropertyValuesHolder_Delegate.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.animation; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.animation.PropertyValuesHolder + * + * Through the layoutlib_create tool, the original native methods of PropertyValuesHolder have been + * replaced by calls to methods of the same name in this delegate class. + * + * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager} + * around to map int to instance of the delegate. + * + * The main goal of this class' methods are to provide a native way to access setters and getters + * on some object. In this case we want to default to using Java reflection instead so the native + * methods do nothing. + * + */ +/*package*/ class PropertyValuesHolder_Delegate { + + @LayoutlibDelegate + /*package*/ static int nGetIntMethod(Class<?> targetClass, String methodName) { + // return 0 to force PropertyValuesHolder to use Java reflection. + return 0; + } + + @LayoutlibDelegate + /*package*/ static int nGetFloatMethod(Class<?> targetClass, String methodName) { + // return 0 to force PropertyValuesHolder to use Java reflection. + return 0; + } + + @LayoutlibDelegate + /*package*/ static void nCallIntMethod(Object target, int methodID, int arg) { + // do nothing + } + + @LayoutlibDelegate + /*package*/ static void nCallFloatMethod(Object target, int methodID, float arg) { + // do nothing + } +} diff --git a/tools/layoutlib/bridge/src/android/app/Fragment_Delegate.java b/tools/layoutlib/bridge/src/android/app/Fragment_Delegate.java new file mode 100644 index 000000000000..aabd3f1fbd4e --- /dev/null +++ b/tools/layoutlib/bridge/src/android/app/Fragment_Delegate.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import com.android.ide.common.rendering.api.IProjectCallback; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.content.Context; +import android.os.Bundle; + +/** + * Delegate used to provide new implementation of a select few methods of {@link Fragment} + * + * Through the layoutlib_create tool, the original methods of Fragment have been replaced + * by calls to methods of the same name in this delegate class. + * + * The methods being re-implemented are the ones responsible for instantiating Fragment objects. + * Because the classes of these objects are found in the project, these methods need access to + * {@link IProjectCallback} object. They are however static methods, so the callback is set + * before the inflation through {@link #setProjectCallback(IProjectCallback)}. + */ +public class Fragment_Delegate { + + private static IProjectCallback sProjectCallback; + + /** + * Sets the current {@link IProjectCallback} to be used to instantiate classes coming + * from the project being rendered. + */ + public static void setProjectCallback(IProjectCallback projectCallback) { + sProjectCallback = projectCallback; + } + + /** + * Like {@link #instantiate(Context, String, Bundle)} but with a null + * argument Bundle. + */ + @LayoutlibDelegate + /*package*/ static Fragment instantiate(Context context, String fname) { + return instantiate(context, fname, null); + } + + /** + * Create a new instance of a Fragment with the given class name. This is + * the same as calling its empty constructor. + * + * @param context The calling context being used to instantiate the fragment. + * This is currently just used to get its ClassLoader. + * @param fname The class name of the fragment to instantiate. + * @param args Bundle of arguments to supply to the fragment, which it + * can retrieve with {@link #getArguments()}. May be null. + * @return Returns a new fragment instance. + * @throws InstantiationException If there is a failure in instantiating + * the given fragment class. This is a runtime exception; it is not + * normally expected to happen. + */ + @LayoutlibDelegate + /*package*/ static Fragment instantiate(Context context, String fname, Bundle args) { + try { + if (sProjectCallback != null) { + Fragment f = (Fragment) sProjectCallback.loadView(fname, + new Class[0], new Object[0]); + + if (args != null) { + args.setClassLoader(f.getClass().getClassLoader()); + f.mArguments = args; + } + return f; + } + + return null; + } catch (ClassNotFoundException e) { + throw new Fragment.InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (java.lang.InstantiationException e) { + throw new Fragment.InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (IllegalAccessException e) { + throw new Fragment.InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (Exception e) { + throw new Fragment.InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } + } +} diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeAssetManager.java b/tools/layoutlib/bridge/src/android/content/res/BridgeAssetManager.java new file mode 100644 index 000000000000..a95391885444 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeAssetManager.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. + */ + +package android.content.res; + +import com.android.layoutlib.bridge.Bridge; + +import android.content.res.AssetManager; + +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. + */ + public static AssetManager initSystem() { + if (!(AssetManager.sSystem instanceof BridgeAssetManager)) { + // Note that AssetManager() creates a system AssetManager and we override it + // with our BridgeAssetManager. + AssetManager.sSystem = new BridgeAssetManager(); + AssetManager.sSystem.makeStringBlocks(false); + } + return AssetManager.sSystem; + } + + /** + * Clears the static {@link AssetManager#sSystem} to make sure we don't leave objects + * around that would prevent us from unloading the library. + */ + public static void clearSystem() { + AssetManager.sSystem = null; + } + + private BridgeAssetManager() { + } +} diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java b/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java new file mode 100644 index 000000000000..879445297ed4 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java @@ -0,0 +1,695 @@ +/* + * 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.content.res; + +import com.android.ide.common.rendering.api.IProjectCallback; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.BridgeConstants; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; +import com.android.layoutlib.bridge.impl.ParserFactory; +import com.android.layoutlib.bridge.impl.ResourceHelper; +import com.android.ninepatch.NinePatch; +import com.android.resources.ResourceType; +import com.android.util.Pair; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +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.InputStream; + +/** + * + */ +public final class BridgeResources extends Resources { + + private BridgeContext mContext; + private IProjectCallback mProjectCallback; + private boolean[] mPlatformResourceFlag = new boolean[1]; + + /** + * Simpler wrapper around FileInputStream. This is used when the input stream represent + * not a normal bitmap but a nine patch. + * This is useful when the InputStream is created in a method but used in another that needs + * to know whether this is 9-patch or not, such as BitmapFactory. + */ + public class NinePatchInputStream extends FileInputStream { + private boolean mFakeMarkSupport = true; + public NinePatchInputStream(File file) throws FileNotFoundException { + super(file); + } + + @Override + public boolean markSupported() { + if (mFakeMarkSupport) { + // this is needed so that BitmapFactory doesn't wrap this in a BufferedInputStream. + return true; + } + + return super.markSupported(); + } + + public void disableFakeMarkSupport() { + // disable fake mark support so that in case codec actually try to use them + // we don't lie to them. + mFakeMarkSupport = false; + } + } + + /** + * 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. + */ + public static Resources initSystem(BridgeContext context, + AssetManager assets, + DisplayMetrics metrics, + Configuration config, + IProjectCallback projectCallback) { + return Resources.mSystem = new BridgeResources(context, + assets, + metrics, + config, + projectCallback); + } + + /** + * Disposes the static {@link Resources#mSystem} to make sure we don't leave objects + * around that would prevent us from unloading the library. + */ + public static void disposeSystem() { + 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 Pair<String, ResourceValue> getResourceValue(int id, boolean[] platformResFlag_out) { + // first get the String related to this id in the framework + Pair<ResourceType, String> resourceInfo = Bridge.resolveResourceId(id); + + if (resourceInfo != null) { + platformResFlag_out[0] = true; + String attributeName = resourceInfo.getSecond(); + + return Pair.of(attributeName, mContext.getRenderResources().getFrameworkResource( + resourceInfo.getFirst(), attributeName)); + } + + // didn't find a match in the framework? look in the project. + if (mProjectCallback != null) { + resourceInfo = mProjectCallback.resolveResourceId(id); + + if (resourceInfo != null) { + platformResFlag_out[0] = false; + String attributeName = resourceInfo.getSecond(); + + return Pair.of(attributeName, mContext.getRenderResources().getProjectResource( + resourceInfo.getFirst(), attributeName)); + } + } + + return null; + } + + @Override + public Drawable getDrawable(int id) throws NotFoundException { + Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + return ResourceHelper.getDrawable(value.getSecond(), mContext); + } + + // 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 { + Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + try { + return ResourceHelper.getColor(value.getSecond().getValue()); + } catch (NumberFormatException e) { + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, e.getMessage(), e, + null /*data*/); + 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 { + Pair<String, ResourceValue> resValue = getResourceValue(id, mPlatformResourceFlag); + + if (resValue != null) { + ColorStateList stateList = ResourceHelper.getColorStateList(resValue.getSecond(), + mContext); + if (stateList != null) { + return stateList; + } + } + + // 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 { + Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + ResourceValue resValue = value.getSecond(); + + assert resValue != null; + if (resValue != null) { + String v = resValue.getValue(); + if (v != null) { + return v; + } + } + } + + // 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 { + Pair<String, ResourceValue> v = getResourceValue(id, mPlatformResourceFlag); + + if (v != null) { + ResourceValue value = v.getSecond(); + XmlPullParser parser = null; + + try { + // check if the current parser can provide us with a custom parser. + if (mPlatformResourceFlag[0] == false) { + parser = mProjectCallback.getParser(value); + } + + // create a new one manually if needed. + if (parser == 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 + parser = ParserFactory.create(xml); + } + } + + if (parser != null) { + return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]); + } + } catch (XmlPullParserException e) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + "Failed to configure parser for " + value.getValue(), e, null /*data*/); + // 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 XmlResourceParser getAnimation(int id) throws NotFoundException { + Pair<String, ResourceValue> v = getResourceValue(id, mPlatformResourceFlag); + + if (v != null) { + ResourceValue value = v.getSecond(); + XmlPullParser parser = null; + + try { + 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 + parser = ParserFactory.create(xml); + + return new BridgeXmlBlockParser(parser, mContext, mPlatformResourceFlag[0]); + } + } catch (XmlPullParserException e) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + "Failed to configure parser for " + value.getValue(), e, null /*data*/); + // 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 { + Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + ResourceValue resValue = value.getSecond(); + + assert resValue != null; + if (resValue != null) { + String v = resValue.getValue(); + if (v != null) { + if (v.equals(BridgeConstants.MATCH_PARENT) || + v.equals(BridgeConstants.FILL_PARENT)) { + return LayoutParams.MATCH_PARENT; + } else if (v.equals(BridgeConstants.WRAP_CONTENT)) { + return LayoutParams.WRAP_CONTENT; + } + + if (ResourceHelper.parseFloatAttribute( + value.getFirst(), v, mTmpValue, true /*requireUnit*/) && + mTmpValue.type == TypedValue.TYPE_DIMENSION) { + return mTmpValue.getDimension(getDisplayMetrics()); + } + } + } + } + + // 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 { + Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + ResourceValue resValue = value.getSecond(); + + assert resValue != null; + if (resValue != null) { + String v = resValue.getValue(); + if (v != null) { + if (ResourceHelper.parseFloatAttribute( + value.getFirst(), v, mTmpValue, true /*requireUnit*/) && + mTmpValue.type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelOffset(mTmpValue.data, + getDisplayMetrics()); + } + } + } + } + + // 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 { + Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + ResourceValue resValue = value.getSecond(); + + assert resValue != null; + if (resValue != null) { + String v = resValue.getValue(); + if (v != null) { + if (ResourceHelper.parseFloatAttribute( + value.getFirst(), v, mTmpValue, true /*requireUnit*/) && + mTmpValue.type == TypedValue.TYPE_DIMENSION) { + return TypedValue.complexToDimensionPixelSize(mTmpValue.data, + getDisplayMetrics()); + } + } + } + } + + // 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 { + Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + ResourceValue resValue = value.getSecond(); + + assert resValue != null; + if (resValue != null) { + String v = resValue.getValue(); + if (v != null) { + 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 boolean getBoolean(int id) throws NotFoundException { + Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + ResourceValue resValue = value.getSecond(); + + assert resValue != null; + if (resValue != null) { + String v = resValue.getValue(); + if (v != null) { + return Boolean.parseBoolean(v); + } + } + } + + // id was not found or not resolved. Throw a NotFoundException. + throwException(id); + + // this is not used since the method above always throws + return false; + } + + @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 { + Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null && value.getSecond().getValue() != null) { + return value.getSecond().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 { + Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getSecond().getValue(); + + if (v != null) { + if (ResourceHelper.parseFloatAttribute(value.getFirst(), v, outValue, + false /*requireUnit*/)) { + return; + } + + // else it's a string + outValue.type = TypedValue.TYPE_STRING; + outValue.string = v; + 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 { + Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String v = value.getSecond().getValue(); + + if (v != null) { + // check this is a file + File f = new File(v); + if (f.isFile()) { + try { + XmlPullParser parser = ParserFactory.create(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 XmlResourceParser loadXmlResourceParser(String file, int id, + int assetCookie, String type) throws NotFoundException { + // even though we know the XML file to load directly, we still need to resolve the + // id so that we can know if it's a platform or project resource. + // (mPlatformResouceFlag will get the result and will be used later). + getResourceValue(id, mPlatformResourceFlag); + + File f = new File(file); + try { + XmlPullParser parser = ParserFactory.create(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; + } + } + + + @Override + public InputStream openRawResource(int id) throws NotFoundException { + Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); + + if (value != null) { + String path = value.getSecond().getValue(); + + if (path != null) { + // check this is a file + File f = new File(path); + if (f.isFile()) { + try { + // if it's a nine-patch return a custom input stream so that + // other methods (mainly bitmap factory) can detect it's a 9-patch + // and actually load it as a 9-patch instead of a normal bitmap + if (path.toLowerCase().endsWith(NinePatch.EXTENSION_9PATCH)) { + return new NinePatchInputStream(f); + } + 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 InputStream openRawResource(int id, TypedValue value) throws NotFoundException { + getValue(id, value, true); + + String path = value.string.toString(); + + File f = new File(path); + if (f.isFile()) { + try { + // if it's a nine-patch return a custom input stream so that + // other methods (mainly bitmap factory) can detect it's a 9-patch + // and actually load it as a 9-patch instead of a normal bitmap + if (path.toLowerCase().endsWith(NinePatch.EXTENSION_9PATCH)) { + return new NinePatchInputStream(f); + } + return new FileInputStream(f); + } catch (FileNotFoundException e) { + NotFoundException exception = new NotFoundException(); + exception.initCause(e); + throw exception; + } + } + + throw new NotFoundException(); + } + + @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 + Pair<ResourceType, String> resourceInfo = Bridge.resolveResourceId(id); + + // if the name is unknown in the framework, get it from the custom view loader. + if (resourceInfo == null && mProjectCallback != null) { + resourceInfo = mProjectCallback.resolveResourceId(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.getFirst(), id, resourceInfo.getSecond()); + } else { + message = String.format( + "Could not resolve resource value: 0x%1$X.", id); + } + + throw new NotFoundException(message); + } +} diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java new file mode 100644 index 000000000000..446d139b2161 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java @@ -0,0 +1,908 @@ +/* + * 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.content.res; + +import com.android.ide.common.rendering.api.AttrResourceValue; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.StyleResourceValue; +import com.android.internal.util.XmlUtils; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.BridgeConstants; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; +import com.android.layoutlib.bridge.impl.ParserFactory; +import com.android.layoutlib.bridge.impl.ResourceHelper; +import com.android.resources.ResourceType; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.LayoutInflater_Delegate; +import android.view.ViewGroup.LayoutParams; + +import java.io.File; +import java.util.Arrays; +import java.util.Map; + +/** + * Custom implementation of TypedArray to handle non compiled resources. + */ +public final class BridgeTypedArray extends TypedArray { + + private final BridgeResources mBridgeResources; + private final BridgeContext mContext; + private final boolean mPlatformFile; + + private ResourceValue[] mResourceData; + private String[] mNames; + private boolean[] mIsFramework; + + public BridgeTypedArray(BridgeResources resources, BridgeContext context, int len, + boolean platformFile) { + super(null, null, null, 0); + mBridgeResources = resources; + mContext = context; + mPlatformFile = platformFile; + mResourceData = new ResourceValue[len]; + mNames = new String[len]; + mIsFramework = new boolean[len]; + } + + /** + * A bridge-specific method that sets a value in the type array + * @param index the index of the value in the TypedArray + * @param name the name of the attribute + * @param isFramework whether the attribute is in the android namespace. + * @param value the value of the attribute + */ + public void bridgeSetValue(int index, String name, boolean isFramework, ResourceValue value) { + mResourceData[index] = value; + mNames[index] = name; + mIsFramework[index] = isFramework; + } + + /** + * Seals the array after all calls to {@link #bridgeSetValue(int, String, ResourceValue)} 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 (ResourceValue data : mResourceData) { + 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 < mResourceData.length ; i++) { + if (mResourceData[i] != null) { + mIndices[index++] = i; + } + } + } + + /** + * Return the number of values in this array. + */ + @Override + public int length() { + return mResourceData.length; + } + + /** + * Return the Resources object this array was loaded from. + */ + @Override + public Resources getResources() { + return mBridgeResources; + } + + /** + * 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 (index < 0 || index >= mResourceData.length) { + return null; + } + + if (mResourceData[index] != null) { + // FIXME: handle styled strings! + return mResourceData[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 (index < 0 || index >= mResourceData.length) { + return null; + } + + if (mResourceData[index] != null) { + return mResourceData[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 (index < 0 || index >= mResourceData.length) { + return defValue; + } + + if (mResourceData[index] == null) { + return defValue; + } + + String s = mResourceData[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 (index < 0 || index >= mResourceData.length) { + return defValue; + } + + if (mResourceData[index] == null) { + return defValue; + } + + String s = mResourceData[index].getValue(); + + if (RenderResources.REFERENCE_NULL.equals(s)) { + return defValue; + } + + if (s == null || s.length() == 0) { + return defValue; + } + + try { + return 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 = null; + if (mIsFramework[index]) { + map = Bridge.getEnumValues(mNames[index]); + } else { + // get the styleable matching the resolved name + RenderResources res = mContext.getRenderResources(); + ResourceValue attr = res.getProjectResource(ResourceType.ATTR, mNames[index]); + if (attr instanceof AttrResourceValue) { + map = ((AttrResourceValue) attr).getAttributeValues(); + } + } + + 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 { + Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT, + String.format( + "\"%s\" in attribute \"%2$s\" is not a valid value", + keyword, mNames[index]), null /*data*/); + } + } + 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 (index < 0 || index >= mResourceData.length) { + return defValue; + } + + if (mResourceData[index] == null) { + return defValue; + } + + String s = mResourceData[index].getValue(); + + if (s != null) { + try { + return Float.parseFloat(s); + } catch (NumberFormatException e) { + Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT, + String.format( + "\"%s\" in attribute \"%2$s\" cannot be converted to float.", + s, mNames[index]), null /*data*/); + + // 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 (index < 0 || index >= mResourceData.length) { + return defValue; + } + + if (mResourceData[index] == null) { + return defValue; + } + + ColorStateList colorStateList = ResourceHelper.getColorStateList( + mResourceData[index], mContext); + if (colorStateList != null) { + return colorStateList.getDefaultColor(); + } + + 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 (index < 0 || index >= mResourceData.length) { + return null; + } + + if (mResourceData[index] == null) { + return null; + } + + ResourceValue resValue = mResourceData[index]; + String value = resValue.getValue(); + + if (value == null) { + return null; + } + + if (RenderResources.REFERENCE_NULL.equals(value)) { + return null; + } + + // let the framework inflate the ColorStateList from the XML file. + File f = new File(value); + if (f.isFile()) { + try { + XmlPullParser parser = ParserFactory.create(f); + + BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( + parser, mContext, resValue.isFramework()); + try { + return ColorStateList.createFromXml(mContext.getResources(), blockParser); + } finally { + blockParser.ensurePopped(); + } + } catch (XmlPullParserException e) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + "Failed to configure parser for " + value, e, null /*data*/); + return null; + } catch (Exception e) { + // this is an error and not warning since the file existence is checked before + // attempting to parse it. + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, + "Failed to parse file " + value, e, null /*data*/); + + return null; + } + } + + try { + int color = ResourceHelper.getColor(value); + return ColorStateList.valueOf(color); + } catch (NumberFormatException e) { + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, e.getMessage(), e, null /*data*/); + } + + 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) { + return getInt(index, 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 (index < 0 || index >= mResourceData.length) { + return defValue; + } + + if (mResourceData[index] == null) { + return defValue; + } + + String s = mResourceData[index].getValue(); + + if (s == null) { + return defValue; + } else if (s.equals(BridgeConstants.MATCH_PARENT) || + s.equals(BridgeConstants.FILL_PARENT)) { + return LayoutParams.MATCH_PARENT; + } else if (s.equals(BridgeConstants.WRAP_CONTENT)) { + return LayoutParams.WRAP_CONTENT; + } else if (RenderResources.REFERENCE_NULL.equals(s)) { + return defValue; + } + + if (ResourceHelper.parseFloatAttribute(mNames[index], s, mValue, true /*requireUnit*/)) { + return mValue.getDimension(mBridgeResources.getDisplayMetrics()); + } + + // looks like we were unable to resolve the dimension value + Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT, + String.format( + "\"%1$s\" in attribute \"%2$s\" is not a valid format.", + s, mNames[index]), null /*data*/); + + 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) { + try { + return getDimension(index); + } catch (RuntimeException e) { + if (mResourceData[index] != null) { + String s = mResourceData[index].getValue(); + + if (s != null) { + // looks like we were unable to resolve the dimension value + Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT, + String.format( + "\"%1$s\" in attribute \"%2$s\" is not a valid format.", + s, mNames[index]), null /*data*/); + } + } + + return defValue; + } + } + + /** + * 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) { + try { + // this will throw an exception + return getDimension(index); + } catch (RuntimeException e) { + + if (LayoutInflater_Delegate.sIsInInclude) { + throw new RuntimeException(); + } + + Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT, + "You must supply a " + name + " attribute.", null); + + return 0; + } + } + + @Override + public int getLayoutDimension(int index, int defValue) { + return getDimensionPixelSize(index, defValue); + } + + private int getDimension(int index) { + if (mResourceData[index] == null) { + throw new RuntimeException(); + } + + String s = mResourceData[index].getValue(); + + if (s == null) { + throw new RuntimeException(); + } else if (s.equals(BridgeConstants.MATCH_PARENT) || + s.equals(BridgeConstants.FILL_PARENT)) { + return LayoutParams.MATCH_PARENT; + } else if (s.equals(BridgeConstants.WRAP_CONTENT)) { + return LayoutParams.WRAP_CONTENT; + } else if (RenderResources.REFERENCE_NULL.equals(s)) { + throw new RuntimeException(); + } + + if (ResourceHelper.parseFloatAttribute(mNames[index], s, mValue, true /*requireUnit*/)) { + float f = mValue.getDimension(mBridgeResources.getDisplayMetrics()); + + final int res = (int)(f+0.5f); + if (res != 0) return res; + if (f == 0) return 0; + if (f > 0) return 1; + } + + throw new RuntimeException(); + } + + /** + * 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 (index < 0 || index >= mResourceData.length) { + return defValue; + } + + if (mResourceData[index] == null) { + return defValue; + } + + String value = mResourceData[index].getValue(); + if (value == null) { + return defValue; + } + + if (ResourceHelper.parseFloatAttribute(mNames[index], value, mValue, + false /*requireUnit*/)) { + return mValue.getFraction(base, pbase); + } + + // looks like we were unable to resolve the fraction value + Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT, + String.format( + "\"%1$s\" in attribute \"%2$s\" cannot be converted to a fraction.", + value, mNames[index]), null /*data*/); + + 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) { + if (index < 0 || index >= mResourceData.length) { + return defValue; + } + + // get the Resource for this index + ResourceValue resValue = mResourceData[index]; + + // no data, return the default value. + if (resValue == null) { + return defValue; + } + + // check if this is a style resource + if (resValue instanceof StyleResourceValue) { + // get the id that will represent this style. + return mContext.getDynamicIdByStyle((StyleResourceValue)resValue); + } + + if (RenderResources.REFERENCE_NULL.equals(resValue.getValue())) { + return defValue; + } + + // if the attribute was a reference to a resource, and not a declaration of an id (@+id), + // then the xml attribute value was "resolved" which leads us to a ResourceValue with a + // valid getType() and getName() returning a resource name. + // (and getValue() returning null!). We need to handle this! + if (resValue.getResourceType() != null) { + // if this is a framework id + if (mPlatformFile || resValue.isFramework()) { + // look for idName in the android R classes + return mContext.getFrameworkResourceValue( + resValue.getResourceType(), resValue.getName(), defValue); + } + + // look for idName in the project R class. + return mContext.getProjectResourceValue( + resValue.getResourceType(), 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.getFrameworkResourceValue(ResourceType.ID, idName, defValue); + } + + // look for idName in the project R class. + return mContext.getProjectResourceValue(ResourceType.ID, idName, defValue); + } + + // not a direct id valid reference? resolve it + Integer idValue = null; + + if (resValue.isFramework()) { + idValue = Bridge.getResourceId(resValue.getResourceType(), + resValue.getName()); + } else { + idValue = mContext.getProjectCallback().getResourceId( + resValue.getResourceType(), resValue.getName()); + } + + if (idValue != null) { + return idValue.intValue(); + } + + Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_RESOLVE, + String.format( + "Unable to resolve id \"%1$s\" for attribute \"%2$s\"", value, mNames[index]), + resValue); + + 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 (index < 0 || index >= mResourceData.length) { + return null; + } + + if (mResourceData[index] == null) { + return null; + } + + ResourceValue value = mResourceData[index]; + String stringValue = value.getValue(); + if (stringValue == null || RenderResources.REFERENCE_NULL.equals(stringValue)) { + return null; + } + + return ResourceHelper.getDrawable(value, mContext); + } + + + /** + * 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 (index < 0 || index >= mResourceData.length) { + return null; + } + + if (mResourceData[index] == null) { + return null; + } + + String value = mResourceData[index].getValue(); + if (value != null) { + if (RenderResources.REFERENCE_NULL.equals(value)) { + return null; + } + + return new CharSequence[] { value }; + } + + Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_FORMAT, + String.format( + String.format("Unknown value for getTextArray(%d) => %s", //DEBUG + index, mResourceData[index].getName())), null /*data*/); + + return null; + } + + /** + * 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 (index < 0 || index >= mResourceData.length) { + return false; + } + + if (mResourceData[index] == null) { + return false; + } + + String s = mResourceData[index].getValue(); + + return ResourceHelper.parseFloatAttribute(mNames[index], s, outValue, + false /*requireUnit*/); + } + + /** + * 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) { + if (index < 0 || index >= mResourceData.length) { + return false; + } + + return mResourceData[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 (index < 0 || index >= mResourceData.length) { + return null; + } + + 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 TypedArray, for later re-use. + */ + @Override + public void recycle() { + // pass + } + + @Override + public String toString() { + return Arrays.toString(mResourceData); + } + } diff --git a/tools/layoutlib/bridge/src/android/content/res/Resources_Theme_Delegate.java b/tools/layoutlib/bridge/src/android/content/res/Resources_Theme_Delegate.java new file mode 100644 index 000000000000..c9d615c6ec6f --- /dev/null +++ b/tools/layoutlib/bridge/src/android/content/res/Resources_Theme_Delegate.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.res; + +import com.android.layoutlib.bridge.impl.RenderSessionImpl; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.content.res.Resources.NotFoundException; +import android.content.res.Resources.Theme; +import android.util.AttributeSet; +import android.util.TypedValue; + +/** + * Delegate used to provide new implementation of a select few methods of {@link Resources$Theme} + * + * Through the layoutlib_create tool, the original methods of Theme have been replaced + * by calls to methods of the same name in this delegate class. + * + */ +public class Resources_Theme_Delegate { + + @LayoutlibDelegate + /*package*/ static TypedArray obtainStyledAttributes( + Resources thisResources, Theme thisTheme, + int[] attrs) { + return RenderSessionImpl.getCurrentContext().obtainStyledAttributes(attrs); + } + + @LayoutlibDelegate + /*package*/ static TypedArray obtainStyledAttributes( + Resources thisResources, Theme thisTheme, + int resid, int[] attrs) + throws NotFoundException { + return RenderSessionImpl.getCurrentContext().obtainStyledAttributes(resid, attrs); + } + + @LayoutlibDelegate + /*package*/ static TypedArray obtainStyledAttributes( + Resources thisResources, Theme thisTheme, + AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) { + return RenderSessionImpl.getCurrentContext().obtainStyledAttributes( + set, attrs, defStyleAttr, defStyleRes); + } + + @LayoutlibDelegate + /*package*/ static boolean resolveAttribute( + Resources thisResources, Theme thisTheme, + int resid, TypedValue outValue, + boolean resolveRefs) { + return RenderSessionImpl.getCurrentContext().resolveThemeAttribute( + resid, outValue, resolveRefs); + } +} diff --git a/tools/layoutlib/bridge/src/android/content/res/TypedArray_Delegate.java b/tools/layoutlib/bridge/src/android/content/res/TypedArray_Delegate.java new file mode 100644 index 000000000000..0a7899a09beb --- /dev/null +++ b/tools/layoutlib/bridge/src/android/content/res/TypedArray_Delegate.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.res; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.util.TypedValue; + +public class TypedArray_Delegate { + + @LayoutlibDelegate + public static boolean getValueAt(TypedArray theTypedArray, int index, TypedValue outValue) { + // pass + return false; + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/AvoidXfermode_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/AvoidXfermode_Delegate.java new file mode 100644 index 000000000000..a50a2bd039f5 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/AvoidXfermode_Delegate.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.Composite; + +/** + * Delegate implementing the native methods of android.graphics.AvoidXfermode + * + * Through the layoutlib_create tool, the original native methods of AvoidXfermode have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original AvoidXfermode class. + * + * Because this extends {@link Xfermode_Delegate}, there's no need to use a + * {@link DelegateManager}, as all the PathEffect classes will be added to the manager owned by + * {@link Xfermode_Delegate}. + * + */ +public class AvoidXfermode_Delegate extends Xfermode_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public Composite getComposite(int alpha) { + // FIXME + return null; + } + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Avoid Xfermodes are not supported in Layout Preview mode."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate(int opColor, int tolerance, int nativeMode) { + AvoidXfermode_Delegate newDelegate = new AvoidXfermode_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java new file mode 100644 index 000000000000..5256b583f8d3 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.ninepatch.NinePatchChunk; +import com.android.resources.Density; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.content.res.BridgeResources.NinePatchInputStream; +import android.graphics.BitmapFactory.Options; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; + +/** + * Delegate implementing the native methods of android.graphics.BitmapFactory + * + * Through the layoutlib_create tool, the original native methods of BitmapFactory have been + * replaced by calls to methods of the same name in this delegate class. + * + * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager} + * around to map int to instance of the delegate. + * + */ +/*package*/ class BitmapFactory_Delegate { + + // ------ Java delegates ------ + + @LayoutlibDelegate + /*package*/ static Bitmap finishDecode(Bitmap bm, Rect outPadding, Options opts) { + if (bm == null || opts == null) { + return bm; + } + + final int density = opts.inDensity; + if (density == 0) { + return bm; + } + + bm.setDensity(density); + final int targetDensity = opts.inTargetDensity; + if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) { + return bm; + } + + byte[] np = bm.getNinePatchChunk(); + final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np); + // DELEGATE CHANGE: never scale 9-patch + if (opts.inScaled && isNinePatch == false) { + float scale = targetDensity / (float)density; + // TODO: This is very inefficient and should be done in native by Skia + final Bitmap oldBitmap = bm; + bm = Bitmap.createScaledBitmap(oldBitmap, (int) (bm.getWidth() * scale + 0.5f), + (int) (bm.getHeight() * scale + 0.5f), true); + oldBitmap.recycle(); + + if (isNinePatch) { + np = nativeScaleNinePatch(np, scale, outPadding); + bm.setNinePatchChunk(np); + } + bm.setDensity(targetDensity); + } + + return bm; + } + + + // ------ Native Delegates ------ + + @LayoutlibDelegate + /*package*/ static Bitmap nativeDecodeStream(InputStream is, byte[] storage, + Rect padding, Options opts) { + return nativeDecodeStream(is, storage, padding, opts, false, 1.f); + } + + @LayoutlibDelegate + /*package*/ static Bitmap nativeDecodeStream(InputStream is, byte[] storage, + Rect padding, Options opts, boolean applyScale, float scale) { + Bitmap bm = null; + + //TODO support rescaling + + Density density = Density.MEDIUM; + if (opts != null) { + density = Density.getEnum(opts.inDensity); + } + + try { + if (is instanceof NinePatchInputStream) { + NinePatchInputStream npis = (NinePatchInputStream) is; + npis.disableFakeMarkSupport(); + + // load the bitmap as a nine patch + com.android.ninepatch.NinePatch ninePatch = com.android.ninepatch.NinePatch.load( + npis, true /*is9Patch*/, false /*convert*/); + + // get the bitmap and chunk objects. + bm = Bitmap_Delegate.createBitmap(ninePatch.getImage(), true /*isMutable*/, + density); + NinePatchChunk chunk = ninePatch.getChunk(); + + // put the chunk in the bitmap + bm.setNinePatchChunk(NinePatch_Delegate.serialize(chunk)); + + // read the padding + int[] paddingarray = chunk.getPadding(); + padding.left = paddingarray[0]; + padding.top = paddingarray[1]; + padding.right = paddingarray[2]; + padding.bottom = paddingarray[3]; + } else { + // load the bitmap directly. + bm = Bitmap_Delegate.createBitmap(is, true, density); + } + } catch (IOException e) { + Bridge.getLog().error(null,"Failed to load image" , e, null); + } + + return bm; + } + + @LayoutlibDelegate + /*package*/ static Bitmap nativeDecodeFileDescriptor(FileDescriptor fd, + Rect padding, Options opts) { + opts.inBitmap = null; + return null; + } + + @LayoutlibDelegate + /*package*/ static Bitmap nativeDecodeAsset(int asset, Rect padding, Options opts) { + opts.inBitmap = null; + return null; + } + + @LayoutlibDelegate + /*package*/ static Bitmap nativeDecodeAsset(int asset, Rect padding, Options opts, + boolean applyScale, float scale) { + opts.inBitmap = null; + return null; + } + + @LayoutlibDelegate + /*package*/ static Bitmap nativeDecodeByteArray(byte[] data, int offset, + int length, Options opts) { + opts.inBitmap = null; + return null; + } + + @LayoutlibDelegate + /*package*/ static byte[] nativeScaleNinePatch(byte[] chunk, float scale, Rect pad) { + // don't scale for now. This should not be called anyway since we re-implement + // BitmapFactory.finishDecode(); + return chunk; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeIsSeekable(FileDescriptor fd) { + return true; + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/BitmapShader_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/BitmapShader_Delegate.java new file mode 100644 index 000000000000..65a75b0ab25d --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/BitmapShader_Delegate.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.graphics.Shader.TileMode; + +/** + * Delegate implementing the native methods of android.graphics.BitmapShader + * + * Through the layoutlib_create tool, the original native methods of BitmapShader have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original BitmapShader class. + * + * Because this extends {@link Shader_Delegate}, there's no need to use a {@link DelegateManager}, + * as all the Shader classes will be added to the manager owned by {@link Shader_Delegate}. + * + * @see Shader_Delegate + * + */ +public class BitmapShader_Delegate extends Shader_Delegate { + + // ---- delegate data ---- + private java.awt.Paint mJavaPaint; + + // ---- Public Helper methods ---- + + @Override + public java.awt.Paint getJavaPaint() { + return mJavaPaint; + } + + @Override + public boolean isSupported() { + return true; + } + + @Override + public String getSupportMessage() { + // no message since isSupported returns true; + return null; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate(int native_bitmap, int shaderTileModeX, + int shaderTileModeY) { + Bitmap_Delegate bitmap = Bitmap_Delegate.getDelegate(native_bitmap); + if (bitmap == null) { + return 0; + } + + BitmapShader_Delegate newDelegate = new BitmapShader_Delegate( + bitmap.getImage(), + Shader_Delegate.getTileMode(shaderTileModeX), + Shader_Delegate.getTileMode(shaderTileModeY)); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static int nativePostCreate(int native_shader, int native_bitmap, + int shaderTileModeX, int shaderTileModeY) { + // pass, not needed. + return 0; + } + + // ---- Private delegate/helper methods ---- + + private BitmapShader_Delegate(java.awt.image.BufferedImage image, + TileMode tileModeX, TileMode tileModeY) { + mJavaPaint = new BitmapShaderPaint(image, tileModeX, tileModeY); + } + + private class BitmapShaderPaint implements java.awt.Paint { + private final java.awt.image.BufferedImage mImage; + private final TileMode mTileModeX; + private final TileMode mTileModeY; + + BitmapShaderPaint(java.awt.image.BufferedImage image, + TileMode tileModeX, TileMode tileModeY) { + mImage = image; + mTileModeX = tileModeX; + mTileModeY = tileModeY; + } + + @Override + public java.awt.PaintContext createContext( + java.awt.image.ColorModel colorModel, + java.awt.Rectangle deviceBounds, + java.awt.geom.Rectangle2D userBounds, + java.awt.geom.AffineTransform xform, + java.awt.RenderingHints hints) { + + java.awt.geom.AffineTransform canvasMatrix; + try { + canvasMatrix = xform.createInverse(); + } catch (java.awt.geom.NoninvertibleTransformException e) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE, + "Unable to inverse matrix in BitmapShader", e, null /*data*/); + canvasMatrix = new java.awt.geom.AffineTransform(); + } + + java.awt.geom.AffineTransform localMatrix = getLocalMatrix(); + try { + localMatrix = localMatrix.createInverse(); + } catch (java.awt.geom.NoninvertibleTransformException e) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE, + "Unable to inverse matrix in BitmapShader", e, null /*data*/); + localMatrix = new java.awt.geom.AffineTransform(); + } + + return new BitmapShaderContext(canvasMatrix, localMatrix, colorModel); + } + + private class BitmapShaderContext implements java.awt.PaintContext { + + private final java.awt.geom.AffineTransform mCanvasMatrix; + private final java.awt.geom.AffineTransform mLocalMatrix; + private final java.awt.image.ColorModel mColorModel; + + public BitmapShaderContext( + java.awt.geom.AffineTransform canvasMatrix, + java.awt.geom.AffineTransform localMatrix, + java.awt.image.ColorModel colorModel) { + mCanvasMatrix = canvasMatrix; + mLocalMatrix = localMatrix; + mColorModel = colorModel; + } + + @Override + public void dispose() { + } + + @Override + public java.awt.image.ColorModel getColorModel() { + return mColorModel; + } + + @Override + public java.awt.image.Raster getRaster(int x, int y, int w, int h) { + java.awt.image.BufferedImage image = new java.awt.image.BufferedImage(w, h, + java.awt.image.BufferedImage.TYPE_INT_ARGB); + + int[] data = new int[w*h]; + + int index = 0; + float[] pt1 = new float[2]; + float[] pt2 = new float[2]; + for (int iy = 0 ; iy < h ; iy++) { + for (int ix = 0 ; ix < w ; ix++) { + // handle the canvas transform + pt1[0] = x + ix; + pt1[1] = y + iy; + mCanvasMatrix.transform(pt1, 0, pt2, 0, 1); + + // handle the local matrix. + pt1[0] = pt2[0]; + pt1[1] = pt2[1]; + mLocalMatrix.transform(pt1, 0, pt2, 0, 1); + + data[index++] = getColor(pt2[0], pt2[1]); + } + } + + image.setRGB(0 /*startX*/, 0 /*startY*/, w, h, data, 0 /*offset*/, w /*scansize*/); + + return image.getRaster(); + } + } + + /** + * Returns a color for an arbitrary point. + */ + private int getColor(float fx, float fy) { + int x = getCoordinate(Math.round(fx), mImage.getWidth(), mTileModeX); + int y = getCoordinate(Math.round(fy), mImage.getHeight(), mTileModeY); + + return mImage.getRGB(x, y); + } + + private int getCoordinate(int i, int size, TileMode mode) { + if (i < 0) { + switch (mode) { + case CLAMP: + i = 0; + break; + case REPEAT: + i = size - 1 - (-i % size); + break; + case MIRROR: + // this is the same as the positive side, just make the value positive + // first. + i = -i; + int count = i / size; + i = i % size; + + if ((count % 2) == 1) { + i = size - 1 - i; + } + break; + } + } else if (i >= size) { + switch (mode) { + case CLAMP: + i = size - 1; + break; + case REPEAT: + i = i % size; + break; + case MIRROR: + int count = i / size; + i = i % size; + + if ((count % 2) == 1) { + i = size - 1 - i; + } + break; + } + } + + return i; + } + + + @Override + public int getTransparency() { + return java.awt.Paint.TRANSLUCENT; + } + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java new file mode 100644 index 000000000000..4121f798ba9a --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java @@ -0,0 +1,592 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.resources.Density; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.graphics.Bitmap.Config; +import android.os.Parcel; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.Buffer; +import java.util.Arrays; + +import javax.imageio.ImageIO; + +/** + * Delegate implementing the native methods of android.graphics.Bitmap + * + * Through the layoutlib_create tool, the original native methods of Bitmap have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original Bitmap class. + * + * @see DelegateManager + * + */ +public final class Bitmap_Delegate { + + // ---- delegate manager ---- + private static final DelegateManager<Bitmap_Delegate> sManager = + new DelegateManager<Bitmap_Delegate>(Bitmap_Delegate.class); + + // ---- delegate helper data ---- + + // ---- delegate data ---- + private final Config mConfig; + private BufferedImage mImage; + private boolean mHasAlpha = true; + private boolean mHasMipMap = false; // TODO: check the default. + private int mGenerationId = 0; + + + // ---- Public Helper methods ---- + + /** + * Returns the native delegate associated to a given {@link Bitmap_Delegate} object. + */ + public static Bitmap_Delegate getDelegate(Bitmap bitmap) { + return sManager.getDelegate(bitmap.mNativeBitmap); + } + + /** + * Returns the native delegate associated to a given an int referencing a {@link Bitmap} object. + */ + public static Bitmap_Delegate getDelegate(int native_bitmap) { + return sManager.getDelegate(native_bitmap); + } + + /** + * Creates and returns a {@link Bitmap} initialized with the given file content. + * + * @param input the file from which to read the bitmap content + * @param isMutable whether the bitmap is mutable + * @param density the density associated with the bitmap + * + * @see Bitmap#isMutable() + * @see Bitmap#getDensity() + */ + public static Bitmap createBitmap(File input, boolean isMutable, Density density) + throws IOException { + // create a delegate with the content of the file. + Bitmap_Delegate delegate = new Bitmap_Delegate(ImageIO.read(input), Config.ARGB_8888); + + return createBitmap(delegate, isMutable, density.getDpiValue()); + } + + /** + * Creates and returns a {@link Bitmap} initialized with the given stream content. + * + * @param input the stream from which to read the bitmap content + * @param isMutable whether the bitmap is mutable + * @param density the density associated with the bitmap + * + * @see Bitmap#isMutable() + * @see Bitmap#getDensity() + */ + public static Bitmap createBitmap(InputStream input, boolean isMutable, Density density) + throws IOException { + // create a delegate with the content of the stream. + Bitmap_Delegate delegate = new Bitmap_Delegate(ImageIO.read(input), Config.ARGB_8888); + + return createBitmap(delegate, isMutable, density.getDpiValue()); + } + + /** + * Creates and returns a {@link Bitmap} initialized with the given {@link BufferedImage} + * + * @param image the bitmap content + * @param isMutable whether the bitmap is mutable + * @param density the density associated with the bitmap + * + * @see Bitmap#isMutable() + * @see Bitmap#getDensity() + */ + public static Bitmap createBitmap(BufferedImage image, boolean isMutable, + Density density) throws IOException { + // create a delegate with the given image. + Bitmap_Delegate delegate = new Bitmap_Delegate(image, Config.ARGB_8888); + + return createBitmap(delegate, isMutable, density.getDpiValue()); + } + + /** + * Returns the {@link BufferedImage} used by the delegate of the given {@link Bitmap}. + */ + public static BufferedImage getImage(Bitmap bitmap) { + // get the delegate from the native int. + Bitmap_Delegate delegate = sManager.getDelegate(bitmap.mNativeBitmap); + if (delegate == null) { + return null; + } + + return delegate.mImage; + } + + public static int getBufferedImageType(int nativeBitmapConfig) { + switch (Config.nativeToConfig(nativeBitmapConfig)) { + case ALPHA_8: + return BufferedImage.TYPE_INT_ARGB; + case RGB_565: + return BufferedImage.TYPE_INT_ARGB; + case ARGB_4444: + return BufferedImage.TYPE_INT_ARGB; + case ARGB_8888: + return BufferedImage.TYPE_INT_ARGB; + } + + return BufferedImage.TYPE_INT_ARGB; + } + + /** + * Returns the {@link BufferedImage} used by the delegate of the given {@link Bitmap}. + */ + public BufferedImage getImage() { + return mImage; + } + + /** + * Returns the Android bitmap config. Note that this not the config of the underlying + * Java2D bitmap. + */ + public Config getConfig() { + return mConfig; + } + + /** + * Returns the hasAlpha rendering hint + * @return true if the bitmap alpha should be used at render time + */ + public boolean hasAlpha() { + return mHasAlpha && mConfig != Config.RGB_565; + } + + public boolean hasMipMap() { + // TODO: check if more checks are required as in hasAlpha. + return mHasMipMap; + } + /** + * Update the generationId. + * + * @see Bitmap#getGenerationId() + */ + public void change() { + mGenerationId++; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static Bitmap nativeCreate(int[] colors, int offset, int stride, int width, + int height, int nativeConfig, boolean mutable) { + int imageType = getBufferedImageType(nativeConfig); + + // create the image + BufferedImage image = new BufferedImage(width, height, imageType); + + if (colors != null) { + image.setRGB(0, 0, width, height, colors, offset, stride); + } + + // create a delegate with the content of the stream. + Bitmap_Delegate delegate = new Bitmap_Delegate(image, Config.nativeToConfig(nativeConfig)); + + return createBitmap(delegate, mutable, Bitmap.getDefaultDensity()); + } + + @LayoutlibDelegate + /*package*/ static Bitmap nativeCopy(int srcBitmap, int nativeConfig, boolean isMutable) { + Bitmap_Delegate srcBmpDelegate = sManager.getDelegate(srcBitmap); + if (srcBmpDelegate == null) { + return null; + } + + BufferedImage srcImage = srcBmpDelegate.getImage(); + + int width = srcImage.getWidth(); + int height = srcImage.getHeight(); + + int imageType = getBufferedImageType(nativeConfig); + + // create the image + BufferedImage image = new BufferedImage(width, height, imageType); + + // copy the source image into the image. + int[] argb = new int[width * height]; + srcImage.getRGB(0, 0, width, height, argb, 0, width); + image.setRGB(0, 0, width, height, argb, 0, width); + + // create a delegate with the content of the stream. + Bitmap_Delegate delegate = new Bitmap_Delegate(image, Config.nativeToConfig(nativeConfig)); + + return createBitmap(delegate, isMutable, Bitmap.getDefaultDensity()); + } + + @LayoutlibDelegate + /*package*/ static void nativeDestructor(int nativeBitmap) { + sManager.removeJavaReferenceFor(nativeBitmap); + } + + @LayoutlibDelegate + /*package*/ static void nativeRecycle(int nativeBitmap) { + sManager.removeJavaReferenceFor(nativeBitmap); + } + + @LayoutlibDelegate + /*package*/ static boolean nativeCompress(int nativeBitmap, int format, int quality, + OutputStream stream, byte[] tempStorage) { + Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, + "Bitmap.compress() is not supported", null /*data*/); + return true; + } + + @LayoutlibDelegate + /*package*/ static void nativeErase(int nativeBitmap, int color) { + // get the delegate from the native int. + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return; + } + + BufferedImage image = delegate.mImage; + + Graphics2D g = image.createGraphics(); + try { + g.setColor(new java.awt.Color(color, true)); + + g.fillRect(0, 0, image.getWidth(), image.getHeight()); + } finally { + g.dispose(); + } + } + + @LayoutlibDelegate + /*package*/ static int nativeWidth(int nativeBitmap) { + // get the delegate from the native int. + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return 0; + } + + return delegate.mImage.getWidth(); + } + + @LayoutlibDelegate + /*package*/ static int nativeHeight(int nativeBitmap) { + // get the delegate from the native int. + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return 0; + } + + return delegate.mImage.getHeight(); + } + + @LayoutlibDelegate + /*package*/ static int nativeRowBytes(int nativeBitmap) { + // get the delegate from the native int. + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return 0; + } + + return delegate.mImage.getWidth(); + } + + @LayoutlibDelegate + /*package*/ static int nativeConfig(int nativeBitmap) { + // get the delegate from the native int. + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return 0; + } + + return delegate.mConfig.nativeInt; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeHasAlpha(int nativeBitmap) { + // get the delegate from the native int. + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return true; + } + + return delegate.mHasAlpha; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeHasMipMap(int nativeBitmap) { + // get the delegate from the native int. + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return true; + } + + return delegate.mHasMipMap; + } + + @LayoutlibDelegate + /*package*/ static int nativeGetPixel(int nativeBitmap, int x, int y) { + // get the delegate from the native int. + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return 0; + } + + return delegate.mImage.getRGB(x, y); + } + + @LayoutlibDelegate + /*package*/ static void nativeGetPixels(int nativeBitmap, int[] pixels, int offset, + int stride, int x, int y, int width, int height) { + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return; + } + + delegate.getImage().getRGB(x, y, width, height, pixels, offset, stride); + } + + + @LayoutlibDelegate + /*package*/ static void nativeSetPixel(int nativeBitmap, int x, int y, int color) { + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return; + } + + delegate.getImage().setRGB(x, y, color); + } + + @LayoutlibDelegate + /*package*/ static void nativeSetPixels(int nativeBitmap, int[] colors, int offset, + int stride, int x, int y, int width, int height) { + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return; + } + + delegate.getImage().setRGB(x, y, width, height, colors, offset, stride); + } + + @LayoutlibDelegate + /*package*/ static void nativeCopyPixelsToBuffer(int nativeBitmap, Buffer dst) { + // FIXME implement native delegate + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Bitmap.copyPixelsToBuffer is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static void nativeCopyPixelsFromBuffer(int nb, Buffer src) { + // FIXME implement native delegate + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Bitmap.copyPixelsFromBuffer is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static int nativeGenerationId(int nativeBitmap) { + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return 0; + } + + return delegate.mGenerationId; + } + + @LayoutlibDelegate + /*package*/ static Bitmap nativeCreateFromParcel(Parcel p) { + // This is only called by Bitmap.CREATOR (Parcelable.Creator<Bitmap>), which is only + // used during aidl call so really this should not be called. + Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, + "AIDL is not suppored, and therefore Bitmaps cannot be created from parcels.", + null /*data*/); + return null; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeWriteToParcel(int nativeBitmap, boolean isMutable, + int density, Parcel p) { + // This is only called when sending a bitmap through aidl, so really this should not + // be called. + Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, + "AIDL is not suppored, and therefore Bitmaps cannot be written to parcels.", + null /*data*/); + return false; + } + + @LayoutlibDelegate + /*package*/ static Bitmap nativeExtractAlpha(int nativeBitmap, int nativePaint, + int[] offsetXY) { + Bitmap_Delegate bitmap = sManager.getDelegate(nativeBitmap); + if (bitmap == null) { + return null; + } + + // get the paint which can be null if nativePaint is 0. + Paint_Delegate paint = Paint_Delegate.getDelegate(nativePaint); + + if (paint != null && paint.getMaskFilter() != null) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_MASKFILTER, + "MaskFilter not supported in Bitmap.extractAlpha", + null, null /*data*/); + } + + int alpha = paint != null ? paint.getAlpha() : 0xFF; + BufferedImage image = createCopy(bitmap.getImage(), BufferedImage.TYPE_INT_ARGB, alpha); + + // create the delegate. The actual Bitmap config is only an alpha channel + Bitmap_Delegate delegate = new Bitmap_Delegate(image, Config.ALPHA_8); + + // the density doesn't matter, it's set by the Java method. + return createBitmap(delegate, false /*isMutable*/, + Density.DEFAULT_DENSITY /*density*/); + } + + @LayoutlibDelegate + /*package*/ static void nativePrepareToDraw(int nativeBitmap) { + // nothing to be done here. + } + + @LayoutlibDelegate + /*package*/ static void nativeSetHasAlpha(int nativeBitmap, boolean hasAlpha) { + // get the delegate from the native int. + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return; + } + + delegate.mHasAlpha = hasAlpha; + } + + @LayoutlibDelegate + /*package*/ static void nativeSetHasMipMap(int nativeBitmap, boolean hasMipMap) { + // get the delegate from the native int. + Bitmap_Delegate delegate = sManager.getDelegate(nativeBitmap); + if (delegate == null) { + return; + } + + delegate.mHasMipMap = hasMipMap; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeSameAs(int nb0, int nb1) { + Bitmap_Delegate delegate1 = sManager.getDelegate(nb0); + if (delegate1 == null) { + return false; + } + + Bitmap_Delegate delegate2 = sManager.getDelegate(nb1); + if (delegate2 == null) { + return false; + } + + BufferedImage image1 = delegate1.getImage(); + BufferedImage image2 = delegate2.getImage(); + if (delegate1.mConfig != delegate2.mConfig || + image1.getWidth() != image2.getWidth() || + image1.getHeight() != image2.getHeight()) { + return false; + } + + // get the internal data + int w = image1.getWidth(); + int h = image2.getHeight(); + int[] argb1 = new int[w*h]; + int[] argb2 = new int[w*h]; + + image1.getRGB(0, 0, w, h, argb1, 0, w); + image2.getRGB(0, 0, w, h, argb2, 0, w); + + // compares + if (delegate1.mConfig == Config.ALPHA_8) { + // in this case we have to manually compare the alpha channel as the rest is garbage. + final int length = w*h; + for (int i = 0 ; i < length ; i++) { + if ((argb1[i] & 0xFF000000) != (argb2[i] & 0xFF000000)) { + return false; + } + } + return true; + } + + return Arrays.equals(argb1, argb2); + } + + // ---- Private delegate/helper methods ---- + + private Bitmap_Delegate(BufferedImage image, Config config) { + mImage = image; + mConfig = config; + } + + private static Bitmap createBitmap(Bitmap_Delegate delegate, boolean isMutable, int density) { + // get its native_int + int nativeInt = sManager.addNewDelegate(delegate); + + // and create/return a new Bitmap with it + // TODO: pass correct width, height, isPremultiplied + return new Bitmap(nativeInt, null /* buffer */, -1 /* width */, -1 /* height */, density, + isMutable, true /* isPremultiplied */, + null /*ninePatchChunk*/, null /* layoutBounds */); + } + + /** + * Creates and returns a copy of a given BufferedImage. + * <p/> + * if alpha is different than 255, then it is applied to the alpha channel of each pixel. + * + * @param image the image to copy + * @param imageType the type of the new image + * @param alpha an optional alpha modifier + * @return a new BufferedImage + */ + /*package*/ static BufferedImage createCopy(BufferedImage image, int imageType, int alpha) { + int w = image.getWidth(); + int h = image.getHeight(); + + BufferedImage result = new BufferedImage(w, h, imageType); + + int[] argb = new int[w * h]; + image.getRGB(0, 0, image.getWidth(), image.getHeight(), argb, 0, image.getWidth()); + + if (alpha != 255) { + final int length = argb.length; + for (int i = 0 ; i < length; i++) { + int a = (argb[i] >>> 24 * alpha) / 255; + argb[i] = (a << 24) | (argb[i] & 0x00FFFFFF); + } + } + + result.setRGB(0, 0, w, h, argb, 0, w); + + return result; + } + +} diff --git a/tools/layoutlib/bridge/src/android/graphics/BlurMaskFilter_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/BlurMaskFilter_Delegate.java new file mode 100644 index 000000000000..4becba130127 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/BlurMaskFilter_Delegate.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.graphics.BlurMaskFilter + * + * Through the layoutlib_create tool, the original native methods of BlurMaskFilter have + * been replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original BlurMaskFilter class. + * + * Because this extends {@link MaskFilter_Delegate}, there's no need to use a + * {@link DelegateManager}, as all the Shader classes will be added to the manager + * owned by {@link MaskFilter_Delegate}. + * + * @see MaskFilter_Delegate + * + */ +public class BlurMaskFilter_Delegate extends MaskFilter_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Blur Mask Filters are not supported."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeConstructor(float radius, int style) { + BlurMaskFilter_Delegate newDelegate = new BlurMaskFilter_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java new file mode 100644 index 000000000000..361f5d7a8bef --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java @@ -0,0 +1,1362 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.layoutlib.bridge.impl.GcSnapshot; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.graphics.Bitmap.Config; +import android.graphics.Paint_Delegate.FontInfo; +import android.text.TextUtils; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Arc2D; +import java.awt.image.BufferedImage; +import java.util.List; + + +/** + * Delegate implementing the native methods of android.graphics.Canvas + * + * Through the layoutlib_create tool, the original native methods of Canvas have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original Canvas class. + * + * @see DelegateManager + * + */ +public final class Canvas_Delegate { + + // ---- delegate manager ---- + private static final DelegateManager<Canvas_Delegate> sManager = + new DelegateManager<Canvas_Delegate>(Canvas_Delegate.class); + + // ---- delegate helper data ---- + + private final static boolean[] sBoolOut = new boolean[1]; + + // ---- delegate data ---- + private Bitmap_Delegate mBitmap; + private GcSnapshot mSnapshot; + + private DrawFilter_Delegate mDrawFilter = null; + + // ---- Public Helper methods ---- + + /** + * Returns the native delegate associated to a given {@link Canvas} object. + */ + public static Canvas_Delegate getDelegate(Canvas canvas) { + return sManager.getDelegate(canvas.mNativeCanvas); + } + + /** + * Returns the native delegate associated to a given an int referencing a {@link Canvas} object. + */ + public static Canvas_Delegate getDelegate(int native_canvas) { + return sManager.getDelegate(native_canvas); + } + + /** + * Returns the current {@link Graphics2D} used to draw. + */ + public GcSnapshot getSnapshot() { + return mSnapshot; + } + + /** + * Returns the {@link DrawFilter} delegate or null if none have been set. + * + * @return the delegate or null. + */ + public DrawFilter_Delegate getDrawFilter() { + return mDrawFilter; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static boolean isOpaque(Canvas thisCanvas) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(thisCanvas.mNativeCanvas); + if (canvasDelegate == null) { + return false; + } + + return canvasDelegate.mBitmap.getConfig() == Config.RGB_565; + } + + @LayoutlibDelegate + /*package*/ static int getWidth(Canvas thisCanvas) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(thisCanvas.mNativeCanvas); + if (canvasDelegate == null) { + return 0; + } + + return canvasDelegate.mBitmap.getImage().getWidth(); + } + + @LayoutlibDelegate + /*package*/ static int getHeight(Canvas thisCanvas) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(thisCanvas.mNativeCanvas); + if (canvasDelegate == null) { + return 0; + } + + return canvasDelegate.mBitmap.getImage().getHeight(); + } + + @LayoutlibDelegate + /*package*/ static void translate(Canvas thisCanvas, float dx, float dy) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(thisCanvas.mNativeCanvas); + if (canvasDelegate == null) { + return; + } + + canvasDelegate.getSnapshot().translate(dx, dy); + } + + @LayoutlibDelegate + /*package*/ static void rotate(Canvas thisCanvas, float degrees) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(thisCanvas.mNativeCanvas); + if (canvasDelegate == null) { + return; + } + + canvasDelegate.getSnapshot().rotate(Math.toRadians(degrees)); + } + + @LayoutlibDelegate + /*package*/ static void scale(Canvas thisCanvas, float sx, float sy) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(thisCanvas.mNativeCanvas); + if (canvasDelegate == null) { + return; + } + + canvasDelegate.getSnapshot().scale(sx, sy); + } + + @LayoutlibDelegate + /*package*/ static void skew(Canvas thisCanvas, float kx, float ky) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(thisCanvas.mNativeCanvas); + if (canvasDelegate == null) { + return; + } + + // get the current top graphics2D object. + GcSnapshot g = canvasDelegate.getSnapshot(); + + // get its current matrix + AffineTransform currentTx = g.getTransform(); + // get the AffineTransform for the given skew. + float[] mtx = Matrix_Delegate.getSkew(kx, ky); + AffineTransform matrixTx = Matrix_Delegate.getAffineTransform(mtx); + + // combine them so that the given matrix is applied after. + currentTx.preConcatenate(matrixTx); + + // give it to the graphics2D as a new matrix replacing all previous transform + g.setTransform(currentTx); + } + + @LayoutlibDelegate + /*package*/ static boolean clipRect(Canvas thisCanvas, RectF rect) { + return clipRect(thisCanvas, rect.left, rect.top, rect.right, rect.bottom); + } + + @LayoutlibDelegate + /*package*/ static boolean clipRect(Canvas thisCanvas, Rect rect) { + return clipRect(thisCanvas, (float) rect.left, (float) rect.top, + (float) rect.right, (float) rect.bottom); + } + + @LayoutlibDelegate + /*package*/ static boolean clipRect(Canvas thisCanvas, float left, float top, float right, + float bottom) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(thisCanvas.mNativeCanvas); + if (canvasDelegate == null) { + return false; + } + + return canvasDelegate.clipRect(left, top, right, bottom, Region.Op.INTERSECT.nativeInt); + } + + @LayoutlibDelegate + /*package*/ static boolean clipRect(Canvas thisCanvas, int left, int top, int right, + int bottom) { + + return clipRect(thisCanvas, (float) left, (float) top, (float) right, (float) bottom); + } + + @LayoutlibDelegate + /*package*/ static int save(Canvas thisCanvas) { + return save(thisCanvas, Canvas.MATRIX_SAVE_FLAG | Canvas.CLIP_SAVE_FLAG); + } + + @LayoutlibDelegate + /*package*/ static int save(Canvas thisCanvas, int saveFlags) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(thisCanvas.mNativeCanvas); + if (canvasDelegate == null) { + return 0; + } + + return canvasDelegate.save(saveFlags); + } + + @LayoutlibDelegate + /*package*/ static void restore(Canvas thisCanvas) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(thisCanvas.mNativeCanvas); + if (canvasDelegate == null) { + return; + } + + canvasDelegate.restore(); + } + + @LayoutlibDelegate + /*package*/ static int getSaveCount(Canvas thisCanvas) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(thisCanvas.mNativeCanvas); + if (canvasDelegate == null) { + return 0; + } + + return canvasDelegate.getSnapshot().size(); + } + + @LayoutlibDelegate + /*package*/ static void restoreToCount(Canvas thisCanvas, int saveCount) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(thisCanvas.mNativeCanvas); + if (canvasDelegate == null) { + return; + } + + canvasDelegate.restoreTo(saveCount); + } + + @LayoutlibDelegate + /*package*/ static void drawPoints(Canvas thisCanvas, float[] pts, int offset, int count, + Paint paint) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Canvas.drawPoint is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static void drawPoint(Canvas thisCanvas, float x, float y, Paint paint) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Canvas.drawPoint is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static void drawLines(Canvas thisCanvas, + final float[] pts, final int offset, final int count, + Paint paint) { + draw(thisCanvas.mNativeCanvas, paint.mNativePaint, false /*compositeOnly*/, + false /*forceSrcMode*/, new GcSnapshot.Drawable() { + @Override + public void draw(Graphics2D graphics, Paint_Delegate paintDelegate) { + for (int i = 0 ; i < count ; i += 4) { + graphics.drawLine((int)pts[i + offset], (int)pts[i + offset + 1], + (int)pts[i + offset + 2], (int)pts[i + offset + 3]); + } + } + }); + } + + @LayoutlibDelegate + /*package*/ static void freeCaches() { + // nothing to be done here. + } + + @LayoutlibDelegate + /*package*/ static void freeTextLayoutCaches() { + // nothing to be done here yet. + } + + @LayoutlibDelegate + /*package*/ static int initRaster(int nativeBitmapOrZero) { + if (nativeBitmapOrZero > 0) { + // get the Bitmap from the int + Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(nativeBitmapOrZero); + + // create a new Canvas_Delegate with the given bitmap and return its new native int. + Canvas_Delegate newDelegate = new Canvas_Delegate(bitmapDelegate); + + return sManager.addNewDelegate(newDelegate); + } + + // create a new Canvas_Delegate and return its new native int. + Canvas_Delegate newDelegate = new Canvas_Delegate(); + + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static void copyNativeCanvasState(int srcCanvas, int dstCanvas) { + // get the delegate from the native int. + Canvas_Delegate srcCanvasDelegate = sManager.getDelegate(srcCanvas); + if (srcCanvasDelegate == null) { + return; + } + + // get the delegate from the native int. + Canvas_Delegate dstCanvasDelegate = sManager.getDelegate(dstCanvas); + if (dstCanvasDelegate == null) { + return; + } + // TODO: actually copy the canvas state. + } + + @LayoutlibDelegate + /*package*/ static int native_saveLayer(int nativeCanvas, RectF bounds, + int paint, int layerFlags) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); + if (canvasDelegate == null) { + return 0; + } + + Paint_Delegate paintDelegate = Paint_Delegate.getDelegate(paint); + if (paintDelegate == null) { + return 0; + } + + return canvasDelegate.saveLayer(bounds, paintDelegate, layerFlags); + } + + @LayoutlibDelegate + /*package*/ static int native_saveLayer(int nativeCanvas, float l, + float t, float r, float b, + int paint, int layerFlags) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); + if (canvasDelegate == null) { + return 0; + } + + Paint_Delegate paintDelegate = Paint_Delegate.getDelegate(paint); + if (paintDelegate == null) { + return 0; + } + + return canvasDelegate.saveLayer(new RectF(l, t, r, b), + paintDelegate, layerFlags); + } + + @LayoutlibDelegate + /*package*/ static int native_saveLayerAlpha(int nativeCanvas, + RectF bounds, int alpha, + int layerFlags) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); + if (canvasDelegate == null) { + return 0; + } + + return canvasDelegate.saveLayerAlpha(bounds, alpha, layerFlags); + } + + @LayoutlibDelegate + /*package*/ static int native_saveLayerAlpha(int nativeCanvas, float l, + float t, float r, float b, + int alpha, int layerFlags) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); + if (canvasDelegate == null) { + return 0; + } + + return canvasDelegate.saveLayerAlpha(new RectF(l, t, r, b), alpha, layerFlags); + } + + + @LayoutlibDelegate + /*package*/ static void native_concat(int nCanvas, int nMatrix) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nCanvas); + if (canvasDelegate == null) { + return; + } + + Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(nMatrix); + if (matrixDelegate == null) { + return; + } + + // get the current top graphics2D object. + GcSnapshot snapshot = canvasDelegate.getSnapshot(); + + // get its current matrix + AffineTransform currentTx = snapshot.getTransform(); + // get the AffineTransform of the given matrix + AffineTransform matrixTx = matrixDelegate.getAffineTransform(); + + // combine them so that the given matrix is applied after. + currentTx.concatenate(matrixTx); + + // give it to the graphics2D as a new matrix replacing all previous transform + snapshot.setTransform(currentTx); + } + + @LayoutlibDelegate + /*package*/ static void native_setMatrix(int nCanvas, int nMatrix) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nCanvas); + if (canvasDelegate == null) { + return; + } + + Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(nMatrix); + if (matrixDelegate == null) { + return; + } + + // get the current top graphics2D object. + GcSnapshot snapshot = canvasDelegate.getSnapshot(); + + // get the AffineTransform of the given matrix + AffineTransform matrixTx = matrixDelegate.getAffineTransform(); + + // give it to the graphics2D as a new matrix replacing all previous transform + snapshot.setTransform(matrixTx); + + if (matrixDelegate.hasPerspective()) { + assert false; + Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_AFFINE, + "android.graphics.Canvas#setMatrix(android.graphics.Matrix) only " + + "supports affine transformations.", null, null /*data*/); + } + } + + @LayoutlibDelegate + /*package*/ static boolean native_clipRect(int nCanvas, + float left, float top, + float right, float bottom, + int regionOp) { + + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nCanvas); + if (canvasDelegate == null) { + return false; + } + + return canvasDelegate.clipRect(left, top, right, bottom, regionOp); + } + + @LayoutlibDelegate + /*package*/ static boolean native_clipPath(int nativeCanvas, + int nativePath, + int regionOp) { + Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); + if (canvasDelegate == null) { + return true; + } + + Path_Delegate pathDelegate = Path_Delegate.getDelegate(nativePath); + if (pathDelegate == null) { + return true; + } + + return canvasDelegate.mSnapshot.clip(pathDelegate.getJavaShape(), regionOp); + } + + @LayoutlibDelegate + /*package*/ static boolean native_clipRegion(int nativeCanvas, + int nativeRegion, + int regionOp) { + Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); + if (canvasDelegate == null) { + return true; + } + + Region_Delegate region = Region_Delegate.getDelegate(nativeRegion); + if (region == null) { + return true; + } + + return canvasDelegate.mSnapshot.clip(region.getJavaArea(), regionOp); + } + + @LayoutlibDelegate + /*package*/ static void nativeSetDrawFilter(int nativeCanvas, int nativeFilter) { + Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); + if (canvasDelegate == null) { + return; + } + + canvasDelegate.mDrawFilter = DrawFilter_Delegate.getDelegate(nativeFilter); + + if (canvasDelegate.mDrawFilter != null && + canvasDelegate.mDrawFilter.isSupported() == false) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_DRAWFILTER, + canvasDelegate.mDrawFilter.getSupportMessage(), null, null /*data*/); + } + } + + @LayoutlibDelegate + /*package*/ static boolean native_getClipBounds(int nativeCanvas, + Rect bounds) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); + if (canvasDelegate == null) { + return false; + } + + Rectangle rect = canvasDelegate.getSnapshot().getClip().getBounds(); + if (rect != null && rect.isEmpty() == false) { + bounds.left = rect.x; + bounds.top = rect.y; + bounds.right = rect.x + rect.width; + bounds.bottom = rect.y + rect.height; + return true; + } + + return false; + } + + @LayoutlibDelegate + /*package*/ static void native_getCTM(int canvas, int matrix) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(canvas); + if (canvasDelegate == null) { + return; + } + + Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(matrix); + if (matrixDelegate == null) { + return; + } + + AffineTransform transform = canvasDelegate.getSnapshot().getTransform(); + matrixDelegate.set(Matrix_Delegate.makeValues(transform)); + } + + @LayoutlibDelegate + /*package*/ static boolean native_quickReject(int nativeCanvas, + RectF rect) { + // FIXME properly implement quickReject + return false; + } + + @LayoutlibDelegate + /*package*/ static boolean native_quickReject(int nativeCanvas, + int path) { + // FIXME properly implement quickReject + return false; + } + + @LayoutlibDelegate + /*package*/ static boolean native_quickReject(int nativeCanvas, + float left, float top, + float right, float bottom) { + // FIXME properly implement quickReject + return false; + } + + @LayoutlibDelegate + /*package*/ static void native_drawRGB(int nativeCanvas, int r, int g, int b) { + native_drawColor(nativeCanvas, 0xFF000000 | r << 16 | (g&0xFF) << 8 | (b&0xFF), + PorterDuff.Mode.SRC_OVER.nativeInt); + + } + + @LayoutlibDelegate + /*package*/ static void native_drawARGB(int nativeCanvas, int a, int r, int g, int b) { + native_drawColor(nativeCanvas, a << 24 | (r&0xFF) << 16 | (g&0xFF) << 8 | (b&0xFF), + PorterDuff.Mode.SRC_OVER.nativeInt); + } + + @LayoutlibDelegate + /*package*/ static void native_drawColor(int nativeCanvas, int color) { + native_drawColor(nativeCanvas, color, PorterDuff.Mode.SRC_OVER.nativeInt); + } + + @LayoutlibDelegate + /*package*/ static void native_drawColor(int nativeCanvas, final int color, final int mode) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); + if (canvasDelegate == null) { + return; + } + + final int w = canvasDelegate.mBitmap.getImage().getWidth(); + final int h = canvasDelegate.mBitmap.getImage().getHeight(); + draw(nativeCanvas, new GcSnapshot.Drawable() { + + @Override + public void draw(Graphics2D graphics, Paint_Delegate paint) { + // reset its transform just in case + graphics.setTransform(new AffineTransform()); + + // set the color + graphics.setColor(new Color(color, true /*alpha*/)); + + Composite composite = PorterDuffXfermode_Delegate.getComposite( + PorterDuffXfermode_Delegate.getPorterDuffMode(mode), 0xFF); + if (composite != null) { + graphics.setComposite(composite); + } + + graphics.fillRect(0, 0, w, h); + } + }); + } + + @LayoutlibDelegate + /*package*/ static void native_drawPaint(int nativeCanvas, int paint) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Canvas.drawPaint is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static void native_drawLine(int nativeCanvas, + final float startX, final float startY, final float stopX, final float stopY, + int paint) { + + draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/, + new GcSnapshot.Drawable() { + @Override + public void draw(Graphics2D graphics, Paint_Delegate paintDelegate) { + graphics.drawLine((int)startX, (int)startY, (int)stopX, (int)stopY); + } + }); + } + + @LayoutlibDelegate + /*package*/ static void native_drawRect(int nativeCanvas, RectF rect, + int paint) { + native_drawRect(nativeCanvas, rect.left, rect.top, rect.right, rect.bottom, paint); + } + + @LayoutlibDelegate + /*package*/ static void native_drawRect(int nativeCanvas, + final float left, final float top, final float right, final float bottom, int paint) { + + draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/, + new GcSnapshot.Drawable() { + @Override + public void draw(Graphics2D graphics, Paint_Delegate paintDelegate) { + int style = paintDelegate.getStyle(); + + // draw + if (style == Paint.Style.FILL.nativeInt || + style == Paint.Style.FILL_AND_STROKE.nativeInt) { + graphics.fillRect((int)left, (int)top, + (int)(right-left), (int)(bottom-top)); + } + + if (style == Paint.Style.STROKE.nativeInt || + style == Paint.Style.FILL_AND_STROKE.nativeInt) { + graphics.drawRect((int)left, (int)top, + (int)(right-left), (int)(bottom-top)); + } + } + }); + } + + @LayoutlibDelegate + /*package*/ static void native_drawOval(int nativeCanvas, final RectF oval, int paint) { + if (oval.right > oval.left && oval.bottom > oval.top) { + draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/, + new GcSnapshot.Drawable() { + @Override + public void draw(Graphics2D graphics, Paint_Delegate paintDelegate) { + int style = paintDelegate.getStyle(); + + // draw + if (style == Paint.Style.FILL.nativeInt || + style == Paint.Style.FILL_AND_STROKE.nativeInt) { + graphics.fillOval((int)oval.left, (int)oval.top, + (int)oval.width(), (int)oval.height()); + } + + if (style == Paint.Style.STROKE.nativeInt || + style == Paint.Style.FILL_AND_STROKE.nativeInt) { + graphics.drawOval((int)oval.left, (int)oval.top, + (int)oval.width(), (int)oval.height()); + } + } + }); + } + } + + @LayoutlibDelegate + /*package*/ static void native_drawCircle(int nativeCanvas, + float cx, float cy, float radius, int paint) { + native_drawOval(nativeCanvas, + new RectF(cx - radius, cy - radius, cx + radius, cy + radius), + paint); + } + + @LayoutlibDelegate + /*package*/ static void native_drawArc(int nativeCanvas, + final RectF oval, final float startAngle, final float sweep, + final boolean useCenter, int paint) { + if (oval.right > oval.left && oval.bottom > oval.top) { + draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/, + new GcSnapshot.Drawable() { + @Override + public void draw(Graphics2D graphics, Paint_Delegate paintDelegate) { + int style = paintDelegate.getStyle(); + + Arc2D.Float arc = new Arc2D.Float( + oval.left, oval.top, oval.width(), oval.height(), + -startAngle, -sweep, + useCenter ? Arc2D.PIE : Arc2D.OPEN); + + // draw + if (style == Paint.Style.FILL.nativeInt || + style == Paint.Style.FILL_AND_STROKE.nativeInt) { + graphics.fill(arc); + } + + if (style == Paint.Style.STROKE.nativeInt || + style == Paint.Style.FILL_AND_STROKE.nativeInt) { + graphics.draw(arc); + } + } + }); + } + } + + @LayoutlibDelegate + /*package*/ static void native_drawRoundRect(int nativeCanvas, + final RectF rect, final float rx, final float ry, int paint) { + + draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/, + new GcSnapshot.Drawable() { + @Override + public void draw(Graphics2D graphics, Paint_Delegate paintDelegate) { + int style = paintDelegate.getStyle(); + + // draw + if (style == Paint.Style.FILL.nativeInt || + style == Paint.Style.FILL_AND_STROKE.nativeInt) { + graphics.fillRoundRect( + (int)rect.left, (int)rect.top, + (int)rect.width(), (int)rect.height(), + (int)rx, (int)ry); + } + + if (style == Paint.Style.STROKE.nativeInt || + style == Paint.Style.FILL_AND_STROKE.nativeInt) { + graphics.drawRoundRect( + (int)rect.left, (int)rect.top, + (int)rect.width(), (int)rect.height(), + (int)rx, (int)ry); + } + } + }); + } + + @LayoutlibDelegate + /*package*/ static void native_drawPath(int nativeCanvas, int path, int paint) { + final Path_Delegate pathDelegate = Path_Delegate.getDelegate(path); + if (pathDelegate == null) { + return; + } + + draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/, + new GcSnapshot.Drawable() { + @Override + public void draw(Graphics2D graphics, Paint_Delegate paintDelegate) { + Shape shape = pathDelegate.getJavaShape(); + int style = paintDelegate.getStyle(); + + if (style == Paint.Style.FILL.nativeInt || + style == Paint.Style.FILL_AND_STROKE.nativeInt) { + graphics.fill(shape); + } + + if (style == Paint.Style.STROKE.nativeInt || + style == Paint.Style.FILL_AND_STROKE.nativeInt) { + graphics.draw(shape); + } + } + }); + } + + @LayoutlibDelegate + /*package*/ static void native_drawBitmap(Canvas thisCanvas, int nativeCanvas, int bitmap, + float left, float top, + int nativePaintOrZero, + int canvasDensity, + int screenDensity, + int bitmapDensity) { + // get the delegate from the native int. + Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(bitmap); + if (bitmapDelegate == null) { + return; + } + + BufferedImage image = bitmapDelegate.getImage(); + float right = left + image.getWidth(); + float bottom = top + image.getHeight(); + + drawBitmap(nativeCanvas, bitmapDelegate, nativePaintOrZero, + 0, 0, image.getWidth(), image.getHeight(), + (int)left, (int)top, (int)right, (int)bottom); + } + + @LayoutlibDelegate + /*package*/ static void native_drawBitmap(Canvas thisCanvas, int nativeCanvas, int bitmap, + Rect src, RectF dst, + int nativePaintOrZero, + int screenDensity, + int bitmapDensity) { + // get the delegate from the native int. + Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(bitmap); + if (bitmapDelegate == null) { + return; + } + + BufferedImage image = bitmapDelegate.getImage(); + + if (src == null) { + drawBitmap(nativeCanvas, bitmapDelegate, nativePaintOrZero, + 0, 0, image.getWidth(), image.getHeight(), + (int)dst.left, (int)dst.top, (int)dst.right, (int)dst.bottom); + } else { + drawBitmap(nativeCanvas, bitmapDelegate, nativePaintOrZero, + src.left, src.top, src.width(), src.height(), + (int)dst.left, (int)dst.top, (int)dst.right, (int)dst.bottom); + } + } + + @LayoutlibDelegate + /*package*/ static void native_drawBitmap(int nativeCanvas, int bitmap, + Rect src, Rect dst, + int nativePaintOrZero, + int screenDensity, + int bitmapDensity) { + // get the delegate from the native int. + Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(bitmap); + if (bitmapDelegate == null) { + return; + } + + BufferedImage image = bitmapDelegate.getImage(); + + if (src == null) { + drawBitmap(nativeCanvas, bitmapDelegate, nativePaintOrZero, + 0, 0, image.getWidth(), image.getHeight(), + dst.left, dst.top, dst.right, dst.bottom); + } else { + drawBitmap(nativeCanvas, bitmapDelegate, nativePaintOrZero, + src.left, src.top, src.width(), src.height(), + dst.left, dst.top, dst.right, dst.bottom); + } + } + + @LayoutlibDelegate + /*package*/ static void native_drawBitmap(int nativeCanvas, int[] colors, + int offset, int stride, final float x, + final float y, int width, int height, + boolean hasAlpha, + int nativePaintOrZero) { + + // create a temp BufferedImage containing the content. + final BufferedImage image = new BufferedImage(width, height, + hasAlpha ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB); + image.setRGB(0, 0, width, height, colors, offset, stride); + + draw(nativeCanvas, nativePaintOrZero, true /*compositeOnly*/, false /*forceSrcMode*/, + new GcSnapshot.Drawable() { + @Override + public void draw(Graphics2D graphics, Paint_Delegate paint) { + if (paint != null && paint.isFilterBitmap()) { + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } + + graphics.drawImage(image, (int) x, (int) y, null); + } + }); + } + + @LayoutlibDelegate + /*package*/ static void nativeDrawBitmapMatrix(int nCanvas, int nBitmap, + int nMatrix, int nPaint) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nCanvas); + if (canvasDelegate == null) { + return; + } + + // get the delegate from the native int, which can be null + Paint_Delegate paintDelegate = Paint_Delegate.getDelegate(nPaint); + + // get the delegate from the native int. + Bitmap_Delegate bitmapDelegate = Bitmap_Delegate.getDelegate(nBitmap); + if (bitmapDelegate == null) { + return; + } + + final BufferedImage image = getImageToDraw(bitmapDelegate, paintDelegate, sBoolOut); + + Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(nMatrix); + if (matrixDelegate == null) { + return; + } + + final AffineTransform mtx = matrixDelegate.getAffineTransform(); + + canvasDelegate.getSnapshot().draw(new GcSnapshot.Drawable() { + @Override + public void draw(Graphics2D graphics, Paint_Delegate paint) { + if (paint != null && paint.isFilterBitmap()) { + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } + + //FIXME add support for canvas, screen and bitmap densities. + graphics.drawImage(image, mtx, null); + } + }, paintDelegate, true /*compositeOnly*/, false /*forceSrcMode*/); + } + + @LayoutlibDelegate + /*package*/ static void nativeDrawBitmapMesh(int nCanvas, int nBitmap, + int meshWidth, int meshHeight, float[] verts, int vertOffset, int[] colors, + int colorOffset, int nPaint) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Canvas.drawBitmapMesh is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static void nativeDrawVertices(int nCanvas, int mode, int n, + float[] verts, int vertOffset, + float[] texs, int texOffset, + int[] colors, int colorOffset, + short[] indices, int indexOffset, + int indexCount, int nPaint) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Canvas.drawVertices is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static void native_drawText(int nativeCanvas, + final char[] text, final int index, final int count, + final float startX, final float startY, int flags, int paint) { + draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/, + new GcSnapshot.Drawable() { + @Override + public void draw(Graphics2D graphics, Paint_Delegate paintDelegate) { + // WARNING: the logic in this method is similar to Paint_Delegate.measureText. + // Any change to this method should be reflected in Paint.measureText + // Paint.TextAlign indicates how the text is positioned relative to X. + // LEFT is the default and there's nothing to do. + float x = startX; + float y = startY; + if (paintDelegate.getTextAlign() != Paint.Align.LEFT.nativeInt) { + // TODO: check the value of bidiFlags. + float m = paintDelegate.measureText(text, index, count, 0); + if (paintDelegate.getTextAlign() == Paint.Align.CENTER.nativeInt) { + x -= m / 2; + } else if (paintDelegate.getTextAlign() == Paint.Align.RIGHT.nativeInt) { + x -= m; + } + } + + List<FontInfo> fonts = paintDelegate.getFonts(); + + if (fonts.size() > 0) { + FontInfo mainFont = fonts.get(0); + int i = index; + int lastIndex = index + count; + while (i < lastIndex) { + // always start with the main font. + int upTo = mainFont.mFont.canDisplayUpTo(text, i, lastIndex); + if (upTo == -1) { + // draw all the rest and exit. + graphics.setFont(mainFont.mFont); + graphics.drawChars(text, i, lastIndex - i, (int)x, (int)y); + return; + } else if (upTo > 0) { + // draw what's possible + graphics.setFont(mainFont.mFont); + graphics.drawChars(text, i, upTo - i, (int)x, (int)y); + + // compute the width that was drawn to increase x + x += mainFont.mMetrics.charsWidth(text, i, upTo - i); + + // move index to the first non displayed char. + i = upTo; + + // don't call continue at this point. Since it is certain the main font + // cannot display the font a index upTo (now ==i), we move on to the + // fallback fonts directly. + } + + // no char supported, attempt to read the next char(s) with the + // fallback font. In this case we only test the first character + // and then go back to test with the main font. + // Special test for 2-char characters. + boolean foundFont = false; + for (int f = 1 ; f < fonts.size() ; f++) { + FontInfo fontInfo = fonts.get(f); + + // need to check that the font can display the character. We test + // differently if the char is a high surrogate. + int charCount = Character.isHighSurrogate(text[i]) ? 2 : 1; + upTo = fontInfo.mFont.canDisplayUpTo(text, i, i + charCount); + if (upTo == -1) { + // draw that char + graphics.setFont(fontInfo.mFont); + graphics.drawChars(text, i, charCount, (int)x, (int)y); + + // update x + x += fontInfo.mMetrics.charsWidth(text, i, charCount); + + // update the index in the text, and move on + i += charCount; + foundFont = true; + break; + + } + } + + // in case no font can display the char, display it with the main font. + // (it'll put a square probably) + if (foundFont == false) { + int charCount = Character.isHighSurrogate(text[i]) ? 2 : 1; + + graphics.setFont(mainFont.mFont); + graphics.drawChars(text, i, charCount, (int)x, (int)y); + + // measure it to advance x + x += mainFont.mMetrics.charsWidth(text, i, charCount); + + // and move to the next chars. + i += charCount; + } + } + } + } + }); + } + + @LayoutlibDelegate + /*package*/ static void native_drawText(int nativeCanvas, String text, + int start, int end, float x, float y, int flags, int paint) { + int count = end - start; + char[] buffer = TemporaryBuffer.obtain(count); + TextUtils.getChars(text, start, end, buffer, 0); + + native_drawText(nativeCanvas, buffer, 0, count, x, y, flags, paint); + } + + @LayoutlibDelegate + /*package*/ static void native_drawTextRun(int nativeCanvas, String text, + int start, int end, int contextStart, int contextEnd, + float x, float y, int flags, int paint) { + int count = end - start; + char[] buffer = TemporaryBuffer.obtain(count); + TextUtils.getChars(text, start, end, buffer, 0); + + native_drawText(nativeCanvas, buffer, 0, count, x, y, flags, paint); + } + + @LayoutlibDelegate + /*package*/ static void native_drawTextRun(int nativeCanvas, char[] text, + int start, int count, int contextStart, int contextCount, + float x, float y, int flags, int paint) { + native_drawText(nativeCanvas, text, start, count, x, y, flags, paint); + } + + @LayoutlibDelegate + /*package*/ static void native_drawPosText(int nativeCanvas, + char[] text, int index, + int count, float[] pos, + int paint) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Canvas.drawPosText is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static void native_drawPosText(int nativeCanvas, + String text, float[] pos, + int paint) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Canvas.drawPosText is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static void native_drawTextOnPath(int nativeCanvas, + char[] text, int index, + int count, int path, + float hOffset, + float vOffset, int bidiFlags, + int paint) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Canvas.drawTextOnPath is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static void native_drawTextOnPath(int nativeCanvas, + String text, int path, + float hOffset, + float vOffset, + int flags, int paint) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Canvas.drawTextOnPath is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static void finalizer(int nativeCanvas) { + // get the delegate from the native int so that it can be disposed. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); + if (canvasDelegate == null) { + return; + } + + canvasDelegate.dispose(); + + // remove it from the manager. + sManager.removeJavaReferenceFor(nativeCanvas); + } + + // ---- Private delegate/helper methods ---- + + /** + * Executes a {@link GcSnapshot.Drawable} with a given canvas and paint. + * <p>Note that the drawable may actually be executed several times if there are + * layers involved (see {@link #saveLayer(RectF, int, int)}. + */ + private static void draw(int nCanvas, int nPaint, boolean compositeOnly, boolean forceSrcMode, + GcSnapshot.Drawable drawable) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nCanvas); + if (canvasDelegate == null) { + return; + } + + // get the paint which can be null if nPaint is 0; + Paint_Delegate paintDelegate = Paint_Delegate.getDelegate(nPaint); + + canvasDelegate.getSnapshot().draw(drawable, paintDelegate, compositeOnly, forceSrcMode); + } + + /** + * Executes a {@link GcSnapshot.Drawable} with a given canvas. No paint object will be provided + * to {@link GcSnapshot.Drawable#draw(Graphics2D, Paint_Delegate)}. + * <p>Note that the drawable may actually be executed several times if there are + * layers involved (see {@link #saveLayer(RectF, int, int)}. + */ + private static void draw(int nCanvas, GcSnapshot.Drawable drawable) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nCanvas); + if (canvasDelegate == null) { + return; + } + + canvasDelegate.mSnapshot.draw(drawable); + } + + private Canvas_Delegate(Bitmap_Delegate bitmap) { + mSnapshot = GcSnapshot.createDefaultSnapshot(mBitmap = bitmap); + } + + private Canvas_Delegate() { + mSnapshot = GcSnapshot.createDefaultSnapshot(null /*image*/); + } + + /** + * Disposes of the {@link Graphics2D} stack. + */ + private void dispose() { + mSnapshot.dispose(); + } + + private int save(int saveFlags) { + // get the current save count + int count = mSnapshot.size(); + + mSnapshot = mSnapshot.save(saveFlags); + + // return the old save count + return count; + } + + private int saveLayerAlpha(RectF rect, int alpha, int saveFlags) { + Paint_Delegate paint = new Paint_Delegate(); + paint.setAlpha(alpha); + return saveLayer(rect, paint, saveFlags); + } + + private int saveLayer(RectF rect, Paint_Delegate paint, int saveFlags) { + // get the current save count + int count = mSnapshot.size(); + + mSnapshot = mSnapshot.saveLayer(rect, paint, saveFlags); + + // return the old save count + return count; + } + + /** + * Restores the {@link GcSnapshot} to <var>saveCount</var> + * @param saveCount the saveCount + */ + private void restoreTo(int saveCount) { + mSnapshot = mSnapshot.restoreTo(saveCount); + } + + /** + * Restores the {@link GcSnapshot} to <var>saveCount</var> + * @param saveCount the saveCount + */ + private void restore() { + mSnapshot = mSnapshot.restore(); + } + + private boolean clipRect(float left, float top, float right, float bottom, int regionOp) { + return mSnapshot.clipRect(left, top, right, bottom, regionOp); + } + + private void setBitmap(Bitmap_Delegate bitmap) { + mBitmap = bitmap; + assert mSnapshot.size() == 1; + mSnapshot.setBitmap(mBitmap); + } + + private static void drawBitmap( + int nativeCanvas, + Bitmap_Delegate bitmap, + int nativePaintOrZero, + final int sleft, final int stop, final int sright, final int sbottom, + final int dleft, final int dtop, final int dright, final int dbottom) { + // get the delegate from the native int. + Canvas_Delegate canvasDelegate = sManager.getDelegate(nativeCanvas); + if (canvasDelegate == null) { + return; + } + + // get the paint, which could be null if the int is 0 + Paint_Delegate paintDelegate = Paint_Delegate.getDelegate(nativePaintOrZero); + + final BufferedImage image = getImageToDraw(bitmap, paintDelegate, sBoolOut); + + draw(nativeCanvas, nativePaintOrZero, true /*compositeOnly*/, sBoolOut[0], + new GcSnapshot.Drawable() { + @Override + public void draw(Graphics2D graphics, Paint_Delegate paint) { + if (paint != null && paint.isFilterBitmap()) { + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } + + //FIXME add support for canvas, screen and bitmap densities. + graphics.drawImage(image, dleft, dtop, dright, dbottom, + sleft, stop, sright, sbottom, null); + } + }); + } + + + /** + * Returns a BufferedImage ready for drawing, based on the bitmap and paint delegate. + * The image returns, through a 1-size boolean array, whether the drawing code should + * use a SRC composite no matter what the paint says. + * + * @param bitmap the bitmap + * @param paint the paint that will be used to draw + * @param forceSrcMode whether the composite will have to be SRC + * @return the image to draw + */ + private static BufferedImage getImageToDraw(Bitmap_Delegate bitmap, Paint_Delegate paint, + boolean[] forceSrcMode) { + BufferedImage image = bitmap.getImage(); + forceSrcMode[0] = false; + + // if the bitmap config is alpha_8, then we erase all color value from it + // before drawing it. + if (bitmap.getConfig() == Bitmap.Config.ALPHA_8) { + fixAlpha8Bitmap(image); + } else if (bitmap.hasAlpha() == false) { + // hasAlpha is merely a rendering hint. There can in fact be alpha values + // in the bitmap but it should be ignored at drawing time. + // There is two ways to do this: + // - override the composite to be SRC. This can only be used if the composite + // was going to be SRC or SRC_OVER in the first place + // - Create a different bitmap to draw in which all the alpha channel values is set + // to 0xFF. + if (paint != null) { + Xfermode_Delegate xfermodeDelegate = paint.getXfermode(); + if (xfermodeDelegate instanceof PorterDuffXfermode_Delegate) { + PorterDuff.Mode mode = + ((PorterDuffXfermode_Delegate)xfermodeDelegate).getMode(); + + forceSrcMode[0] = mode == PorterDuff.Mode.SRC_OVER || + mode == PorterDuff.Mode.SRC; + } + } + + // if we can't force SRC mode, then create a temp bitmap of TYPE_RGB + if (forceSrcMode[0] == false) { + image = Bitmap_Delegate.createCopy(image, BufferedImage.TYPE_INT_RGB, 0xFF); + } + } + + return image; + } + + private static void fixAlpha8Bitmap(final BufferedImage image) { + int w = image.getWidth(); + int h = image.getHeight(); + int[] argb = new int[w * h]; + image.getRGB(0, 0, image.getWidth(), image.getHeight(), argb, 0, image.getWidth()); + + final int length = argb.length; + for (int i = 0 ; i < length; i++) { + argb[i] &= 0xFF000000; + } + image.setRGB(0, 0, w, h, argb, 0, w); + } +} + diff --git a/tools/layoutlib/bridge/src/android/graphics/ColorFilter_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/ColorFilter_Delegate.java new file mode 100644 index 000000000000..e5a7ab6d0d23 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/ColorFilter_Delegate.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.graphics.ColorFilter + * + * Through the layoutlib_create tool, the original native methods of ColorFilter have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original ColorFilter class. + * + * This also serve as a base class for all ColorFilter delegate classes. + * + * @see DelegateManager + * + */ +public abstract class ColorFilter_Delegate { + + // ---- delegate manager ---- + protected static final DelegateManager<ColorFilter_Delegate> sManager = + new DelegateManager<ColorFilter_Delegate>(ColorFilter_Delegate.class); + + // ---- delegate helper data ---- + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + public static ColorFilter_Delegate getDelegate(int nativeShader) { + return sManager.getDelegate(nativeShader); + } + + public abstract boolean isSupported(); + public abstract String getSupportMessage(); + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static void finalizer(int native_instance, int nativeColorFilter) { + sManager.removeJavaReferenceFor(native_instance); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/ColorMatrixColorFilter_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/ColorMatrixColorFilter_Delegate.java new file mode 100644 index 000000000000..2de344b41f8a --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/ColorMatrixColorFilter_Delegate.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.graphics.ColorMatrixColorFilter + * + * Through the layoutlib_create tool, the original native methods of ColorMatrixColorFilter have + * been replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original ColorMatrixColorFilter class. + * + * Because this extends {@link ColorFilter_Delegate}, there's no need to use a + * {@link DelegateManager}, as all the Shader classes will be added to the manager + * owned by {@link ColorFilter_Delegate}. + * + * @see ColorFilter_Delegate + * + */ +public class ColorMatrixColorFilter_Delegate extends ColorFilter_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "ColorMatrix Color Filters are not supported."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeColorMatrixFilter(float[] array) { + ColorMatrixColorFilter_Delegate newDelegate = new ColorMatrixColorFilter_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static int nColorMatrixFilter(int nativeFilter, float[] array) { + // pass + return 0; + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/ComposePathEffect_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/ComposePathEffect_Delegate.java new file mode 100644 index 000000000000..7c04a8709db2 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/ComposePathEffect_Delegate.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.Stroke; + +/** + * Delegate implementing the native methods of android.graphics.ComposePathEffect + * + * Through the layoutlib_create tool, the original native methods of ComposePathEffect have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original ComposePathEffect class. + * + * Because this extends {@link PathEffect_Delegate}, there's no need to use a {@link DelegateManager}, + * as all the Shader classes will be added to the manager owned by {@link PathEffect_Delegate}. + * + * @see PathEffect_Delegate + * + */ +public class ComposePathEffect_Delegate extends PathEffect_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public Stroke getStroke(Paint_Delegate paint) { + // FIXME + return null; + } + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Compose Path Effects are not supported in Layout Preview mode."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate(int outerpe, int innerpe) { + ComposePathEffect_Delegate newDelegate = new ComposePathEffect_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/ComposeShader_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/ComposeShader_Delegate.java new file mode 100644 index 000000000000..f6e1d0094925 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/ComposeShader_Delegate.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.Paint; + +/** + * Delegate implementing the native methods of android.graphics.ComposeShader + * + * Through the layoutlib_create tool, the original native methods of ComposeShader have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original ComposeShader class. + * + * Because this extends {@link Shader_Delegate}, there's no need to use a {@link DelegateManager}, + * as all the Shader classes will be added to the manager owned by {@link Shader_Delegate}. + * + * @see Shader_Delegate + * + */ +public class ComposeShader_Delegate extends Shader_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public Paint getJavaPaint() { + // FIXME + return null; + } + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Compose Shaders are not supported in Layout Preview mode."; + } + + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate1(int native_shaderA, int native_shaderB, + int native_mode) { + // FIXME not supported yet. + ComposeShader_Delegate newDelegate = new ComposeShader_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static int nativeCreate2(int native_shaderA, int native_shaderB, + int porterDuffMode) { + // FIXME not supported yet. + ComposeShader_Delegate newDelegate = new ComposeShader_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static int nativePostCreate1(int native_shader, int native_skiaShaderA, + int native_skiaShaderB, int native_mode) { + // pass, not needed. + return 0; + } + + @LayoutlibDelegate + /*package*/ static int nativePostCreate2(int native_shader, int native_skiaShaderA, + int native_skiaShaderB, int porterDuffMode) { + // pass, not needed. + return 0; + } + + // ---- Private delegate/helper methods ---- + +} diff --git a/tools/layoutlib/bridge/src/android/graphics/CornerPathEffect_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/CornerPathEffect_Delegate.java new file mode 100644 index 000000000000..b0f8168aa3a0 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/CornerPathEffect_Delegate.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.Stroke; + +/** + * Delegate implementing the native methods of android.graphics.CornerPathEffect + * + * Through the layoutlib_create tool, the original native methods of CornerPathEffect have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original CornerPathEffect class. + * + * Because this extends {@link PathEffect_Delegate}, there's no need to use a {@link DelegateManager}, + * as all the Shader classes will be added to the manager owned by {@link PathEffect_Delegate}. + * + * @see PathEffect_Delegate + * + */ +public class CornerPathEffect_Delegate extends PathEffect_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public Stroke getStroke(Paint_Delegate paint) { + // FIXME + return null; + } + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Corner Path Effects are not supported in Layout Preview mode."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate(float radius) { + CornerPathEffect_Delegate newDelegate = new CornerPathEffect_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/DashPathEffect_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/DashPathEffect_Delegate.java new file mode 100644 index 000000000000..d97c2eccd508 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/DashPathEffect_Delegate.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.BasicStroke; +import java.awt.Stroke; + +/** + * Delegate implementing the native methods of android.graphics.DashPathEffect + * + * Through the layoutlib_create tool, the original native methods of DashPathEffect have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original DashPathEffect class. + * + * Because this extends {@link PathEffect_Delegate}, there's no need to use a + * {@link DelegateManager}, as all the PathEffect classes will be added to the manager owned by + * {@link PathEffect_Delegate}. + * + * @see PathEffect_Delegate + * + */ +public final class DashPathEffect_Delegate extends PathEffect_Delegate { + + // ---- delegate data ---- + + private final float[] mIntervals; + private final float mPhase; + + // ---- Public Helper methods ---- + + @Override + public Stroke getStroke(Paint_Delegate paint) { + return new BasicStroke( + paint.getStrokeWidth(), + paint.getJavaCap(), + paint.getJavaJoin(), + paint.getJavaStrokeMiter(), + mIntervals, + mPhase); + } + + @Override + public boolean isSupported() { + return true; + } + + @Override + public String getSupportMessage() { + // no message since isSupported returns true; + return null; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate(float intervals[], float phase) { + DashPathEffect_Delegate newDelegate = new DashPathEffect_Delegate(intervals, phase); + return sManager.addNewDelegate(newDelegate); + } + + // ---- Private delegate/helper methods ---- + + private DashPathEffect_Delegate(float intervals[], float phase) { + mIntervals = new float[intervals.length]; + System.arraycopy(intervals, 0, mIntervals, 0, intervals.length); + mPhase = phase; + } +} + diff --git a/tools/layoutlib/bridge/src/android/graphics/DiscretePathEffect_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/DiscretePathEffect_Delegate.java new file mode 100644 index 000000000000..ec4a810fbda5 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/DiscretePathEffect_Delegate.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.Stroke; + +/** + * Delegate implementing the native methods of android.graphics.DiscretePathEffect + * + * Through the layoutlib_create tool, the original native methods of DiscretePathEffect have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original DiscretePathEffect class. + * + * Because this extends {@link PathEffect_Delegate}, there's no need to use a {@link DelegateManager}, + * as all the Shader classes will be added to the manager owned by {@link PathEffect_Delegate}. + * + * @see PathEffect_Delegate + * + */ +public class DiscretePathEffect_Delegate extends PathEffect_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public Stroke getStroke(Paint_Delegate paint) { + // FIXME + return null; + } + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Discrete Path Effects are not supported in Layout Preview mode."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate(float length, float deviation) { + DiscretePathEffect_Delegate newDelegate = new DiscretePathEffect_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/DrawFilter_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/DrawFilter_Delegate.java new file mode 100644 index 000000000000..870c46b8f5dd --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/DrawFilter_Delegate.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.graphics.DrawFilter + * + * Through the layoutlib_create tool, the original native methods of DrawFilter have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original DrawFilter class. + * + * This also serve as a base class for all DrawFilter delegate classes. + * + * @see DelegateManager + * + */ +public abstract class DrawFilter_Delegate { + + // ---- delegate manager ---- + protected static final DelegateManager<DrawFilter_Delegate> sManager = + new DelegateManager<DrawFilter_Delegate>(DrawFilter_Delegate.class); + + // ---- delegate helper data ---- + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + public static DrawFilter_Delegate getDelegate(int nativeDrawFilter) { + return sManager.getDelegate(nativeDrawFilter); + } + + public abstract boolean isSupported(); + public abstract String getSupportMessage(); + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static void nativeDestructor(int nativeDrawFilter) { + sManager.removeJavaReferenceFor(nativeDrawFilter); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/EmbossMaskFilter_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/EmbossMaskFilter_Delegate.java new file mode 100644 index 000000000000..ebc1c1d2eca4 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/EmbossMaskFilter_Delegate.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.graphics.EmbossMaskFilter + * + * Through the layoutlib_create tool, the original native methods of EmbossMaskFilter have + * been replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original EmbossMaskFilter class. + * + * Because this extends {@link MaskFilter_Delegate}, there's no need to use a + * {@link DelegateManager}, as all the Shader classes will be added to the manager + * owned by {@link MaskFilter_Delegate}. + * + * @see MaskFilter_Delegate + * + */ +public class EmbossMaskFilter_Delegate extends MaskFilter_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Emboss Mask Filters are not supported."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeConstructor(float[] direction, float ambient, + float specular, float blurRadius) { + EmbossMaskFilter_Delegate newDelegate = new EmbossMaskFilter_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Gradient_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Gradient_Delegate.java new file mode 100644 index 000000000000..7475c22bf58c --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Gradient_Delegate.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import android.graphics.Shader.TileMode; + +/** + * Base class for true Gradient shader delegate. + */ +public abstract class Gradient_Delegate extends Shader_Delegate { + + protected final int[] mColors; + protected final float[] mPositions; + + @Override + public boolean isSupported() { + // all gradient shaders are supported. + return true; + } + + @Override + public String getSupportMessage() { + // all gradient shaders are supported, no need for a gradient support + return null; + } + + /** + * Creates the base shader and do some basic test on the parameters. + * + * @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. + */ + protected Gradient_Delegate(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"); + } + + if (positions == null) { + float spacing = 1.f / (colors.length - 1); + positions = new float[colors.length]; + positions[0] = 0.f; + positions[colors.length-1] = 1.f; + for (int i = 1; i < colors.length - 1 ; i++) { + positions[i] = spacing * i; + } + } + + mColors = colors; + mPositions = positions; + } + + /** + * Base class for (Java) Gradient Paints. This handles computing the gradient colors based + * on the color and position lists, as well as the {@link TileMode} + * + */ + protected abstract static class GradientPaint implements java.awt.Paint { + private final static int GRADIENT_SIZE = 100; + + private final int[] mColors; + private final float[] mPositions; + private final TileMode mTileMode; + private int[] mGradient; + + protected GradientPaint(int[] colors, float[] positions, TileMode tileMode) { + mColors = colors; + mPositions = positions; + mTileMode = tileMode; + } + + @Override + public int getTransparency() { + return java.awt.Paint.TRANSLUCENT; + } + + /** + * Pre-computes the colors for the gradient. This must be called once before any call + * to {@link #getGradientColor(float)} + */ + protected void precomputeGradientColors() { + if (mGradient == null) { + // actually create an array with an extra size, so that we can really go + // from 0 to SIZE (100%), or currentPos in the loop below will never equal 1.0 + mGradient = new int[GRADIENT_SIZE+1]; + + int prevPos = 0; + int nextPos = 1; + for (int i = 0 ; i <= GRADIENT_SIZE ; i++) { + // compute current position + float currentPos = (float)i/GRADIENT_SIZE; + while (currentPos > mPositions[nextPos]) { + prevPos = nextPos++; + } + + float percent = (currentPos - mPositions[prevPos]) / + (mPositions[nextPos] - mPositions[prevPos]); + + mGradient[i] = computeColor(mColors[prevPos], mColors[nextPos], percent); + } + } + } + + /** + * Returns the color based on the position in the gradient. + * <var>pos</var> can be anything, even < 0 or > > 1, as the gradient + * will use {@link TileMode} value to convert it into a [0,1] value. + */ + protected int getGradientColor(float pos) { + if (pos < 0.f) { + if (mTileMode != null) { + switch (mTileMode) { + case CLAMP: + pos = 0.f; + break; + case REPEAT: + // remove the integer part to stay in the [0,1] range. + // we also need to invert the value from [-1,0] to [0, 1] + pos = pos - (float)Math.floor(pos); + break; + case MIRROR: + // this is the same as the positive side, just make the value positive + // first. + pos = Math.abs(pos); + + // get the integer and the decimal part + int intPart = (int)Math.floor(pos); + pos = pos - intPart; + // 0 -> 1 : normal order + // 1 -> 2: mirrored + // etc.. + // this means if the intpart is odd we invert + if ((intPart % 2) == 1) { + pos = 1.f - pos; + } + break; + } + } else { + pos = 0.0f; + } + } else if (pos > 1f) { + if (mTileMode != null) { + switch (mTileMode) { + case CLAMP: + pos = 1.f; + break; + case REPEAT: + // remove the integer part to stay in the [0,1] range + pos = pos - (float)Math.floor(pos); + break; + case MIRROR: + // get the integer and the decimal part + int intPart = (int)Math.floor(pos); + pos = pos - intPart; + // 0 -> 1 : normal order + // 1 -> 2: mirrored + // etc.. + // this means if the intpart is odd we invert + if ((intPart % 2) == 1) { + pos = 1.f - pos; + } + break; + } + } else { + pos = 1.0f; + } + } + + int index = (int)((pos * GRADIENT_SIZE) + .5); + + return mGradient[index]; + } + + /** + * Returns the color between c1, and c2, based on the percent of the distance + * between c1 and c2. + */ + private int computeColor(int c1, int c2, float percent) { + int a = computeChannel((c1 >> 24) & 0xFF, (c2 >> 24) & 0xFF, percent); + int r = computeChannel((c1 >> 16) & 0xFF, (c2 >> 16) & 0xFF, percent); + int g = computeChannel((c1 >> 8) & 0xFF, (c2 >> 8) & 0xFF, percent); + int b = computeChannel((c1 ) & 0xFF, (c2 ) & 0xFF, percent); + return a << 24 | r << 16 | g << 8 | b; + } + + /** + * Returns the channel value between 2 values based on the percent of the distance between + * the 2 values.. + */ + private int computeChannel(int c1, int c2, float percent) { + return c1 + (int)((percent * (c2-c1)) + .5); + } + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/LayerRasterizer_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/LayerRasterizer_Delegate.java new file mode 100644 index 000000000000..51e0576169c4 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/LayerRasterizer_Delegate.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.graphics.LayerRasterizer + * + * Through the layoutlib_create tool, the original native methods of LayerRasterizer have + * been replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original LayerRasterizer class. + * + * Because this extends {@link Rasterizer_Delegate}, there's no need to use a + * {@link DelegateManager}, as all the Shader classes will be added to the manager + * owned by {@link Rasterizer_Delegate}. + * + * @see Rasterizer_Delegate + * + */ +public class LayerRasterizer_Delegate extends Rasterizer_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Layer Rasterizers are not supported."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeConstructor() { + LayerRasterizer_Delegate newDelegate = new LayerRasterizer_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static void nativeAddLayer(int native_layer, int native_paint, float dx, float dy) { + + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/LightingColorFilter_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/LightingColorFilter_Delegate.java new file mode 100644 index 000000000000..0ee883dcd68c --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/LightingColorFilter_Delegate.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.graphics.LightingColorFilter + * + * Through the layoutlib_create tool, the original native methods of LightingColorFilter have + * been replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original LightingColorFilter class. + * + * Because this extends {@link ColorFilter_Delegate}, there's no need to use a + * {@link DelegateManager}, as all the Shader classes will be added to the manager + * owned by {@link ColorFilter_Delegate}. + * + * @see ColorFilter_Delegate + * + */ +public class LightingColorFilter_Delegate extends ColorFilter_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Lighting Color Filters are not supported."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int native_CreateLightingFilter(int mul, int add) { + LightingColorFilter_Delegate newDelegate = new LightingColorFilter_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static int nCreateLightingFilter(int nativeFilter, int mul, int add) { + // pass + return 0; + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/LinearGradient_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/LinearGradient_Delegate.java new file mode 100644 index 000000000000..f117fca52cc1 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/LinearGradient_Delegate.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.graphics.Shader.TileMode; + +/** + * Delegate implementing the native methods of android.graphics.LinearGradient + * + * Through the layoutlib_create tool, the original native methods of LinearGradient have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original LinearGradient class. + * + * Because this extends {@link Shader_Delegate}, there's no need to use a {@link DelegateManager}, + * as all the Shader classes will be added to the manager owned by {@link Shader_Delegate}. + * + * @see Shader_Delegate + * + */ +public final class LinearGradient_Delegate extends Gradient_Delegate { + + // ---- delegate data ---- + private java.awt.Paint mJavaPaint; + + // ---- Public Helper methods ---- + + @Override + public java.awt.Paint getJavaPaint() { + return mJavaPaint; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate1(LinearGradient thisGradient, + float x0, float y0, float x1, float y1, + int colors[], float positions[], int tileMode) { + LinearGradient_Delegate newDelegate = new LinearGradient_Delegate(x0, y0, x1, y1, + colors, positions, Shader_Delegate.getTileMode(tileMode)); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static int nativeCreate2(LinearGradient thisGradient, + float x0, float y0, float x1, float y1, + int color0, int color1, int tileMode) { + return nativeCreate1(thisGradient, + x0, y0, x1, y1, new int[] { color0, color1}, null /*positions*/, + tileMode); + } + + @LayoutlibDelegate + /*package*/ static int nativePostCreate1(LinearGradient thisGradient, + int native_shader, float x0, float y0, float x1, float y1, + int colors[], float positions[], int tileMode) { + // nothing to be done here. + return 0; + } + + @LayoutlibDelegate + /*package*/ static int nativePostCreate2(LinearGradient thisGradient, + int native_shader, float x0, float y0, float x1, float y1, + int color0, int color1, int tileMode) { + // nothing to be done here. + return 0; + } + + // ---- Private delegate/helper methods ---- + + /** + * 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 + */ + private LinearGradient_Delegate(float x0, float y0, float x1, float y1, + int colors[], float positions[], TileMode tile) { + super(colors, positions); + mJavaPaint = new LinearGradientPaint(x0, y0, x1, y1, mColors, mPositions, tile); + } + + // ---- Custom Java Paint ---- + /** + * Linear Gradient (Java) Paint able to handle more than 2 points, as + * {@link java.awt.GradientPaint} only supports 2 points and does not support Android's tile + * modes. + */ + private class LinearGradientPaint extends GradientPaint { + + private final float mX0; + private final float mY0; + private final float mDx; + private final float mDy; + private final float mDSize2; + + public LinearGradientPaint(float x0, float y0, float x1, float y1, int colors[], + float positions[], TileMode tile) { + super(colors, positions, tile); + mX0 = x0; + mY0 = y0; + mDx = x1 - x0; + mDy = y1 - y0; + mDSize2 = mDx * mDx + mDy * mDy; + } + + @Override + public java.awt.PaintContext createContext( + java.awt.image.ColorModel colorModel, + java.awt.Rectangle deviceBounds, + java.awt.geom.Rectangle2D userBounds, + java.awt.geom.AffineTransform xform, + java.awt.RenderingHints hints) { + precomputeGradientColors(); + + java.awt.geom.AffineTransform canvasMatrix; + try { + canvasMatrix = xform.createInverse(); + } catch (java.awt.geom.NoninvertibleTransformException e) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE, + "Unable to inverse matrix in LinearGradient", e, null /*data*/); + canvasMatrix = new java.awt.geom.AffineTransform(); + } + + java.awt.geom.AffineTransform localMatrix = getLocalMatrix(); + try { + localMatrix = localMatrix.createInverse(); + } catch (java.awt.geom.NoninvertibleTransformException e) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE, + "Unable to inverse matrix in LinearGradient", e, null /*data*/); + localMatrix = new java.awt.geom.AffineTransform(); + } + + return new LinearGradientPaintContext(canvasMatrix, localMatrix, colorModel); + } + + private class LinearGradientPaintContext implements java.awt.PaintContext { + + private final java.awt.geom.AffineTransform mCanvasMatrix; + private final java.awt.geom.AffineTransform mLocalMatrix; + private final java.awt.image.ColorModel mColorModel; + + private LinearGradientPaintContext( + java.awt.geom.AffineTransform canvasMatrix, + java.awt.geom.AffineTransform localMatrix, + java.awt.image.ColorModel colorModel) { + mCanvasMatrix = canvasMatrix; + mLocalMatrix = localMatrix; + mColorModel = colorModel; + } + + @Override + public void dispose() { + } + + @Override + public java.awt.image.ColorModel getColorModel() { + return mColorModel; + } + + @Override + public java.awt.image.Raster getRaster(int x, int y, int w, int h) { + java.awt.image.BufferedImage image = new java.awt.image.BufferedImage(w, h, + java.awt.image.BufferedImage.TYPE_INT_ARGB); + + int[] data = new int[w*h]; + + int index = 0; + float[] pt1 = new float[2]; + float[] pt2 = new float[2]; + for (int iy = 0 ; iy < h ; iy++) { + for (int ix = 0 ; ix < w ; ix++) { + // handle the canvas transform + pt1[0] = x + ix; + pt1[1] = y + iy; + mCanvasMatrix.transform(pt1, 0, pt2, 0, 1); + + // handle the local matrix. + pt1[0] = pt2[0]; + pt1[1] = pt2[1]; + mLocalMatrix.transform(pt1, 0, pt2, 0, 1); + + data[index++] = getColor(pt2[0], pt2[1]); + } + } + + image.setRGB(0 /*startX*/, 0 /*startY*/, w, h, data, 0 /*offset*/, w /*scansize*/); + + return image.getRaster(); + } + } + + /** + * Returns a color for an arbitrary point. + */ + private int getColor(float x, float y) { + float pos; + if (mDx == 0) { + pos = (y - mY0) / mDy; + } else if (mDy == 0) { + pos = (x - mX0) / mDx; + } else { + // find the x position on the gradient vector. + float _x = (mDx*mDy*(y-mY0) + mDy*mDy*mX0 + mDx*mDx*x) / mDSize2; + // from it get the position relative to the vector + pos = (_x - mX0) / mDx; + } + + return getGradientColor(pos); + } + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/MaskFilter_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/MaskFilter_Delegate.java new file mode 100644 index 000000000000..c2f27e43c7bf --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/MaskFilter_Delegate.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.graphics.MaskFilter + * + * Through the layoutlib_create tool, the original native methods of MaskFilter have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original MaskFilter class. + * + * This also serve as a base class for all MaskFilter delegate classes. + * + * @see DelegateManager + * + */ +public abstract class MaskFilter_Delegate { + + // ---- delegate manager ---- + protected static final DelegateManager<MaskFilter_Delegate> sManager = + new DelegateManager<MaskFilter_Delegate>(MaskFilter_Delegate.class); + + // ---- delegate helper data ---- + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + public static MaskFilter_Delegate getDelegate(int nativeShader) { + return sManager.getDelegate(nativeShader); + } + + public abstract boolean isSupported(); + public abstract String getSupportMessage(); + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static void nativeDestructor(int native_filter) { + sManager.removeJavaReferenceFor(native_filter); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Matrix_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Matrix_Delegate.java new file mode 100644 index 000000000000..5df2a21567cf --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Matrix_Delegate.java @@ -0,0 +1,1129 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.graphics.Matrix.ScaleToFit; + +import java.awt.geom.AffineTransform; +import java.awt.geom.NoninvertibleTransformException; + +/** + * Delegate implementing the native methods of android.graphics.Matrix + * + * Through the layoutlib_create tool, the original native methods of Matrix have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original Matrix class. + * + * @see DelegateManager + * + */ +public final class Matrix_Delegate { + + private final static int MATRIX_SIZE = 9; + + // ---- delegate manager ---- + private static final DelegateManager<Matrix_Delegate> sManager = + new DelegateManager<Matrix_Delegate>(Matrix_Delegate.class); + + // ---- delegate data ---- + private float mValues[] = new float[MATRIX_SIZE]; + + // ---- Public Helper methods ---- + + public static Matrix_Delegate getDelegate(int native_instance) { + return sManager.getDelegate(native_instance); + } + + /** + * Returns an {@link AffineTransform} matching the given Matrix. + */ + public static AffineTransform getAffineTransform(Matrix m) { + Matrix_Delegate delegate = sManager.getDelegate(m.native_instance); + if (delegate == null) { + return null; + } + + return delegate.getAffineTransform(); + } + + public static boolean hasPerspective(Matrix m) { + Matrix_Delegate delegate = sManager.getDelegate(m.native_instance); + if (delegate == null) { + return false; + } + + return delegate.hasPerspective(); + } + + /** + * Sets the content of the matrix with the content of another matrix. + */ + public void set(Matrix_Delegate matrix) { + System.arraycopy(matrix.mValues, 0, mValues, 0, MATRIX_SIZE); + } + + /** + * Sets the content of the matrix with the content of another matrix represented as an array + * of values. + */ + public void set(float[] values) { + System.arraycopy(values, 0, mValues, 0, MATRIX_SIZE); + } + + /** + * Resets the matrix to be the identity matrix. + */ + public void reset() { + reset(mValues); + } + + /** + * Returns whether or not the matrix is identity. + */ + 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; + } + + public static float[] makeValues(AffineTransform matrix) { + float[] values = new float[MATRIX_SIZE]; + values[0] = (float) matrix.getScaleX(); + values[1] = (float) matrix.getShearX(); + values[2] = (float) matrix.getTranslateX(); + values[3] = (float) matrix.getShearY(); + values[4] = (float) matrix.getScaleY(); + values[5] = (float) matrix.getTranslateY(); + values[6] = 0.f; + values[7] = 0.f; + values[8] = 1.f; + + return values; + } + + public static Matrix_Delegate make(AffineTransform matrix) { + return new Matrix_Delegate(makeValues(matrix)); + } + + public boolean mapRect(RectF dst, RectF src) { + // array with 4 corners + float[] corners = new float[] { + src.left, src.top, + src.right, src.top, + src.right, src.bottom, + src.left, src.bottom, + }; + + // apply the transform to them. + mapPoints(corners); + + // now put the result in the rect. We take the min/max of Xs and min/max of Ys + dst.left = Math.min(Math.min(corners[0], corners[2]), Math.min(corners[4], corners[6])); + dst.right = Math.max(Math.max(corners[0], corners[2]), Math.max(corners[4], corners[6])); + + dst.top = Math.min(Math.min(corners[1], corners[3]), Math.min(corners[5], corners[7])); + dst.bottom = Math.max(Math.max(corners[1], corners[3]), Math.max(corners[5], corners[7])); + + + return (computeTypeMask() & kRectStaysRect_Mask) != 0; + } + + + /** + * Returns an {@link AffineTransform} matching the matrix. + */ + public AffineTransform getAffineTransform() { + return getAffineTransform(mValues); + } + + public boolean hasPerspective() { + return (mValues[6] != 0 || mValues[7] != 0 || mValues[8] != 1); + } + + + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int native_create(int native_src_or_zero) { + // create the delegate + Matrix_Delegate newDelegate = new Matrix_Delegate(); + + // copy from values if needed. + if (native_src_or_zero > 0) { + Matrix_Delegate oldDelegate = sManager.getDelegate(native_src_or_zero); + if (oldDelegate != null) { + System.arraycopy( + oldDelegate.mValues, 0, + newDelegate.mValues, 0, + MATRIX_SIZE); + } + } + + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static boolean native_isIdentity(int native_object) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + return d.isIdentity(); + } + + @LayoutlibDelegate + /*package*/ static boolean native_rectStaysRect(int native_object) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return true; + } + + return (d.computeTypeMask() & kRectStaysRect_Mask) != 0; + } + + @LayoutlibDelegate + /*package*/ static void native_reset(int native_object) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + reset(d.mValues); + } + + @LayoutlibDelegate + /*package*/ static void native_set(int native_object, int other) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + Matrix_Delegate src = sManager.getDelegate(other); + if (src == null) { + return; + } + + System.arraycopy(src.mValues, 0, d.mValues, 0, MATRIX_SIZE); + } + + @LayoutlibDelegate + /*package*/ static void native_setTranslate(int native_object, float dx, float dy) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + setTranslate(d.mValues, dx, dy); + } + + @LayoutlibDelegate + /*package*/ static void native_setScale(int native_object, float sx, float sy, + float px, float py) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + d.mValues = getScale(sx, sy, px, py); + } + + @LayoutlibDelegate + /*package*/ static void native_setScale(int native_object, float sx, float sy) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + d.mValues[0] = sx; + d.mValues[1] = 0; + d.mValues[2] = 0; + d.mValues[3] = 0; + d.mValues[4] = sy; + d.mValues[5] = 0; + d.mValues[6] = 0; + d.mValues[7] = 0; + d.mValues[8] = 1; + } + + @LayoutlibDelegate + /*package*/ static void native_setRotate(int native_object, float degrees, float px, float py) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + d.mValues = getRotate(degrees, px, py); + } + + @LayoutlibDelegate + /*package*/ static void native_setRotate(int native_object, float degrees) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + setRotate(d.mValues, degrees); + } + + @LayoutlibDelegate + /*package*/ static void native_setSinCos(int native_object, float sinValue, float cosValue, + float px, float py) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + // TODO: do it in one pass + + // translate so that the pivot is in 0,0 + setTranslate(d.mValues, -px, -py); + + // scale + d.postTransform(getRotate(sinValue, cosValue)); + // translate back the pivot + d.postTransform(getTranslate(px, py)); + } + + @LayoutlibDelegate + /*package*/ static void native_setSinCos(int native_object, float sinValue, float cosValue) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + setRotate(d.mValues, sinValue, cosValue); + } + + @LayoutlibDelegate + /*package*/ static void native_setSkew(int native_object, float kx, float ky, + float px, float py) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + d.mValues = getSkew(kx, ky, px, py); + } + + @LayoutlibDelegate + /*package*/ static void native_setSkew(int native_object, float kx, float ky) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + d.mValues[0] = 1; + d.mValues[1] = kx; + d.mValues[2] = -0; + d.mValues[3] = ky; + d.mValues[4] = 1; + d.mValues[5] = 0; + d.mValues[6] = 0; + d.mValues[7] = 0; + d.mValues[8] = 1; + } + + @LayoutlibDelegate + /*package*/ static boolean native_setConcat(int native_object, int a, int b) { + if (a == native_object) { + return native_preConcat(native_object, b); + } else if (b == native_object) { + return native_postConcat(native_object, a); + } + + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + Matrix_Delegate a_mtx = sManager.getDelegate(a); + if (a_mtx == null) { + return false; + } + + Matrix_Delegate b_mtx = sManager.getDelegate(b); + if (b_mtx == null) { + return false; + } + + multiply(d.mValues, a_mtx.mValues, b_mtx.mValues); + + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_preTranslate(int native_object, float dx, float dy) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.preTransform(getTranslate(dx, dy)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_preScale(int native_object, float sx, float sy, + float px, float py) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.preTransform(getScale(sx, sy, px, py)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_preScale(int native_object, float sx, float sy) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.preTransform(getScale(sx, sy)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_preRotate(int native_object, float degrees, + float px, float py) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.preTransform(getRotate(degrees, px, py)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_preRotate(int native_object, float degrees) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + double rad = Math.toRadians(degrees); + float sin = (float)Math.sin(rad); + float cos = (float)Math.cos(rad); + + d.preTransform(getRotate(sin, cos)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_preSkew(int native_object, float kx, float ky, + float px, float py) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.preTransform(getSkew(kx, ky, px, py)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_preSkew(int native_object, float kx, float ky) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.preTransform(getSkew(kx, ky)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_preConcat(int native_object, int other_matrix) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + Matrix_Delegate other = sManager.getDelegate(other_matrix); + if (other == null) { + return false; + } + + d.preTransform(other.mValues); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_postTranslate(int native_object, float dx, float dy) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.postTransform(getTranslate(dx, dy)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_postScale(int native_object, float sx, float sy, + float px, float py) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.postTransform(getScale(sx, sy, px, py)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_postScale(int native_object, float sx, float sy) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.postTransform(getScale(sx, sy)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_postRotate(int native_object, float degrees, + float px, float py) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.postTransform(getRotate(degrees, px, py)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_postRotate(int native_object, float degrees) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.postTransform(getRotate(degrees)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_postSkew(int native_object, float kx, float ky, + float px, float py) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.postTransform(getSkew(kx, ky, px, py)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_postSkew(int native_object, float kx, float ky) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + d.postTransform(getSkew(kx, ky)); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_postConcat(int native_object, int other_matrix) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + Matrix_Delegate other = sManager.getDelegate(other_matrix); + if (other == null) { + return false; + } + + d.postTransform(other.mValues); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_setRectToRect(int native_object, RectF src, + RectF dst, int stf) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + if (src.isEmpty()) { + reset(d.mValues); + return false; + } + + if (dst.isEmpty()) { + d.mValues[0] = d.mValues[1] = d.mValues[2] = d.mValues[3] = d.mValues[4] = d.mValues[5] + = d.mValues[6] = d.mValues[7] = 0; + d.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.nativeInt) { + 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.nativeInt || stf == ScaleToFit.END.nativeInt) { + float diff; + + if (xLarger) { + diff = dst.width() - src.width() * sy; + } else { + diff = dst.height() - src.height() * sy; + } + + if (stf == ScaleToFit.CENTER.nativeInt) { + diff = diff / 2; + } + + if (xLarger) { + tx += diff; + } else { + ty += diff; + } + } + + d.mValues[0] = sx; + d.mValues[4] = sy; + d.mValues[2] = tx; + d.mValues[5] = ty; + d.mValues[1] = d.mValues[3] = d.mValues[6] = d.mValues[7] = 0; + + } + // shared cleanup + d.mValues[8] = 1; + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean native_setPolyToPoly(int native_object, float[] src, int srcIndex, + float[] dst, int dstIndex, int pointCount) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Matrix.setPolyToPoly is not supported.", + null, null /*data*/); + return false; + } + + @LayoutlibDelegate + /*package*/ static boolean native_invert(int native_object, int inverse) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + Matrix_Delegate inv_mtx = sManager.getDelegate(inverse); + if (inv_mtx == null) { + return false; + } + + try { + AffineTransform affineTransform = d.getAffineTransform(); + AffineTransform inverseTransform = affineTransform.createInverse(); + inv_mtx.mValues[0] = (float)inverseTransform.getScaleX(); + inv_mtx.mValues[1] = (float)inverseTransform.getShearX(); + inv_mtx.mValues[2] = (float)inverseTransform.getTranslateX(); + inv_mtx.mValues[3] = (float)inverseTransform.getScaleX(); + inv_mtx.mValues[4] = (float)inverseTransform.getShearY(); + inv_mtx.mValues[5] = (float)inverseTransform.getTranslateY(); + + return true; + } catch (NoninvertibleTransformException e) { + return false; + } + } + + @LayoutlibDelegate + /*package*/ static void native_mapPoints(int native_object, float[] dst, int dstIndex, + float[] src, int srcIndex, int ptCount, boolean isPts) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + if (isPts) { + d.mapPoints(dst, dstIndex, src, srcIndex, ptCount); + } else { + d.mapVectors(dst, dstIndex, src, srcIndex, ptCount); + } + } + + @LayoutlibDelegate + /*package*/ static boolean native_mapRect(int native_object, RectF dst, RectF src) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return false; + } + + return d.mapRect(dst, src); + } + + @LayoutlibDelegate + /*package*/ static float native_mapRadius(int native_object, float radius) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return 0.f; + } + + float[] src = new float[] { radius, 0.f, 0.f, radius }; + d.mapVectors(src, 0, src, 0, 2); + + float l1 = getPointLength(src, 0); + float l2 = getPointLength(src, 2); + + return (float) Math.sqrt(l1 * l2); + } + + @LayoutlibDelegate + /*package*/ static void native_getValues(int native_object, float[] values) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + System.arraycopy(d.mValues, 0, d.mValues, 0, MATRIX_SIZE); + } + + @LayoutlibDelegate + /*package*/ static void native_setValues(int native_object, float[] values) { + Matrix_Delegate d = sManager.getDelegate(native_object); + if (d == null) { + return; + } + + System.arraycopy(values, 0, d.mValues, 0, MATRIX_SIZE); + } + + @LayoutlibDelegate + /*package*/ static boolean native_equals(int native_a, int native_b) { + Matrix_Delegate a = sManager.getDelegate(native_a); + if (a == null) { + return false; + } + + Matrix_Delegate b = sManager.getDelegate(native_b); + if (b == null) { + return false; + } + + for (int i = 0 ; i < MATRIX_SIZE ; i++) { + if (a.mValues[i] != b.mValues[i]) { + return false; + } + } + + return true; + } + + @LayoutlibDelegate + /*package*/ static void finalizer(int native_instance) { + sManager.removeJavaReferenceFor(native_instance); + } + + // ---- Private helper methods ---- + + /*package*/ static AffineTransform getAffineTransform(float[] matrix) { + // the AffineTransform constructor takes the value in a different order + // for a matrix [ 0 1 2 ] + // [ 3 4 5 ] + // the order is 0, 3, 1, 4, 2, 5... + return new AffineTransform( + matrix[0], matrix[3], matrix[1], + matrix[4], matrix[2], matrix[5]); + } + + /** + * Reset a matrix to the identity + */ + private static void reset(float[] mtx) { + for (int i = 0, k = 0; i < 3; i++) { + for (int j = 0; j < 3; j++, k++) { + mtx[k] = ((i==j) ? 1 : 0); + } + } + } + + @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; + } + + private Matrix_Delegate() { + reset(); + } + + private Matrix_Delegate(float[] values) { + System.arraycopy(values, 0, mValues, 0, MATRIX_SIZE); + } + + /** + * Adds the given transformation to the current Matrix + * <p/>This in effect does this = this*matrix + * @param matrix + */ + private void postTransform(float[] matrix) { + float[] tmp = new float[9]; + multiply(tmp, mValues, matrix); + mValues = tmp; + } + + /** + * Adds the given transformation to the current Matrix + * <p/>This in effect does this = matrix*this + * @param matrix + */ + private void preTransform(float[] matrix) { + float[] tmp = new float[9]; + multiply(tmp, matrix, mValues); + mValues = tmp; + } + + /** + * 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 + */ + + private void mapPoints(float[] dst, int dstIndex, float[] src, int srcIndex, + int pointCount) { + final int count = pointCount * 2; + + float[] tmpDest = dst; + boolean inPlace = dst == src; + if (inPlace) { + tmpDest = new float[dstIndex + count]; + } + + for (int i = 0 ; i < count ; i += 2) { + // just in case we are doing in place, we better put this in temp vars + float x = mValues[0] * src[i + srcIndex] + + mValues[1] * src[i + srcIndex + 1] + + mValues[2]; + float y = mValues[3] * src[i + srcIndex] + + mValues[4] * src[i + srcIndex + 1] + + mValues[5]; + + tmpDest[i + dstIndex] = x; + tmpDest[i + dstIndex + 1] = y; + } + + if (inPlace) { + System.arraycopy(tmpDest, dstIndex, dst, dstIndex, count); + } + } + + /** + * 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. + */ + + private void mapPoints(float[] pts) { + mapPoints(pts, 0, pts, 0, pts.length >> 1); + } + + private void mapVectors(float[] dst, int dstIndex, float[] src, int srcIndex, int ptCount) { + if (hasPerspective()) { + // transform the (0,0) point + float[] origin = new float[] { 0.f, 0.f}; + mapPoints(origin); + + // translate the vector data as points + mapPoints(dst, dstIndex, src, srcIndex, ptCount); + + // then substract the transformed origin. + final int count = ptCount * 2; + for (int i = 0 ; i < count ; i += 2) { + dst[dstIndex + i] = dst[dstIndex + i] - origin[0]; + dst[dstIndex + i + 1] = dst[dstIndex + i + 1] - origin[1]; + } + } else { + // make a copy of the matrix + Matrix_Delegate copy = new Matrix_Delegate(mValues); + + // remove the translation + setTranslate(copy.mValues, 0, 0); + + // map the content as points. + copy.mapPoints(dst, dstIndex, src, srcIndex, ptCount); + } + } + + private static float getPointLength(float[] src, int index) { + return (float) Math.sqrt(src[index] * src[index] + src[index + 1] * src[index + 1]); + } + + /** + * multiply two matrices and store them in a 3rd. + * <p/>This in effect does dest = a*b + * dest cannot be the same as a or b. + */ + /*package*/ static void multiply(float dest[], float[] a, float[] b) { + // first row + dest[0] = b[0] * a[0] + b[1] * a[3] + b[2] * a[6]; + dest[1] = b[0] * a[1] + b[1] * a[4] + b[2] * a[7]; + dest[2] = b[0] * a[2] + b[1] * a[5] + b[2] * a[8]; + + // 2nd row + dest[3] = b[3] * a[0] + b[4] * a[3] + b[5] * a[6]; + dest[4] = b[3] * a[1] + b[4] * a[4] + b[5] * a[7]; + dest[5] = b[3] * a[2] + b[4] * a[5] + b[5] * a[8]; + + // 3rd row + dest[6] = b[6] * a[0] + b[7] * a[3] + b[8] * a[6]; + dest[7] = b[6] * a[1] + b[7] * a[4] + b[8] * a[7]; + dest[8] = b[6] * a[2] + b[7] * a[5] + b[8] * a[8]; + } + + /** + * Returns a matrix that represents a given translate + * @param dx + * @param dy + * @return + */ + /*package*/ static float[] getTranslate(float dx, float dy) { + return setTranslate(new float[9], dx, dy); + } + + /*package*/ static float[] setTranslate(float[] dest, float dx, float dy) { + dest[0] = 1; + dest[1] = 0; + dest[2] = dx; + dest[3] = 0; + dest[4] = 1; + dest[5] = dy; + dest[6] = 0; + dest[7] = 0; + dest[8] = 1; + return dest; + } + + /*package*/ static float[] getScale(float sx, float sy) { + return new float[] { sx, 0, 0, 0, sy, 0, 0, 0, 1 }; + } + + /** + * Returns a matrix that represents the given scale info. + * @param sx + * @param sy + * @param px + * @param py + */ + /*package*/ static float[] getScale(float sx, float sy, float px, float py) { + float[] tmp = new float[9]; + float[] tmp2 = new float[9]; + + // TODO: do it in one pass + + // translate tmp so that the pivot is in 0,0 + setTranslate(tmp, -px, -py); + + // scale into tmp2 + multiply(tmp2, tmp, getScale(sx, sy)); + + // translate back the pivot back into tmp + multiply(tmp, tmp2, getTranslate(px, py)); + + return tmp; + } + + + /*package*/ static float[] getRotate(float degrees) { + double rad = Math.toRadians(degrees); + float sin = (float)Math.sin(rad); + float cos = (float)Math.cos(rad); + + return getRotate(sin, cos); + } + + /*package*/ static float[] getRotate(float sin, float cos) { + return setRotate(new float[9], sin, cos); + } + + /*package*/ static float[] setRotate(float[] dest, float degrees) { + double rad = Math.toRadians(degrees); + float sin = (float)Math.sin(rad); + float cos = (float)Math.cos(rad); + + return setRotate(dest, sin, cos); + } + + /*package*/ static float[] setRotate(float[] dest, float sin, float cos) { + dest[0] = cos; + dest[1] = -sin; + dest[2] = 0; + dest[3] = sin; + dest[4] = cos; + dest[5] = 0; + dest[6] = 0; + dest[7] = 0; + dest[8] = 1; + return dest; + } + + /*package*/ static float[] getRotate(float degrees, float px, float py) { + float[] tmp = new float[9]; + float[] tmp2 = new float[9]; + + // TODO: do it in one pass + + // translate so that the pivot is in 0,0 + setTranslate(tmp, -px, -py); + + // rotate into tmp2 + double rad = Math.toRadians(degrees); + float cos = (float)Math.cos(rad); + float sin = (float)Math.sin(rad); + multiply(tmp2, tmp, getRotate(sin, cos)); + + // translate back the pivot back into tmp + multiply(tmp, tmp2, getTranslate(px, py)); + + return tmp; + } + + /*package*/ static float[] getSkew(float kx, float ky) { + return new float[] { 1, kx, 0, ky, 1, 0, 0, 0, 1 }; + } + + /*package*/ static float[] getSkew(float kx, float ky, float px, float py) { + float[] tmp = new float[9]; + float[] tmp2 = new float[9]; + + // TODO: do it in one pass + + // translate so that the pivot is in 0,0 + setTranslate(tmp, -px, -py); + + // skew into tmp2 + multiply(tmp2, tmp, new float[] { 1, kx, 0, ky, 1, 0, 0, 0, 1 }); + // translate back the pivot back into tmp + multiply(tmp, tmp2, getTranslate(px, py)); + + return tmp; + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/NinePatch_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/NinePatch_Delegate.java new file mode 100644 index 000000000000..be27b54bb828 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/NinePatch_Delegate.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.layoutlib.bridge.impl.GcSnapshot; +import com.android.ninepatch.NinePatchChunk; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.graphics.drawable.NinePatchDrawable; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.ref.SoftReference; +import java.util.HashMap; +import java.util.Map; + +/** + * Delegate implementing the native methods of android.graphics.NinePatch + * + * Through the layoutlib_create tool, the original native methods of NinePatch have been replaced + * by calls to methods of the same name in this delegate class. + * + * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager} + * around to map int to instance of the delegate. + * + */ +public final class NinePatch_Delegate { + + /** + * Cache map for {@link NinePatchChunk}. + * When the chunks are created they are serialized into a byte[], and both are put + * in the cache, using a {@link SoftReference} for the chunk. The default Java classes + * for {@link NinePatch} and {@link NinePatchDrawable} only reference to the byte[] data, and + * provide this for drawing. + * Using the cache map allows us to not have to deserialize the byte[] back into a + * {@link NinePatchChunk} every time a rendering is done. + */ + private final static Map<byte[], SoftReference<NinePatchChunk>> sChunkCache = + new HashMap<byte[], SoftReference<NinePatchChunk>>(); + + // ---- Public Helper methods ---- + + /** + * Serializes the given chunk. + * + * @return the serialized data for the chunk. + */ + public static byte[] serialize(NinePatchChunk chunk) { + // serialize the chunk to get a byte[] + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = null; + try { + oos = new ObjectOutputStream(baos); + oos.writeObject(chunk); + } catch (IOException e) { + Bridge.getLog().error(null, "Failed to serialize NinePatchChunk.", e, null /*data*/); + return null; + } finally { + if (oos != null) { + try { + oos.close(); + } catch (IOException e) { + } + } + } + + // get the array and add it to the cache + byte[] array = baos.toByteArray(); + sChunkCache.put(array, new SoftReference<NinePatchChunk>(chunk)); + return array; + } + + /** + * Returns a {@link NinePatchChunk} object for the given serialized representation. + * + * If the chunk is present in the cache then the object from the cache is returned, otherwise + * the array is deserialized into a {@link NinePatchChunk} object. + * + * @param array the serialized representation of the chunk. + * @return the NinePatchChunk or null if deserialization failed. + */ + public static NinePatchChunk getChunk(byte[] array) { + SoftReference<NinePatchChunk> chunkRef = sChunkCache.get(array); + NinePatchChunk chunk = chunkRef.get(); + if (chunk == null) { + ByteArrayInputStream bais = new ByteArrayInputStream(array); + ObjectInputStream ois = null; + try { + ois = new ObjectInputStream(bais); + chunk = (NinePatchChunk) ois.readObject(); + + // put back the chunk in the cache + if (chunk != null) { + sChunkCache.put(array, new SoftReference<NinePatchChunk>(chunk)); + } + } catch (IOException e) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + "Failed to deserialize NinePatchChunk content.", e, null /*data*/); + return null; + } catch (ClassNotFoundException e) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + "Failed to deserialize NinePatchChunk class.", e, null /*data*/); + return null; + } finally { + if (ois != null) { + try { + ois.close(); + } catch (IOException e) { + } + } + } + } + + return chunk; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static boolean isNinePatchChunk(byte[] chunk) { + NinePatchChunk chunkObject = getChunk(chunk); + if (chunkObject != null) { + return true; + } + + return false; + } + + @LayoutlibDelegate + /*package*/ static void validateNinePatchChunk(int bitmap, byte[] chunk) { + // the default JNI implementation only checks that the byte[] has the same + // size as the C struct it represent. Since we cannot do the same check (serialization + // will return different size depending on content), we do nothing. + } + + @LayoutlibDelegate + /*package*/ static void nativeDraw(int canvas_instance, RectF loc, int bitmap_instance, + byte[] c, int paint_instance_or_null, int destDensity, int srcDensity) { + draw(canvas_instance, + (int) loc.left, (int) loc.top, (int) loc.width(), (int) loc.height(), + bitmap_instance, c, paint_instance_or_null, + destDensity, srcDensity); + } + + @LayoutlibDelegate + /*package*/ static void nativeDraw(int canvas_instance, Rect loc, int bitmap_instance, + byte[] c, int paint_instance_or_null, int destDensity, int srcDensity) { + draw(canvas_instance, + loc.left, loc.top, loc.width(), loc.height(), + bitmap_instance, c, paint_instance_or_null, + destDensity, srcDensity); + } + + @LayoutlibDelegate + /*package*/ static int nativeGetTransparentRegion(int bitmap, byte[] chunk, Rect location) { + return 0; + } + + // ---- Private Helper methods ---- + + private static void draw(int canvas_instance, + final int left, final int top, final int right, final int bottom, + int bitmap_instance, byte[] c, int paint_instance_or_null, + final int destDensity, final int srcDensity) { + // get the delegate from the native int. + final Bitmap_Delegate bitmap_delegate = Bitmap_Delegate.getDelegate(bitmap_instance); + if (bitmap_delegate == null) { + return; + } + + if (c == null) { + // not a 9-patch? + BufferedImage image = bitmap_delegate.getImage(); + Canvas_Delegate.native_drawBitmap(canvas_instance, bitmap_instance, + new Rect(0, 0, image.getWidth(), image.getHeight()), + new Rect(left, top, right, bottom), + paint_instance_or_null, destDensity, srcDensity); + return; + } + + final NinePatchChunk chunkObject = getChunk(c); + assert chunkObject != null; + if (chunkObject == null) { + return; + } + + Canvas_Delegate canvas_delegate = Canvas_Delegate.getDelegate(canvas_instance); + if (canvas_delegate == null) { + return; + } + + // this one can be null + Paint_Delegate paint_delegate = Paint_Delegate.getDelegate(paint_instance_or_null); + + canvas_delegate.getSnapshot().draw(new GcSnapshot.Drawable() { + @Override + public void draw(Graphics2D graphics, Paint_Delegate paint) { + chunkObject.draw(bitmap_delegate.getImage(), graphics, + left, top, right - left, bottom - top, destDensity, srcDensity); + } + }, paint_delegate, true /*compositeOnly*/, false /*forceSrcMode*/); + + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/PaintFlagsDrawFilter_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/PaintFlagsDrawFilter_Delegate.java new file mode 100644 index 000000000000..71d346a93553 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/PaintFlagsDrawFilter_Delegate.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.graphics.PaintFlagsDrawFilter + * + * Through the layoutlib_create tool, the original native methods of PaintFlagsDrawFilter have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original PaintFlagsDrawFilter class. + * + * Because this extends {@link DrawFilter_Delegate}, there's no need to use a + * {@link DelegateManager}, as all the DrawFilter classes will be added to the manager owned by + * {@link DrawFilter_Delegate}. + * + * @see DrawFilter_Delegate + * + */ +public class PaintFlagsDrawFilter_Delegate extends DrawFilter_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Paint Flags Draw Filters are not supported."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeConstructor(int clearBits, int setBits) { + PaintFlagsDrawFilter_Delegate newDelegate = new PaintFlagsDrawFilter_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java new file mode 100644 index 000000000000..c9c98007556f --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java @@ -0,0 +1,1284 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.graphics.Paint.FontMetrics; +import android.graphics.Paint.FontMetricsInt; +import android.text.TextUtils; + +import java.awt.BasicStroke; +import java.awt.Font; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.Toolkit; +import java.awt.font.FontRenderContext; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Delegate implementing the native methods of android.graphics.Paint + * + * Through the layoutlib_create tool, the original native methods of Paint have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original Paint class. + * + * @see DelegateManager + * + */ +public class Paint_Delegate { + + /** + * Class associating a {@link Font} and it's {@link java.awt.FontMetrics}. + */ + /*package*/ static final class FontInfo { + Font mFont; + java.awt.FontMetrics mMetrics; + } + + // ---- delegate manager ---- + private static final DelegateManager<Paint_Delegate> sManager = + new DelegateManager<Paint_Delegate>(Paint_Delegate.class); + + // ---- delegate helper data ---- + private List<FontInfo> mFonts; + private final FontRenderContext mFontContext = new FontRenderContext( + new AffineTransform(), true, true); + + // ---- delegate data ---- + private int mFlags; + private int mColor; + private int mStyle; + private int mCap; + private int mJoin; + private int mTextAlign; + private Typeface_Delegate mTypeface; + private float mStrokeWidth; + private float mStrokeMiter; + private float mTextSize; + private float mTextScaleX; + private float mTextSkewX; + private int mHintingMode = Paint.HINTING_ON; + + private Xfermode_Delegate mXfermode; + private ColorFilter_Delegate mColorFilter; + private Shader_Delegate mShader; + private PathEffect_Delegate mPathEffect; + private MaskFilter_Delegate mMaskFilter; + private Rasterizer_Delegate mRasterizer; + + private Locale mLocale = Locale.getDefault(); + + + // ---- Public Helper methods ---- + + public static Paint_Delegate getDelegate(int native_paint) { + return sManager.getDelegate(native_paint); + } + + /** + * Returns the list of {@link Font} objects. The first item is the main font, the rest + * are fall backs for characters not present in the main font. + */ + public List<FontInfo> getFonts() { + return mFonts; + } + + public boolean isAntiAliased() { + return (mFlags & Paint.ANTI_ALIAS_FLAG) != 0; + } + + public boolean isFilterBitmap() { + return (mFlags & Paint.FILTER_BITMAP_FLAG) != 0; + } + + public int getStyle() { + return mStyle; + } + + public int getColor() { + return mColor; + } + + public int getAlpha() { + return mColor >>> 24; + } + + public void setAlpha(int alpha) { + mColor = (alpha << 24) | (mColor & 0x00FFFFFF); + } + + public int getTextAlign() { + return mTextAlign; + } + + public float getStrokeWidth() { + return mStrokeWidth; + } + + /** + * returns the value of stroke miter needed by the java api. + */ + public float getJavaStrokeMiter() { + float miter = mStrokeMiter * mStrokeWidth; + if (miter < 1.f) { + miter = 1.f; + } + return miter; + } + + public int getJavaCap() { + switch (Paint.sCapArray[mCap]) { + case BUTT: + return BasicStroke.CAP_BUTT; + case ROUND: + return BasicStroke.CAP_ROUND; + default: + case SQUARE: + return BasicStroke.CAP_SQUARE; + } + } + + public int getJavaJoin() { + switch (Paint.sJoinArray[mJoin]) { + default: + case MITER: + return BasicStroke.JOIN_MITER; + case ROUND: + return BasicStroke.JOIN_ROUND; + case BEVEL: + return BasicStroke.JOIN_BEVEL; + } + } + + public Stroke getJavaStroke() { + if (mPathEffect != null) { + if (mPathEffect.isSupported()) { + Stroke stroke = mPathEffect.getStroke(this); + assert stroke != null; + if (stroke != null) { + return stroke; + } + } else { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_PATHEFFECT, + mPathEffect.getSupportMessage(), + null, null /*data*/); + } + } + + // if no custom stroke as been set, set the default one. + return new BasicStroke( + getStrokeWidth(), + getJavaCap(), + getJavaJoin(), + getJavaStrokeMiter()); + } + + /** + * Returns the {@link Xfermode} delegate or null if none have been set + * + * @return the delegate or null. + */ + public Xfermode_Delegate getXfermode() { + return mXfermode; + } + + /** + * Returns the {@link ColorFilter} delegate or null if none have been set + * + * @return the delegate or null. + */ + public ColorFilter_Delegate getColorFilter() { + return mColorFilter; + } + + /** + * Returns the {@link Shader} delegate or null if none have been set + * + * @return the delegate or null. + */ + public Shader_Delegate getShader() { + return mShader; + } + + /** + * Returns the {@link MaskFilter} delegate or null if none have been set + * + * @return the delegate or null. + */ + public MaskFilter_Delegate getMaskFilter() { + return mMaskFilter; + } + + /** + * Returns the {@link Rasterizer} delegate or null if none have been set + * + * @return the delegate or null. + */ + public Rasterizer_Delegate getRasterizer() { + return mRasterizer; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int getFlags(Paint thisPaint) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 0; + } + + return delegate.mFlags; + } + + + + @LayoutlibDelegate + /*package*/ static void setFlags(Paint thisPaint, int flags) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return; + } + + delegate.mFlags = flags; + } + + @LayoutlibDelegate + /*package*/ static void setFilterBitmap(Paint thisPaint, boolean filter) { + setFlag(thisPaint, Paint.FILTER_BITMAP_FLAG, filter); + } + + @LayoutlibDelegate + /*package*/ static int getHinting(Paint thisPaint) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return Paint.HINTING_ON; + } + + return delegate.mHintingMode; + } + + @LayoutlibDelegate + /*package*/ static void setHinting(Paint thisPaint, int mode) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return; + } + + delegate.mHintingMode = mode; + } + + @LayoutlibDelegate + /*package*/ static void setAntiAlias(Paint thisPaint, boolean aa) { + setFlag(thisPaint, Paint.ANTI_ALIAS_FLAG, aa); + } + + @LayoutlibDelegate + /*package*/ static void setSubpixelText(Paint thisPaint, boolean subpixelText) { + setFlag(thisPaint, Paint.SUBPIXEL_TEXT_FLAG, subpixelText); + } + + @LayoutlibDelegate + /*package*/ static void setUnderlineText(Paint thisPaint, boolean underlineText) { + setFlag(thisPaint, Paint.UNDERLINE_TEXT_FLAG, underlineText); + } + + @LayoutlibDelegate + /*package*/ static void setStrikeThruText(Paint thisPaint, boolean strikeThruText) { + setFlag(thisPaint, Paint.STRIKE_THRU_TEXT_FLAG, strikeThruText); + } + + @LayoutlibDelegate + /*package*/ static void setFakeBoldText(Paint thisPaint, boolean fakeBoldText) { + setFlag(thisPaint, Paint.FAKE_BOLD_TEXT_FLAG, fakeBoldText); + } + + @LayoutlibDelegate + /*package*/ static void setDither(Paint thisPaint, boolean dither) { + setFlag(thisPaint, Paint.DITHER_FLAG, dither); + } + + @LayoutlibDelegate + /*package*/ static void setLinearText(Paint thisPaint, boolean linearText) { + setFlag(thisPaint, Paint.LINEAR_TEXT_FLAG, linearText); + } + + @LayoutlibDelegate + /*package*/ static int getColor(Paint thisPaint) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 0; + } + + return delegate.mColor; + } + + @LayoutlibDelegate + /*package*/ static void setColor(Paint thisPaint, int color) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return; + } + + delegate.mColor = color; + } + + @LayoutlibDelegate + /*package*/ static int getAlpha(Paint thisPaint) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 0; + } + + return delegate.getAlpha(); + } + + @LayoutlibDelegate + /*package*/ static void setAlpha(Paint thisPaint, int a) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return; + } + + delegate.setAlpha(a); + } + + @LayoutlibDelegate + /*package*/ static float getStrokeWidth(Paint thisPaint) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 1.f; + } + + return delegate.mStrokeWidth; + } + + @LayoutlibDelegate + /*package*/ static void setStrokeWidth(Paint thisPaint, float width) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return; + } + + delegate.mStrokeWidth = width; + } + + @LayoutlibDelegate + /*package*/ static float getStrokeMiter(Paint thisPaint) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 1.f; + } + + return delegate.mStrokeMiter; + } + + @LayoutlibDelegate + /*package*/ static void setStrokeMiter(Paint thisPaint, float miter) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return; + } + + delegate.mStrokeMiter = miter; + } + + @LayoutlibDelegate + /*package*/ static void nSetShadowLayer(Paint thisPaint, float radius, float dx, float dy, + int color) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Paint.setShadowLayer is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static float getTextSize(Paint thisPaint) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 1.f; + } + + return delegate.mTextSize; + } + + @LayoutlibDelegate + /*package*/ static void setTextSize(Paint thisPaint, float textSize) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return; + } + + delegate.mTextSize = textSize; + delegate.updateFontObject(); + } + + @LayoutlibDelegate + /*package*/ static float getTextScaleX(Paint thisPaint) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 1.f; + } + + return delegate.mTextScaleX; + } + + @LayoutlibDelegate + /*package*/ static void setTextScaleX(Paint thisPaint, float scaleX) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return; + } + + delegate.mTextScaleX = scaleX; + delegate.updateFontObject(); + } + + @LayoutlibDelegate + /*package*/ static float getTextSkewX(Paint thisPaint) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 1.f; + } + + return delegate.mTextSkewX; + } + + @LayoutlibDelegate + /*package*/ static void setTextSkewX(Paint thisPaint, float skewX) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return; + } + + delegate.mTextSkewX = skewX; + delegate.updateFontObject(); + } + + @LayoutlibDelegate + /*package*/ static float ascent(Paint thisPaint) { + // get the delegate + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 0; + } + + if (delegate.mFonts.size() > 0) { + java.awt.FontMetrics javaMetrics = delegate.mFonts.get(0).mMetrics; + // Android expects negative ascent so we invert the value from Java. + return - javaMetrics.getAscent(); + } + + return 0; + } + + @LayoutlibDelegate + /*package*/ static float descent(Paint thisPaint) { + // get the delegate + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 0; + } + + if (delegate.mFonts.size() > 0) { + java.awt.FontMetrics javaMetrics = delegate.mFonts.get(0).mMetrics; + return javaMetrics.getDescent(); + } + + return 0; + + } + + @LayoutlibDelegate + /*package*/ static float getFontMetrics(Paint thisPaint, FontMetrics metrics) { + // get the delegate + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 0; + } + + return delegate.getFontMetrics(metrics); + } + + @LayoutlibDelegate + /*package*/ static int getFontMetricsInt(Paint thisPaint, FontMetricsInt fmi) { + // get the delegate + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 0; + } + + if (delegate.mFonts.size() > 0) { + java.awt.FontMetrics javaMetrics = delegate.mFonts.get(0).mMetrics; + if (fmi != null) { + // Android expects negative ascent so we invert the value from Java. + fmi.top = - javaMetrics.getMaxAscent(); + fmi.ascent = - javaMetrics.getAscent(); + fmi.descent = javaMetrics.getDescent(); + fmi.bottom = javaMetrics.getMaxDescent(); + fmi.leading = javaMetrics.getLeading(); + } + + return javaMetrics.getHeight(); + } + + return 0; + } + + @LayoutlibDelegate + /*package*/ static float native_measureText(Paint thisPaint, char[] text, int index, + int count, int bidiFlags) { + // get the delegate + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 0; + } + + return delegate.measureText(text, index, count, bidiFlags); + } + + @LayoutlibDelegate + /*package*/ static float native_measureText(Paint thisPaint, String text, int start, int end, + int bidiFlags) { + return native_measureText(thisPaint, text.toCharArray(), start, end - start, bidiFlags); + } + + @LayoutlibDelegate + /*package*/ static float native_measureText(Paint thisPaint, String text, int bidiFlags) { + return native_measureText(thisPaint, text.toCharArray(), 0, text.length(), bidiFlags); + } + + @LayoutlibDelegate + /*package*/ static int native_breakText(Paint thisPaint, char[] text, int index, int count, + float maxWidth, int bidiFlags, float[] measuredWidth) { + + // get the delegate + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return 0; + } + + 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 = delegate.measureText(text, start, end - start + 1, bidiFlags); + + 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; + } + + @LayoutlibDelegate + /*package*/ static int native_breakText(Paint thisPaint, String text, boolean measureForwards, + float maxWidth, int bidiFlags, float[] measuredWidth) { + return native_breakText(thisPaint, text.toCharArray(), 0, text.length(), maxWidth, + bidiFlags, measuredWidth); + } + + @LayoutlibDelegate + /*package*/ static int native_init() { + Paint_Delegate newDelegate = new Paint_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static int native_initWithPaint(int paint) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(paint); + if (delegate == null) { + return 0; + } + + Paint_Delegate newDelegate = new Paint_Delegate(delegate); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static void native_reset(int native_object) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return; + } + + delegate.reset(); + } + + @LayoutlibDelegate + /*package*/ static void native_set(int native_dst, int native_src) { + // get the delegate from the native int. + Paint_Delegate delegate_dst = sManager.getDelegate(native_dst); + if (delegate_dst == null) { + return; + } + + // get the delegate from the native int. + Paint_Delegate delegate_src = sManager.getDelegate(native_src); + if (delegate_src == null) { + return; + } + + delegate_dst.set(delegate_src); + } + + @LayoutlibDelegate + /*package*/ static int native_getStyle(int native_object) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return 0; + } + + return delegate.mStyle; + } + + @LayoutlibDelegate + /*package*/ static void native_setStyle(int native_object, int style) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return; + } + + delegate.mStyle = style; + } + + @LayoutlibDelegate + /*package*/ static int native_getStrokeCap(int native_object) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return 0; + } + + return delegate.mCap; + } + + @LayoutlibDelegate + /*package*/ static void native_setStrokeCap(int native_object, int cap) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return; + } + + delegate.mCap = cap; + } + + @LayoutlibDelegate + /*package*/ static int native_getStrokeJoin(int native_object) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return 0; + } + + return delegate.mJoin; + } + + @LayoutlibDelegate + /*package*/ static void native_setStrokeJoin(int native_object, int join) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return; + } + + delegate.mJoin = join; + } + + @LayoutlibDelegate + /*package*/ static boolean native_getFillPath(int native_object, int src, int dst) { + Paint_Delegate paint = sManager.getDelegate(native_object); + if (paint == null) { + return false; + } + + Path_Delegate srcPath = Path_Delegate.getDelegate(src); + if (srcPath == null) { + return true; + } + + Path_Delegate dstPath = Path_Delegate.getDelegate(dst); + if (dstPath == null) { + return true; + } + + Stroke stroke = paint.getJavaStroke(); + Shape strokeShape = stroke.createStrokedShape(srcPath.getJavaShape()); + + dstPath.setJavaShape(strokeShape); + + // FIXME figure out the return value? + return true; + } + + @LayoutlibDelegate + /*package*/ static int native_setShader(int native_object, int shader) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return shader; + } + + delegate.mShader = Shader_Delegate.getDelegate(shader); + + return shader; + } + + @LayoutlibDelegate + /*package*/ static int native_setColorFilter(int native_object, int filter) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return filter; + } + + delegate.mColorFilter = ColorFilter_Delegate.getDelegate(filter);; + + // since none of those are supported, display a fidelity warning right away + if (delegate.mColorFilter != null && delegate.mColorFilter.isSupported() == false) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_COLORFILTER, + delegate.mColorFilter.getSupportMessage(), null, null /*data*/); + } + + return filter; + } + + @LayoutlibDelegate + /*package*/ static int native_setXfermode(int native_object, int xfermode) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return xfermode; + } + + delegate.mXfermode = Xfermode_Delegate.getDelegate(xfermode); + + return xfermode; + } + + @LayoutlibDelegate + /*package*/ static int native_setPathEffect(int native_object, int effect) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return effect; + } + + delegate.mPathEffect = PathEffect_Delegate.getDelegate(effect); + + return effect; + } + + @LayoutlibDelegate + /*package*/ static int native_setMaskFilter(int native_object, int maskfilter) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return maskfilter; + } + + delegate.mMaskFilter = MaskFilter_Delegate.getDelegate(maskfilter); + + // since none of those are supported, display a fidelity warning right away + if (delegate.mMaskFilter != null && delegate.mMaskFilter.isSupported() == false) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_MASKFILTER, + delegate.mMaskFilter.getSupportMessage(), null, null /*data*/); + } + + return maskfilter; + } + + @LayoutlibDelegate + /*package*/ static int native_setTypeface(int native_object, int typeface) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return 0; + } + + delegate.mTypeface = Typeface_Delegate.getDelegate(typeface); + delegate.updateFontObject(); + return typeface; + } + + @LayoutlibDelegate + /*package*/ static int native_setRasterizer(int native_object, int rasterizer) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return rasterizer; + } + + delegate.mRasterizer = Rasterizer_Delegate.getDelegate(rasterizer); + + // since none of those are supported, display a fidelity warning right away + if (delegate.mRasterizer != null && delegate.mRasterizer.isSupported() == false) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_RASTERIZER, + delegate.mRasterizer.getSupportMessage(), null, null /*data*/); + } + + return rasterizer; + } + + @LayoutlibDelegate + /*package*/ static int native_getTextAlign(int native_object) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return 0; + } + + return delegate.mTextAlign; + } + + @LayoutlibDelegate + /*package*/ static void native_setTextAlign(int native_object, int align) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return; + } + + delegate.mTextAlign = align; + } + + @LayoutlibDelegate + /*package*/ static void native_setTextLocale(int native_object, String locale) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return; + } + + delegate.setTextLocale(locale); + } + + @LayoutlibDelegate + /*package*/ static int native_getTextWidths(int native_object, char[] text, int index, + int count, int bidiFlags, float[] widths) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return 0; + } + + if (delegate.mFonts.size() > 0) { + // FIXME: handle multi-char characters (see measureText) + float totalAdvance = 0; + for (int i = 0; i < count; i++) { + char c = text[i + index]; + boolean found = false; + for (FontInfo info : delegate.mFonts) { + if (info.mFont.canDisplay(c)) { + float adv = info.mMetrics.charWidth(c); + totalAdvance += adv; + if (widths != null) { + widths[i] = adv; + } + + found = true; + break; + } + } + + if (found == false) { + // no advance for this char. + if (widths != null) { + widths[i] = 0.f; + } + } + } + + return (int) totalAdvance; + } + + return 0; + } + + @LayoutlibDelegate + /*package*/ static int native_getTextWidths(int native_object, String text, int start, + int end, int bidiFlags, float[] widths) { + return native_getTextWidths(native_object, text.toCharArray(), start, end - start, + bidiFlags, widths); + } + + @LayoutlibDelegate + /* package */static int native_getTextGlyphs(int native_object, String text, int start, + int end, int contextStart, int contextEnd, int flags, char[] glyphs) { + // FIXME + return 0; + } + + @LayoutlibDelegate + /*package*/ static float native_getTextRunAdvances(int native_object, + char[] text, int index, int count, int contextIndex, int contextCount, + int flags, float[] advances, int advancesIndex) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(native_object); + if (delegate == null) { + return 0.f; + } + + if (delegate.mFonts.size() > 0) { + // FIXME: handle multi-char characters (see measureText) + float totalAdvance = 0; + for (int i = 0; i < count; i++) { + char c = text[i + index]; + boolean found = false; + for (FontInfo info : delegate.mFonts) { + if (info.mFont.canDisplay(c)) { + float adv = info.mMetrics.charWidth(c); + totalAdvance += adv; + if (advances != null) { + advances[i] = adv; + } + + found = true; + break; + } + } + + if (found == false) { + // no advance for this char. + if (advances != null) { + advances[i] = 0.f; + } + } + } + + return totalAdvance; + } + + return 0; + + } + + @LayoutlibDelegate + /*package*/ static float native_getTextRunAdvances(int native_object, + String text, int start, int end, int contextStart, int contextEnd, + int flags, float[] advances, int advancesIndex) { + // FIXME: support contextStart, contextEnd and direction flag + int count = end - start; + char[] buffer = TemporaryBuffer.obtain(count); + TextUtils.getChars(text, start, end, buffer, 0); + + return native_getTextRunAdvances(native_object, buffer, 0, count, contextStart, + contextEnd - contextStart, flags, advances, advancesIndex); + } + + @LayoutlibDelegate + /*package*/ static int native_getTextRunCursor(Paint thisPaint, int native_object, char[] text, + int contextStart, int contextLength, int flags, int offset, int cursorOpt) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Paint.getTextRunCursor is not supported.", null, null /*data*/); + return 0; + } + + @LayoutlibDelegate + /*package*/ static int native_getTextRunCursor(Paint thisPaint, int native_object, String text, + int contextStart, int contextEnd, int flags, int offset, int cursorOpt) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Paint.getTextRunCursor is not supported.", null, null /*data*/); + return 0; + } + + @LayoutlibDelegate + /*package*/ static void native_getTextPath(int native_object, int bidiFlags, + char[] text, int index, int count, float x, float y, int path) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Paint.getTextPath is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static void native_getTextPath(int native_object, int bidiFlags, + String text, int start, int end, float x, float y, int path) { + // FIXME + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Paint.getTextPath is not supported.", null, null /*data*/); + } + + @LayoutlibDelegate + /*package*/ static void nativeGetStringBounds(int nativePaint, String text, int start, + int end, int bidiFlags, Rect bounds) { + nativeGetCharArrayBounds(nativePaint, text.toCharArray(), start, end - start, bidiFlags, + bounds); + } + + @LayoutlibDelegate + /*package*/ static void nativeGetCharArrayBounds(int nativePaint, char[] text, int index, + int count, int bidiFlags, Rect bounds) { + + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(nativePaint); + if (delegate == null) { + return; + } + + // FIXME should test if the main font can display all those characters. + // See MeasureText + if (delegate.mFonts.size() > 0) { + FontInfo mainInfo = delegate.mFonts.get(0); + + Rectangle2D rect = mainInfo.mFont.getStringBounds(text, index, index + count, + delegate.mFontContext); + bounds.set(0, 0, (int) rect.getWidth(), (int) rect.getHeight()); + } + } + + @LayoutlibDelegate + /*package*/ static void finalizer(int nativePaint) { + sManager.removeJavaReferenceFor(nativePaint); + } + + // ---- Private delegate/helper methods ---- + + /*package*/ Paint_Delegate() { + reset(); + } + + private Paint_Delegate(Paint_Delegate paint) { + set(paint); + } + + private void set(Paint_Delegate paint) { + mFlags = paint.mFlags; + mColor = paint.mColor; + mStyle = paint.mStyle; + mCap = paint.mCap; + mJoin = paint.mJoin; + mTextAlign = paint.mTextAlign; + mTypeface = paint.mTypeface; + mStrokeWidth = paint.mStrokeWidth; + mStrokeMiter = paint.mStrokeMiter; + mTextSize = paint.mTextSize; + mTextScaleX = paint.mTextScaleX; + mTextSkewX = paint.mTextSkewX; + mXfermode = paint.mXfermode; + mColorFilter = paint.mColorFilter; + mShader = paint.mShader; + mPathEffect = paint.mPathEffect; + mMaskFilter = paint.mMaskFilter; + mRasterizer = paint.mRasterizer; + mHintingMode = paint.mHintingMode; + updateFontObject(); + } + + private void reset() { + mFlags = Paint.DEFAULT_PAINT_FLAGS; + mColor = 0xFF000000; + mStyle = Paint.Style.FILL.nativeInt; + mCap = Paint.Cap.BUTT.nativeInt; + mJoin = Paint.Join.MITER.nativeInt; + mTextAlign = 0; + mTypeface = Typeface_Delegate.getDelegate(Typeface.sDefaults[0].native_instance); + mStrokeWidth = 1.f; + mStrokeMiter = 4.f; + mTextSize = 20.f; + mTextScaleX = 1.f; + mTextSkewX = 0.f; + mXfermode = null; + mColorFilter = null; + mShader = null; + mPathEffect = null; + mMaskFilter = null; + mRasterizer = null; + updateFontObject(); + mHintingMode = Paint.HINTING_ON; + } + + /** + * Update the {@link Font} object from the typeface, text size and scaling + */ + @SuppressWarnings("deprecation") + private void updateFontObject() { + if (mTypeface != null) { + // Get the fonts from the TypeFace object. + List<Font> fonts = mTypeface.getFonts(); + + // create new font objects as well as FontMetrics, based on the current text size + // and skew info. + ArrayList<FontInfo> infoList = new ArrayList<FontInfo>(fonts.size()); + for (Font font : fonts) { + FontInfo info = new FontInfo(); + info.mFont = font.deriveFont(mTextSize); + if (mTextScaleX != 1.0 || mTextSkewX != 0) { + // TODO: support skew + info.mFont = info.mFont.deriveFont(new AffineTransform( + mTextScaleX, mTextSkewX, 0, 1, 0, 0)); + } + info.mMetrics = Toolkit.getDefaultToolkit().getFontMetrics(info.mFont); + + infoList.add(info); + } + + mFonts = Collections.unmodifiableList(infoList); + } + } + + /*package*/ float measureText(char[] text, int index, int count, int bidiFlags) { + // TODO: find out what bidiFlags actually does. + + // WARNING: the logic in this method is similar to Canvas_Delegate.native_drawText + // Any change to this method should be reflected there as well + + if (mFonts.size() > 0) { + FontInfo mainFont = mFonts.get(0); + int i = index; + int lastIndex = index + count; + float total = 0f; + while (i < lastIndex) { + // always start with the main font. + int upTo = mainFont.mFont.canDisplayUpTo(text, i, lastIndex); + if (upTo == -1) { + // shortcut to exit + return total + mainFont.mMetrics.charsWidth(text, i, lastIndex - i); + } else if (upTo > 0) { + total += mainFont.mMetrics.charsWidth(text, i, upTo - i); + i = upTo; + // don't call continue at this point. Since it is certain the main font + // cannot display the font a index upTo (now ==i), we move on to the + // fallback fonts directly. + } + + // no char supported, attempt to read the next char(s) with the + // fallback font. In this case we only test the first character + // and then go back to test with the main font. + // Special test for 2-char characters. + boolean foundFont = false; + for (int f = 1 ; f < mFonts.size() ; f++) { + FontInfo fontInfo = mFonts.get(f); + + // need to check that the font can display the character. We test + // differently if the char is a high surrogate. + int charCount = Character.isHighSurrogate(text[i]) ? 2 : 1; + upTo = fontInfo.mFont.canDisplayUpTo(text, i, i + charCount); + if (upTo == -1) { + total += fontInfo.mMetrics.charsWidth(text, i, charCount); + i += charCount; + foundFont = true; + break; + + } + } + + // in case no font can display the char, measure it with the main font. + if (foundFont == false) { + int size = Character.isHighSurrogate(text[i]) ? 2 : 1; + total += mainFont.mMetrics.charsWidth(text, i, size); + i += size; + } + } + + return total; + } + + return 0; + } + + private float getFontMetrics(FontMetrics metrics) { + if (mFonts.size() > 0) { + java.awt.FontMetrics javaMetrics = mFonts.get(0).mMetrics; + if (metrics != null) { + // Android expects negative ascent so we invert the value from Java. + metrics.top = - javaMetrics.getMaxAscent(); + metrics.ascent = - javaMetrics.getAscent(); + metrics.descent = javaMetrics.getDescent(); + metrics.bottom = javaMetrics.getMaxDescent(); + metrics.leading = javaMetrics.getLeading(); + } + + return javaMetrics.getHeight(); + } + + return 0; + } + + private void setTextLocale(String locale) { + mLocale = new Locale(locale); + } + + private static void setFlag(Paint thisPaint, int flagMask, boolean flagValue) { + // get the delegate from the native int. + Paint_Delegate delegate = sManager.getDelegate(thisPaint.mNativePaint); + if (delegate == null) { + return; + } + + if (flagValue) { + delegate.mFlags |= flagMask; + } else { + delegate.mFlags &= ~flagMask; + } + } + +} diff --git a/tools/layoutlib/bridge/src/android/graphics/PathDashPathEffect_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/PathDashPathEffect_Delegate.java new file mode 100644 index 000000000000..c448f0ec0f20 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/PathDashPathEffect_Delegate.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.Stroke; + +/** + * Delegate implementing the native methods of android.graphics.PathDashPathEffect + * + * Through the layoutlib_create tool, the original native methods of PathDashPathEffect have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original PathDashPathEffect class. + * + * Because this extends {@link PathEffect_Delegate}, there's no need to use a {@link DelegateManager}, + * as all the Shader classes will be added to the manager owned by {@link PathEffect_Delegate}. + * + * @see PathEffect_Delegate + * + */ +public class PathDashPathEffect_Delegate extends PathEffect_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public Stroke getStroke(Paint_Delegate paint) { + // FIXME + return null; + } + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Path Dash Path Effects are not supported in Layout Preview mode."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate(int native_path, float advance, float phase, + int native_style) { + PathDashPathEffect_Delegate newDelegate = new PathDashPathEffect_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/PathEffect_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/PathEffect_Delegate.java new file mode 100644 index 000000000000..bd2b6de480da --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/PathEffect_Delegate.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.Stroke; + +/** + * Delegate implementing the native methods of android.graphics.PathEffect + * + * Through the layoutlib_create tool, the original native methods of PathEffect have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original PathEffect class. + * + * This also serve as a base class for all PathEffect delegate classes. + * + * @see DelegateManager + * + */ +public abstract class PathEffect_Delegate { + + // ---- delegate manager ---- + protected static final DelegateManager<PathEffect_Delegate> sManager = + new DelegateManager<PathEffect_Delegate>(PathEffect_Delegate.class); + + // ---- delegate helper data ---- + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + public static PathEffect_Delegate getDelegate(int nativeShader) { + return sManager.getDelegate(nativeShader); + } + + public abstract Stroke getStroke(Paint_Delegate paint); + public abstract boolean isSupported(); + public abstract String getSupportMessage(); + + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static void nativeDestructor(int native_patheffect) { + sManager.removeJavaReferenceFor(native_patheffect); + } + + // ---- Private delegate/helper methods ---- + +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java new file mode 100644 index 000000000000..64f19d310d2f --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java @@ -0,0 +1,815 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.graphics.Path.Direction; +import android.graphics.Path.FillType; + +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Arc2D; +import java.awt.geom.Area; +import java.awt.geom.Ellipse2D; +import java.awt.geom.GeneralPath; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; + +/** + * Delegate implementing the native methods of android.graphics.Path + * + * Through the layoutlib_create tool, the original native methods of Path have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original Path class. + * + * @see DelegateManager + * + */ +public final class Path_Delegate { + + // ---- delegate manager ---- + private static final DelegateManager<Path_Delegate> sManager = + new DelegateManager<Path_Delegate>(Path_Delegate.class); + + // ---- delegate data ---- + private FillType mFillType = FillType.WINDING; + private GeneralPath mPath = new GeneralPath(); + + private float mLastX = 0; + private float mLastY = 0; + + // ---- Public Helper methods ---- + + public static Path_Delegate getDelegate(int nPath) { + return sManager.getDelegate(nPath); + } + + public Shape getJavaShape() { + return mPath; + } + + public void setJavaShape(Shape shape) { + mPath.reset(); + mPath.append(shape, false /*connect*/); + } + + public void reset() { + mPath.reset(); + } + + public void setPathIterator(PathIterator iterator) { + mPath.reset(); + mPath.append(iterator, false /*connect*/); + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int init1() { + // create the delegate + Path_Delegate newDelegate = new Path_Delegate(); + + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static int init2(int nPath) { + // create the delegate + Path_Delegate newDelegate = new Path_Delegate(); + + // get the delegate to copy, which could be null if nPath is 0 + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate != null) { + newDelegate.set(pathDelegate); + } + + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static void native_reset(int nPath) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.mPath.reset(); + } + + @LayoutlibDelegate + /*package*/ static void native_rewind(int nPath) { + // call out to reset since there's nothing to optimize in + // terms of data structs. + native_reset(nPath); + } + + @LayoutlibDelegate + /*package*/ static void native_set(int native_dst, int native_src) { + Path_Delegate pathDstDelegate = sManager.getDelegate(native_dst); + if (pathDstDelegate == null) { + return; + } + + Path_Delegate pathSrcDelegate = sManager.getDelegate(native_src); + if (pathSrcDelegate == null) { + return; + } + + pathDstDelegate.set(pathSrcDelegate); + } + + @LayoutlibDelegate + /*package*/ static int native_getFillType(int nPath) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return 0; + } + + return pathDelegate.mFillType.nativeInt; + } + + @LayoutlibDelegate + /*package*/ static void native_setFillType(int nPath, int ft) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.mFillType = Path.sFillTypeArray[ft]; + } + + @LayoutlibDelegate + /*package*/ static boolean native_isEmpty(int nPath) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return true; + } + + return pathDelegate.isEmpty(); + } + + @LayoutlibDelegate + /*package*/ static boolean native_isRect(int nPath, RectF rect) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return false; + } + + // create an Area that can test if the path is a rect + Area area = new Area(pathDelegate.mPath); + if (area.isRectangular()) { + if (rect != null) { + pathDelegate.fillBounds(rect); + } + + return true; + } + + return false; + } + + @LayoutlibDelegate + /*package*/ static void native_computeBounds(int nPath, RectF bounds) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.fillBounds(bounds); + } + + @LayoutlibDelegate + /*package*/ static void native_incReserve(int nPath, int extraPtCount) { + // since we use a java2D path, there's no way to pre-allocate new points, + // so we do nothing. + } + + @LayoutlibDelegate + /*package*/ static void native_moveTo(int nPath, float x, float y) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.moveTo(x, y); + } + + @LayoutlibDelegate + /*package*/ static void native_rMoveTo(int nPath, float dx, float dy) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.rMoveTo(dx, dy); + } + + @LayoutlibDelegate + /*package*/ static void native_lineTo(int nPath, float x, float y) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.lineTo(x, y); + } + + @LayoutlibDelegate + /*package*/ static void native_rLineTo(int nPath, float dx, float dy) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.rLineTo(dx, dy); + } + + @LayoutlibDelegate + /*package*/ static void native_quadTo(int nPath, float x1, float y1, float x2, float y2) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.quadTo(x1, y1, x2, y2); + } + + @LayoutlibDelegate + /*package*/ static void native_rQuadTo(int nPath, float dx1, float dy1, float dx2, float dy2) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.rQuadTo(dx1, dy1, dx2, dy2); + } + + @LayoutlibDelegate + /*package*/ static void native_cubicTo(int nPath, float x1, float y1, + float x2, float y2, float x3, float y3) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.cubicTo(x1, y1, x2, y2, x3, y3); + } + + @LayoutlibDelegate + /*package*/ static void native_rCubicTo(int nPath, float x1, float y1, + float x2, float y2, float x3, float y3) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.rCubicTo(x1, y1, x2, y2, x3, y3); + } + + @LayoutlibDelegate + /*package*/ static void native_arcTo(int nPath, RectF oval, + float startAngle, float sweepAngle, boolean forceMoveTo) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.arcTo(oval, startAngle, sweepAngle, forceMoveTo); + } + + @LayoutlibDelegate + /*package*/ static void native_close(int nPath) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.close(); + } + + @LayoutlibDelegate + /*package*/ static void native_addRect(int nPath, RectF rect, int dir) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.addRect(rect.left, rect.top, rect.right, rect.bottom, dir); + } + + @LayoutlibDelegate + /*package*/ static void native_addRect(int nPath, + float left, float top, float right, float bottom, int dir) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.addRect(left, top, right, bottom, dir); + } + + @LayoutlibDelegate + /*package*/ static void native_addOval(int nPath, RectF oval, int dir) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.mPath.append(new Ellipse2D.Float( + oval.left, oval.top, oval.width(), oval.height()), false); + } + + @LayoutlibDelegate + /*package*/ static void native_addCircle(int nPath, float x, float y, float radius, int dir) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + // because x/y is the center of the circle, need to offset this by the radius + pathDelegate.mPath.append(new Ellipse2D.Float( + x - radius, y - radius, radius * 2, radius * 2), false); + } + + @LayoutlibDelegate + /*package*/ static void native_addArc(int nPath, RectF oval, + float startAngle, float sweepAngle) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + // because x/y is the center of the circle, need to offset this by the radius + pathDelegate.mPath.append(new Arc2D.Float( + oval.left, oval.top, oval.width(), oval.height(), + -startAngle, -sweepAngle, Arc2D.OPEN), false); + } + + @LayoutlibDelegate + /*package*/ static void native_addRoundRect( + int nPath, RectF rect, float rx, float ry, int dir) { + + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.mPath.append(new RoundRectangle2D.Float( + rect.left, rect.top, rect.width(), rect.height(), rx * 2, ry * 2), false); + } + + @LayoutlibDelegate + /*package*/ static void native_addRoundRect(int nPath, RectF rect, float[] radii, int dir) { + // Java2D doesn't support different rounded corners in each corner, so just use the + // first value. + native_addRoundRect(nPath, rect, radii[0], radii[1], dir); + + // there can be a case where this API is used but with similar values for all corners, so + // in that case we don't warn. + // we only care if 2 corners are different so just compare to the next one. + for (int i = 0 ; i < 3 ; i++) { + if (radii[i * 2] != radii[(i + 1) * 2] || radii[i * 2 + 1] != radii[(i + 1) * 2 + 1]) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Different corner sizes are not supported in Path.addRoundRect.", + null, null /*data*/); + break; + } + } + } + + @LayoutlibDelegate + /*package*/ static void native_addPath(int nPath, int src, float dx, float dy) { + addPath(nPath, src, AffineTransform.getTranslateInstance(dx, dy)); + } + + @LayoutlibDelegate + /*package*/ static void native_addPath(int nPath, int src) { + addPath(nPath, src, null /*transform*/); + } + + @LayoutlibDelegate + /*package*/ static void native_addPath(int nPath, int src, int matrix) { + Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(matrix); + if (matrixDelegate == null) { + return; + } + + addPath(nPath, src, matrixDelegate.getAffineTransform()); + } + + @LayoutlibDelegate + /*package*/ static void native_offset(int nPath, float dx, float dy, int dst_path) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + // could be null if the int is 0; + Path_Delegate dstDelegate = sManager.getDelegate(dst_path); + + pathDelegate.offset(dx, dy, dstDelegate); + } + + @LayoutlibDelegate + /*package*/ static void native_offset(int nPath, float dx, float dy) { + native_offset(nPath, dx, dy, 0); + } + + @LayoutlibDelegate + /*package*/ static void native_setLastPoint(int nPath, float dx, float dy) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + pathDelegate.mLastX = dx; + pathDelegate.mLastY = dy; + } + + @LayoutlibDelegate + /*package*/ static void native_transform(int nPath, int matrix, + int dst_path) { + Path_Delegate pathDelegate = sManager.getDelegate(nPath); + if (pathDelegate == null) { + return; + } + + Matrix_Delegate matrixDelegate = Matrix_Delegate.getDelegate(matrix); + if (matrixDelegate == null) { + return; + } + + // this can be null if dst_path is 0 + Path_Delegate dstDelegate = sManager.getDelegate(dst_path); + + pathDelegate.transform(matrixDelegate, dstDelegate); + } + + @LayoutlibDelegate + /*package*/ static void native_transform(int nPath, int matrix) { + native_transform(nPath, matrix, 0); + } + + @LayoutlibDelegate + /*package*/ static void finalizer(int nPath) { + sManager.removeJavaReferenceFor(nPath); + } + + + // ---- Private helper methods ---- + + private void set(Path_Delegate delegate) { + mPath.reset(); + setFillType(delegate.mFillType); + mPath.append(delegate.mPath, false /*connect*/); + } + + private void setFillType(FillType fillType) { + mFillType = fillType; + mPath.setWindingRule(getWindingRule(fillType)); + } + + /** + * Returns the Java2D winding rules matching a given Android {@link FillType}. + * @param type the android fill type + * @return the matching java2d winding rule. + */ + private static int getWindingRule(FillType type) { + switch (type) { + case WINDING: + case INVERSE_WINDING: + return GeneralPath.WIND_NON_ZERO; + case EVEN_ODD: + case INVERSE_EVEN_ODD: + return GeneralPath.WIND_EVEN_ODD; + } + + assert false; + throw new IllegalArgumentException(); + } + + private static Direction getDirection(int direction) { + for (Direction d : Direction.values()) { + if (direction == d.nativeInt) { + return d; + } + } + + assert false; + return null; + } + + private static void addPath(int destPath, int srcPath, AffineTransform transform) { + Path_Delegate destPathDelegate = sManager.getDelegate(destPath); + if (destPathDelegate == null) { + return; + } + + Path_Delegate srcPathDelegate = sManager.getDelegate(srcPath); + if (srcPathDelegate == null) { + return; + } + + if (transform != null) { + destPathDelegate.mPath.append( + srcPathDelegate.mPath.getPathIterator(transform), false); + } else { + destPathDelegate.mPath.append(srcPathDelegate.mPath, false); + } + } + + + /** + * Returns whether the path is empty. + * @return true if the path is empty. + */ + private boolean isEmpty() { + return mPath.getCurrentPoint() == null; + } + + /** + * Fills the given {@link RectF} with the path bounds. + * @param bounds the RectF to be filled. + */ + private void fillBounds(RectF bounds) { + Rectangle2D rect = mPath.getBounds2D(); + bounds.left = (float)rect.getMinX(); + bounds.right = (float)rect.getMaxX(); + bounds.top = (float)rect.getMinY(); + bounds.bottom = (float)rect.getMaxY(); + } + + /** + * 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 + */ + private 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 + */ + private 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 + */ + private 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 + */ + private 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 + */ + private 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 + */ + private 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 + */ + private 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. + */ + private 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 + */ + private void arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) { + Arc2D arc = new Arc2D.Float(oval.left, oval.top, oval.width(), oval.height(), -startAngle, + -sweepAngle, Arc2D.OPEN); + mPath.append(arc, true /*connect*/); + + resetLastPointFromPath(); + } + + /** + * Close the current contour. If the current point is not equal to the + * first point of the contour, a line segment is automatically added. + */ + private void close() { + mPath.closePath(); + } + + private void resetLastPointFromPath() { + Point2D last = mPath.getCurrentPoint(); + mLastX = (float) last.getX(); + mLastY = (float) last.getY(); + } + + /** + * 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 + */ + private void addRect(float left, float top, float right, float bottom, + int dir) { + moveTo(left, top); + + Direction direction = getDirection(dir); + + switch (direction) { + 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(); + + resetLastPointFromPath(); + } + + /** + * 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_Delegate 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; + } + } + + /** + * 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_Delegate matrix, Path_Delegate dst) { + if (matrix.hasPerspective()) { + assert false; + Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_AFFINE, + "android.graphics.Path#transform() only " + + "supports affine transformations.", null, null /*data*/); + } + + GeneralPath newPath = new GeneralPath(); + + PathIterator iterator = mPath.getPathIterator(matrix.getAffineTransform()); + + newPath.append(iterator, false /*connect*/); + + if (dst != null) { + dst.mPath = newPath; + } else { + mPath = newPath; + } + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/PixelXorXfermode_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/PixelXorXfermode_Delegate.java new file mode 100644 index 000000000000..4ab044bd107a --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/PixelXorXfermode_Delegate.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.Composite; + +/** + * Delegate implementing the native methods of android.graphics.PixelXorXfermode + * + * Through the layoutlib_create tool, the original native methods of PixelXorXfermode have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original PixelXorXfermode class. + * + * Because this extends {@link Xfermode_Delegate}, there's no need to use a + * {@link DelegateManager}, as all the PathEffect classes will be added to the manager owned by + * {@link Xfermode_Delegate}. + * + * @see Xfermode_Delegate + */ +public class PixelXorXfermode_Delegate extends Xfermode_Delegate { + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public Composite getComposite(int alpha) { + // FIXME + return null; + } + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Pixel XOR Xfermodes are not supported in Layout Preview mode."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate(int opColor) { + PixelXorXfermode_Delegate newDelegate = new PixelXorXfermode_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/PorterDuffColorFilter_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/PorterDuffColorFilter_Delegate.java new file mode 100644 index 000000000000..c45dbaad344d --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/PorterDuffColorFilter_Delegate.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.graphics.PorterDuffColorFilter + * + * Through the layoutlib_create tool, the original native methods of PorterDuffColorFilter have + * been replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original PorterDuffColorFilter class. + * + * Because this extends {@link ColorFilter_Delegate}, there's no need to use a + * {@link DelegateManager}, as all the Shader classes will be added to the manager + * owned by {@link ColorFilter_Delegate}. + * + * @see ColorFilter_Delegate + * + */ +public class PorterDuffColorFilter_Delegate extends ColorFilter_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "PorterDuff Color Filters are not supported."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int native_CreatePorterDuffFilter(int srcColor, int porterDuffMode) { + PorterDuffColorFilter_Delegate newDelegate = new PorterDuffColorFilter_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static int nCreatePorterDuffFilter(int nativeFilter, int srcColor, + int porterDuffMode) { + // pass + return 0; + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/PorterDuffXfermode_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/PorterDuffXfermode_Delegate.java new file mode 100644 index 000000000000..4301c1a2c5d7 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/PorterDuffXfermode_Delegate.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.AlphaComposite; +import java.awt.Composite; + +/** + * Delegate implementing the native methods of android.graphics.PorterDuffXfermode + * + * Through the layoutlib_create tool, the original native methods of PorterDuffXfermode have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original PorterDuffXfermode class. + * + * Because this extends {@link Xfermode_Delegate}, there's no need to use a + * {@link DelegateManager}, as all the PathEffect classes will be added to the manager owned by + * {@link Xfermode_Delegate}. + * + */ +public class PorterDuffXfermode_Delegate extends Xfermode_Delegate { + + // ---- delegate data ---- + + private final int mMode; + + // ---- Public Helper methods ---- + + public PorterDuff.Mode getMode() { + return getPorterDuffMode(mMode); + } + + @Override + public Composite getComposite(int alpha) { + return getComposite(getPorterDuffMode(mMode), alpha); + } + + @Override + public boolean isSupported() { + return true; + } + + @Override + public String getSupportMessage() { + // no message since isSupported returns true; + return null; + } + + public static PorterDuff.Mode getPorterDuffMode(int mode) { + for (PorterDuff.Mode m : PorterDuff.Mode.values()) { + if (m.nativeInt == mode) { + return m; + } + } + + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + String.format("Unknown PorterDuff.Mode: %d", mode), null /*data*/); + assert false; + return PorterDuff.Mode.SRC_OVER; + } + + public static Composite getComposite(PorterDuff.Mode mode, int alpha) { + float falpha = alpha != 0xFF ? (float)alpha / 255.f : 1.f; + switch (mode) { + case CLEAR: + return AlphaComposite.getInstance(AlphaComposite.CLEAR, falpha); + case DARKEN: + break; + case DST: + return AlphaComposite.getInstance(AlphaComposite.DST, falpha); + case DST_ATOP: + return AlphaComposite.getInstance(AlphaComposite.DST_ATOP, falpha); + case DST_IN: + return AlphaComposite.getInstance(AlphaComposite.DST_IN, falpha); + case DST_OUT: + return AlphaComposite.getInstance(AlphaComposite.DST_OUT, falpha); + case DST_OVER: + return AlphaComposite.getInstance(AlphaComposite.DST_OVER, falpha); + case LIGHTEN: + break; + case MULTIPLY: + break; + case SCREEN: + break; + case SRC: + return AlphaComposite.getInstance(AlphaComposite.SRC, falpha); + case SRC_ATOP: + return AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, falpha); + case SRC_IN: + return AlphaComposite.getInstance(AlphaComposite.SRC_IN, falpha); + case SRC_OUT: + return AlphaComposite.getInstance(AlphaComposite.SRC_OUT, falpha); + case SRC_OVER: + return AlphaComposite.getInstance(AlphaComposite.SRC_OVER, falpha); + case XOR: + return AlphaComposite.getInstance(AlphaComposite.XOR, falpha); + } + + Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN, + String.format("Unsupported PorterDuff Mode: %s", mode.name()), + null, null /*data*/); + + return AlphaComposite.getInstance(AlphaComposite.SRC_OVER, falpha); + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreateXfermode(int mode) { + PorterDuffXfermode_Delegate newDelegate = new PorterDuffXfermode_Delegate(mode); + return sManager.addNewDelegate(newDelegate); + } + + // ---- Private delegate/helper methods ---- + + private PorterDuffXfermode_Delegate(int mode) { + mMode = mode; + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/RadialGradient_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/RadialGradient_Delegate.java new file mode 100644 index 000000000000..3fe45fae2a3d --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/RadialGradient_Delegate.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.graphics.Shader.TileMode; + +/** + * Delegate implementing the native methods of android.graphics.RadialGradient + * + * Through the layoutlib_create tool, the original native methods of RadialGradient have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original RadialGradient class. + * + * Because this extends {@link Shader_Delegate}, there's no need to use a {@link DelegateManager}, + * as all the Shader classes will be added to the manager owned by {@link Shader_Delegate}. + * + * @see Shader_Delegate + * + */ +public class RadialGradient_Delegate extends Gradient_Delegate { + + // ---- delegate data ---- + private java.awt.Paint mJavaPaint; + + // ---- Public Helper methods ---- + + @Override + public java.awt.Paint getJavaPaint() { + return mJavaPaint; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate1(float x, float y, float radius, + int colors[], float positions[], int tileMode) { + RadialGradient_Delegate newDelegate = new RadialGradient_Delegate(x, y, radius, + colors, positions, Shader_Delegate.getTileMode(tileMode)); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static int nativeCreate2(float x, float y, float radius, + int color0, int color1, int tileMode) { + return nativeCreate1(x, y, radius, new int[] { color0, color1 }, null /*positions*/, + tileMode); + } + + @LayoutlibDelegate + /*package*/ static int nativePostCreate1(int native_shader, float x, float y, float radius, + int colors[], float positions[], int tileMode) { + // nothing to be done here. + return 0; + } + + @LayoutlibDelegate + /*package*/ static int nativePostCreate2(int native_shader, float x, float y, float radius, + int color0, int color1, int tileMode) { + // nothing to be done here. + return 0; + } + + // ---- Private delegate/helper methods ---- + + /** + * 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 + */ + private RadialGradient_Delegate(float x, float y, float radius, int colors[], float positions[], + TileMode tile) { + super(colors, positions); + mJavaPaint = new RadialGradientPaint(x, y, radius, mColors, mPositions, tile); + } + + private class RadialGradientPaint extends GradientPaint { + + private final float mX; + private final float mY; + private final float mRadius; + + public RadialGradientPaint(float x, float y, float radius, + int[] colors, float[] positions, TileMode mode) { + super(colors, positions, mode); + mX = x; + mY = y; + mRadius = radius; + } + + @Override + public java.awt.PaintContext createContext( + java.awt.image.ColorModel colorModel, + java.awt.Rectangle deviceBounds, + java.awt.geom.Rectangle2D userBounds, + java.awt.geom.AffineTransform xform, + java.awt.RenderingHints hints) { + precomputeGradientColors(); + + java.awt.geom.AffineTransform canvasMatrix; + try { + canvasMatrix = xform.createInverse(); + } catch (java.awt.geom.NoninvertibleTransformException e) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE, + "Unable to inverse matrix in RadialGradient", e, null /*data*/); + canvasMatrix = new java.awt.geom.AffineTransform(); + } + + java.awt.geom.AffineTransform localMatrix = getLocalMatrix(); + try { + localMatrix = localMatrix.createInverse(); + } catch (java.awt.geom.NoninvertibleTransformException e) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE, + "Unable to inverse matrix in RadialGradient", e, null /*data*/); + localMatrix = new java.awt.geom.AffineTransform(); + } + + return new RadialGradientPaintContext(canvasMatrix, localMatrix, colorModel); + } + + private class RadialGradientPaintContext implements java.awt.PaintContext { + + private final java.awt.geom.AffineTransform mCanvasMatrix; + private final java.awt.geom.AffineTransform mLocalMatrix; + private final java.awt.image.ColorModel mColorModel; + + public RadialGradientPaintContext( + java.awt.geom.AffineTransform canvasMatrix, + java.awt.geom.AffineTransform localMatrix, + java.awt.image.ColorModel colorModel) { + mCanvasMatrix = canvasMatrix; + mLocalMatrix = localMatrix; + mColorModel = colorModel; + } + + @Override + public void dispose() { + } + + @Override + public java.awt.image.ColorModel getColorModel() { + return mColorModel; + } + + @Override + public java.awt.image.Raster getRaster(int x, int y, int w, int h) { + java.awt.image.BufferedImage image = new java.awt.image.BufferedImage(w, h, + java.awt.image.BufferedImage.TYPE_INT_ARGB); + + int[] data = new int[w*h]; + + // compute distance from each point to the center, and figure out the distance from + // it. + int index = 0; + float[] pt1 = new float[2]; + float[] pt2 = new float[2]; + for (int iy = 0 ; iy < h ; iy++) { + for (int ix = 0 ; ix < w ; ix++) { + // handle the canvas transform + pt1[0] = x + ix; + pt1[1] = y + iy; + mCanvasMatrix.transform(pt1, 0, pt2, 0, 1); + + // handle the local matrix + pt1[0] = pt2[0] - mX; + pt1[1] = pt2[1] - mY; + mLocalMatrix.transform(pt1, 0, pt2, 0, 1); + + float _x = pt2[0]; + float _y = pt2[1]; + float distance = (float) Math.sqrt(_x * _x + _y * _y); + + data[index++] = getGradientColor(distance / mRadius); + } + } + + image.setRGB(0 /*startX*/, 0 /*startY*/, w, h, data, 0 /*offset*/, w /*scansize*/); + + return image.getRaster(); + } + + } + } + +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Rasterizer_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Rasterizer_Delegate.java new file mode 100644 index 000000000000..2812b6b2539b --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Rasterizer_Delegate.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.graphics.Rasterizer + * + * Through the layoutlib_create tool, the original native methods of Rasterizer have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original Rasterizer class. + * + * This also serve as a base class for all Rasterizer delegate classes. + * + * @see DelegateManager + * + */ +public abstract class Rasterizer_Delegate { + + // ---- delegate manager ---- + protected static final DelegateManager<Rasterizer_Delegate> sManager = + new DelegateManager<Rasterizer_Delegate>(Rasterizer_Delegate.class); + + // ---- delegate helper data ---- + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + public static Rasterizer_Delegate getDelegate(int nativeShader) { + return sManager.getDelegate(nativeShader); + } + + public abstract boolean isSupported(); + public abstract String getSupportMessage(); + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static void finalizer(int native_instance) { + sManager.removeJavaReferenceFor(native_instance); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Region_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Region_Delegate.java new file mode 100644 index 000000000000..cb31b8fdb711 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Region_Delegate.java @@ -0,0 +1,484 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.os.Parcel; + +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Rectangle2D; + +/** + * Delegate implementing the native methods of android.graphics.Region + * + * Through the layoutlib_create tool, the original native methods of Region have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original Region class. + * + * This also serve as a base class for all Region delegate classes. + * + * @see DelegateManager + * + */ +public class Region_Delegate { + + // ---- delegate manager ---- + protected static final DelegateManager<Region_Delegate> sManager = + new DelegateManager<Region_Delegate>(Region_Delegate.class); + + // ---- delegate helper data ---- + + // ---- delegate data ---- + private Area mArea = new Area(); + + // ---- Public Helper methods ---- + + public static Region_Delegate getDelegate(int nativeShader) { + return sManager.getDelegate(nativeShader); + } + + public Area getJavaArea() { + return mArea; + } + + /** + * Combines two {@link Shape} into another one (actually an {@link Area}), according + * to the given {@link Region.Op}. + * + * If the Op is not one that combines two shapes, then this return null + * + * @param shape1 the firt shape to combine which can be null if there's no original clip. + * @param shape2 the 2nd shape to combine + * @param regionOp the operande for the combine + * @return a new area or null. + */ + public static Area combineShapes(Shape shape1, Shape shape2, int regionOp) { + if (regionOp == Region.Op.DIFFERENCE.nativeInt) { + // if shape1 is null (empty), then the result is null. + if (shape1 == null) { + return null; + } + + // result is always a new area. + Area result = new Area(shape1); + result.subtract(shape2 instanceof Area ? (Area) shape2 : new Area(shape2)); + return result; + + } else if (regionOp == Region.Op.INTERSECT.nativeInt) { + // if shape1 is null, then the result is simply shape2. + if (shape1 == null) { + return new Area(shape2); + } + + // result is always a new area. + Area result = new Area(shape1); + result.intersect(shape2 instanceof Area ? (Area) shape2 : new Area(shape2)); + return result; + + } else if (regionOp == Region.Op.UNION.nativeInt) { + // if shape1 is null, then the result is simply shape2. + if (shape1 == null) { + return new Area(shape2); + } + + // result is always a new area. + Area result = new Area(shape1); + result.add(shape2 instanceof Area ? (Area) shape2 : new Area(shape2)); + return result; + + } else if (regionOp == Region.Op.XOR.nativeInt) { + // if shape1 is null, then the result is simply shape2 + if (shape1 == null) { + return new Area(shape2); + } + + // result is always a new area. + Area result = new Area(shape1); + result.exclusiveOr(shape2 instanceof Area ? (Area) shape2 : new Area(shape2)); + return result; + + } else if (regionOp == Region.Op.REVERSE_DIFFERENCE.nativeInt) { + // result is always a new area. + Area result = new Area(shape2); + + if (shape1 != null) { + result.subtract(shape1 instanceof Area ? (Area) shape1 : new Area(shape1)); + } + + return result; + } + + return null; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static boolean isEmpty(Region thisRegion) { + Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion); + if (regionDelegate == null) { + return true; + } + + return regionDelegate.mArea.isEmpty(); + } + + @LayoutlibDelegate + /*package*/ static boolean isRect(Region thisRegion) { + Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion); + if (regionDelegate == null) { + return true; + } + + return regionDelegate.mArea.isRectangular(); + } + + @LayoutlibDelegate + /*package*/ static boolean isComplex(Region thisRegion) { + Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion); + if (regionDelegate == null) { + return true; + } + + return regionDelegate.mArea.isSingular() == false; + } + + @LayoutlibDelegate + /*package*/ static boolean contains(Region thisRegion, int x, int y) { + Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion); + if (regionDelegate == null) { + return false; + } + + return regionDelegate.mArea.contains(x, y); + } + + @LayoutlibDelegate + /*package*/ static boolean quickContains(Region thisRegion, + int left, int top, int right, int bottom) { + Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion); + if (regionDelegate == null) { + return false; + } + + return regionDelegate.mArea.isRectangular() && + regionDelegate.mArea.contains(left, top, right - left, bottom - top); + } + + @LayoutlibDelegate + /*package*/ static boolean quickReject(Region thisRegion, + int left, int top, int right, int bottom) { + Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion); + if (regionDelegate == null) { + return false; + } + + return regionDelegate.mArea.isEmpty() || + regionDelegate.mArea.intersects(left, top, right - left, bottom - top) == false; + } + + @LayoutlibDelegate + /*package*/ static boolean quickReject(Region thisRegion, Region rgn) { + Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion); + if (regionDelegate == null) { + return false; + } + + Region_Delegate targetRegionDelegate = sManager.getDelegate(rgn.mNativeRegion); + if (targetRegionDelegate == null) { + return false; + } + + return regionDelegate.mArea.isEmpty() || + regionDelegate.mArea.getBounds().intersects( + targetRegionDelegate.mArea.getBounds()) == false; + + } + + @LayoutlibDelegate + /*package*/ static void translate(Region thisRegion, int dx, int dy, Region dst) { + Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion); + if (regionDelegate == null) { + return; + } + + Region_Delegate targetRegionDelegate = sManager.getDelegate(dst.mNativeRegion); + if (targetRegionDelegate == null) { + return; + } + + if (regionDelegate.mArea.isEmpty()) { + targetRegionDelegate.mArea = new Area(); + } else { + targetRegionDelegate.mArea = new Area(regionDelegate.mArea); + AffineTransform mtx = new AffineTransform(); + mtx.translate(dx, dy); + targetRegionDelegate.mArea.transform(mtx); + } + } + + @LayoutlibDelegate + /*package*/ static void scale(Region thisRegion, float scale, Region dst) { + Region_Delegate regionDelegate = sManager.getDelegate(thisRegion.mNativeRegion); + if (regionDelegate == null) { + return; + } + + Region_Delegate targetRegionDelegate = sManager.getDelegate(dst.mNativeRegion); + if (targetRegionDelegate == null) { + return; + } + + if (regionDelegate.mArea.isEmpty()) { + targetRegionDelegate.mArea = new Area(); + } else { + targetRegionDelegate.mArea = new Area(regionDelegate.mArea); + AffineTransform mtx = new AffineTransform(); + mtx.scale(scale, scale); + targetRegionDelegate.mArea.transform(mtx); + } + } + + @LayoutlibDelegate + /*package*/ static int nativeConstructor() { + Region_Delegate newDelegate = new Region_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static void nativeDestructor(int native_region) { + sManager.removeJavaReferenceFor(native_region); + } + + @LayoutlibDelegate + /*package*/ static boolean nativeSetRegion(int native_dst, int native_src) { + Region_Delegate dstRegion = sManager.getDelegate(native_dst); + if (dstRegion == null) { + return true; + } + + Region_Delegate srcRegion = sManager.getDelegate(native_src); + if (srcRegion == null) { + return true; + } + + dstRegion.mArea.reset(); + dstRegion.mArea.add(srcRegion.mArea); + + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeSetRect(int native_dst, + int left, int top, int right, int bottom) { + Region_Delegate dstRegion = sManager.getDelegate(native_dst); + if (dstRegion == null) { + return true; + } + + dstRegion.mArea = new Area(new Rectangle2D.Float(left, top, right - left, bottom - top)); + return dstRegion.mArea.getBounds().isEmpty() == false; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeSetPath(int native_dst, int native_path, int native_clip) { + Region_Delegate dstRegion = sManager.getDelegate(native_dst); + if (dstRegion == null) { + return true; + } + + Path_Delegate path = Path_Delegate.getDelegate(native_path); + if (path == null) { + return true; + } + + dstRegion.mArea = new Area(path.getJavaShape()); + + Region_Delegate clip = sManager.getDelegate(native_clip); + if (clip != null) { + dstRegion.mArea.subtract(clip.getJavaArea()); + } + + return dstRegion.mArea.getBounds().isEmpty() == false; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeGetBounds(int native_region, Rect rect) { + Region_Delegate region = sManager.getDelegate(native_region); + if (region == null) { + return true; + } + + Rectangle bounds = region.mArea.getBounds(); + if (bounds.isEmpty()) { + rect.left = rect.top = rect.right = rect.bottom = 0; + return false; + } + + rect.left = bounds.x; + rect.top = bounds.y; + rect.right = bounds.x + bounds.width; + rect.bottom = bounds.y + bounds.height; + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeGetBoundaryPath(int native_region, int native_path) { + Region_Delegate region = sManager.getDelegate(native_region); + if (region == null) { + return false; + } + + Path_Delegate path = Path_Delegate.getDelegate(native_path); + if (path == null) { + return false; + } + + if (region.mArea.isEmpty()) { + path.reset(); + return false; + } + + path.setPathIterator(region.mArea.getPathIterator(new AffineTransform())); + return true; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeOp(int native_dst, + int left, int top, int right, int bottom, int op) { + Region_Delegate region = sManager.getDelegate(native_dst); + if (region == null) { + return false; + } + + region.mArea = combineShapes(region.mArea, + new Rectangle2D.Float(left, top, right - left, bottom - top), op); + + assert region.mArea != null; + if (region.mArea != null) { + region.mArea = new Area(); + } + + return region.mArea.getBounds().isEmpty() == false; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeOp(int native_dst, Rect rect, int native_region, int op) { + Region_Delegate region = sManager.getDelegate(native_dst); + if (region == null) { + return false; + } + + region.mArea = combineShapes(region.mArea, + new Rectangle2D.Float(rect.left, rect.top, rect.width(), rect.height()), op); + + assert region.mArea != null; + if (region.mArea != null) { + region.mArea = new Area(); + } + + return region.mArea.getBounds().isEmpty() == false; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeOp(int native_dst, + int native_region1, int native_region2, int op) { + Region_Delegate dstRegion = sManager.getDelegate(native_dst); + if (dstRegion == null) { + return true; + } + + Region_Delegate region1 = sManager.getDelegate(native_region1); + if (region1 == null) { + return false; + } + + Region_Delegate region2 = sManager.getDelegate(native_region2); + if (region2 == null) { + return false; + } + + dstRegion.mArea = combineShapes(region1.mArea, region2.mArea, op); + + assert dstRegion.mArea != null; + if (dstRegion.mArea != null) { + dstRegion.mArea = new Area(); + } + + return dstRegion.mArea.getBounds().isEmpty() == false; + + } + + @LayoutlibDelegate + /*package*/ static int nativeCreateFromParcel(Parcel p) { + // This is only called by Region.CREATOR (Parcelable.Creator<Region>), which is only + // used during aidl call so really this should not be called. + Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, + "AIDL is not suppored, and therefore Regions cannot be created from parcels.", + null /*data*/); + return 0; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeWriteToParcel(int native_region, + Parcel p) { + // This is only called when sending a region through aidl, so really this should not + // be called. + Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, + "AIDL is not suppored, and therefore Regions cannot be written to parcels.", + null /*data*/); + return false; + } + + @LayoutlibDelegate + /*package*/ static boolean nativeEquals(int native_r1, int native_r2) { + Region_Delegate region1 = sManager.getDelegate(native_r1); + if (region1 == null) { + return false; + } + + Region_Delegate region2 = sManager.getDelegate(native_r2); + if (region2 == null) { + return false; + } + + return region1.mArea.equals(region2.mArea); + } + + @LayoutlibDelegate + /*package*/ static String nativeToString(int native_region) { + Region_Delegate region = sManager.getDelegate(native_region); + if (region == null) { + return "not found"; + } + + return region.mArea.toString(); + } + + // ---- Private delegate/helper methods ---- + +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Shader_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Shader_Delegate.java new file mode 100644 index 000000000000..368c0384ded5 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Shader_Delegate.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.graphics.Shader.TileMode; + +/** + * Delegate implementing the native methods of android.graphics.Shader + * + * Through the layoutlib_create tool, the original native methods of Shader have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original Shader class. + * + * This also serve as a base class for all Shader delegate classes. + * + * @see DelegateManager + * + */ +public abstract class Shader_Delegate { + + // ---- delegate manager ---- + protected static final DelegateManager<Shader_Delegate> sManager = + new DelegateManager<Shader_Delegate>(Shader_Delegate.class); + + // ---- delegate helper data ---- + + // ---- delegate data ---- + private Matrix_Delegate mLocalMatrix = null; + + // ---- Public Helper methods ---- + + public static Shader_Delegate getDelegate(int nativeShader) { + return sManager.getDelegate(nativeShader); + } + + /** + * Returns the {@link TileMode} matching the given int. + * @param tileMode the tile mode int value + * @return the TileMode enum. + */ + public static TileMode getTileMode(int tileMode) { + for (TileMode tm : TileMode.values()) { + if (tm.nativeInt == tileMode) { + return tm; + } + } + + assert false; + return TileMode.CLAMP; + } + + public abstract java.awt.Paint getJavaPaint(); + public abstract boolean isSupported(); + public abstract String getSupportMessage(); + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static void nativeDestructor(int native_shader, int native_skiaShader) { + sManager.removeJavaReferenceFor(native_shader); + } + + @LayoutlibDelegate + /*package*/ static void nativeSetLocalMatrix(int native_shader, int native_skiaShader, + int matrix_instance) { + // get the delegate from the native int. + Shader_Delegate shaderDelegate = sManager.getDelegate(native_shader); + if (shaderDelegate == null) { + return; + } + + shaderDelegate.mLocalMatrix = Matrix_Delegate.getDelegate(matrix_instance); + } + + // ---- Private delegate/helper methods ---- + + protected java.awt.geom.AffineTransform getLocalMatrix() { + if (mLocalMatrix != null) { + return mLocalMatrix.getAffineTransform(); + } + + return new java.awt.geom.AffineTransform(); + } + +} diff --git a/tools/layoutlib/bridge/src/android/graphics/SumPathEffect_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/SumPathEffect_Delegate.java new file mode 100644 index 000000000000..410df0cf72f5 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/SumPathEffect_Delegate.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.Stroke; + +/** + * Delegate implementing the native methods of android.graphics.SumPathEffect + * + * Through the layoutlib_create tool, the original native methods of SumPathEffect have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original SumPathEffect class. + * + * Because this extends {@link PathEffect_Delegate}, there's no need to use a {@link DelegateManager}, + * as all the Shader classes will be added to the manager owned by {@link PathEffect_Delegate}. + * + * @see PathEffect_Delegate + * + */ +public class SumPathEffect_Delegate extends PathEffect_Delegate { + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + @Override + public Stroke getStroke(Paint_Delegate paint) { + // FIXME + return null; + } + + @Override + public boolean isSupported() { + return false; + } + + @Override + public String getSupportMessage() { + return "Sum Path Effects are not supported in Layout Preview mode."; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate(int first, int second) { + SumPathEffect_Delegate newDelegate = new SumPathEffect_Delegate(); + return sManager.addNewDelegate(newDelegate); + } + + // ---- Private delegate/helper methods ---- +} diff --git a/tools/layoutlib/bridge/src/android/graphics/SweepGradient_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/SweepGradient_Delegate.java new file mode 100644 index 000000000000..13ae12e5a9fa --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/SweepGradient_Delegate.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.graphics.SweepGradient + * + * Through the layoutlib_create tool, the original native methods of SweepGradient have been + * replaced by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original SweepGradient class. + * + * Because this extends {@link Shader_Delegate}, there's no need to use a {@link DelegateManager}, + * as all the Shader classes will be added to the manager owned by {@link Shader_Delegate}. + * + * @see Shader_Delegate + * + */ +public class SweepGradient_Delegate extends Gradient_Delegate { + + // ---- delegate data ---- + private java.awt.Paint mJavaPaint; + + // ---- Public Helper methods ---- + + @Override + public java.awt.Paint getJavaPaint() { + return mJavaPaint; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static int nativeCreate1(float x, float y, int colors[], float positions[]) { + SweepGradient_Delegate newDelegate = new SweepGradient_Delegate(x, y, colors, positions); + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static int nativeCreate2(float x, float y, int color0, int color1) { + return nativeCreate1(x, y, new int[] { color0, color1 }, null /*positions*/); + } + + @LayoutlibDelegate + /*package*/ static int nativePostCreate1(int native_shader, float cx, float cy, + int[] colors, float[] positions) { + // nothing to be done here. + return 0; + } + + @LayoutlibDelegate + /*package*/ static int nativePostCreate2(int native_shader, float cx, float cy, + int color0, int color1) { + // nothing to be done here. + return 0; + } + + // ---- Private delegate/helper methods ---- + + /** + * 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. + */ + private SweepGradient_Delegate(float cx, float cy, + int colors[], float positions[]) { + super(colors, positions); + mJavaPaint = new SweepGradientPaint(cx, cy, mColors, mPositions); + } + + private class SweepGradientPaint extends GradientPaint { + + private final float mCx; + private final float mCy; + + public SweepGradientPaint(float cx, float cy, int[] colors, + float[] positions) { + super(colors, positions, null /*tileMode*/); + mCx = cx; + mCy = cy; + } + + @Override + public java.awt.PaintContext createContext( + java.awt.image.ColorModel colorModel, + java.awt.Rectangle deviceBounds, + java.awt.geom.Rectangle2D userBounds, + java.awt.geom.AffineTransform xform, + java.awt.RenderingHints hints) { + precomputeGradientColors(); + + java.awt.geom.AffineTransform canvasMatrix; + try { + canvasMatrix = xform.createInverse(); + } catch (java.awt.geom.NoninvertibleTransformException e) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE, + "Unable to inverse matrix in SweepGradient", e, null /*data*/); + canvasMatrix = new java.awt.geom.AffineTransform(); + } + + java.awt.geom.AffineTransform localMatrix = getLocalMatrix(); + try { + localMatrix = localMatrix.createInverse(); + } catch (java.awt.geom.NoninvertibleTransformException e) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_MATRIX_INVERSE, + "Unable to inverse matrix in SweepGradient", e, null /*data*/); + localMatrix = new java.awt.geom.AffineTransform(); + } + + return new SweepGradientPaintContext(canvasMatrix, localMatrix, colorModel); + } + + private class SweepGradientPaintContext implements java.awt.PaintContext { + + private final java.awt.geom.AffineTransform mCanvasMatrix; + private final java.awt.geom.AffineTransform mLocalMatrix; + private final java.awt.image.ColorModel mColorModel; + + public SweepGradientPaintContext( + java.awt.geom.AffineTransform canvasMatrix, + java.awt.geom.AffineTransform localMatrix, + java.awt.image.ColorModel colorModel) { + mCanvasMatrix = canvasMatrix; + mLocalMatrix = localMatrix; + mColorModel = colorModel; + } + + @Override + public void dispose() { + } + + @Override + public java.awt.image.ColorModel getColorModel() { + return mColorModel; + } + + @Override + public java.awt.image.Raster getRaster(int x, int y, int w, int h) { + java.awt.image.BufferedImage image = new java.awt.image.BufferedImage(w, h, + java.awt.image.BufferedImage.TYPE_INT_ARGB); + + int[] data = new int[w*h]; + + // compute angle from each point to the center, and figure out the distance from + // it. + int index = 0; + float[] pt1 = new float[2]; + float[] pt2 = new float[2]; + for (int iy = 0 ; iy < h ; iy++) { + for (int ix = 0 ; ix < w ; ix++) { + // handle the canvas transform + pt1[0] = x + ix; + pt1[1] = y + iy; + mCanvasMatrix.transform(pt1, 0, pt2, 0, 1); + + // handle the local matrix + pt1[0] = pt2[0] - mCx; + pt1[1] = pt2[1] - mCy; + mLocalMatrix.transform(pt1, 0, pt2, 0, 1); + + float dx = pt2[0]; + float dy = pt2[1]; + + float angle; + if (dx == 0) { + angle = (float) (dy < 0 ? 3 * Math.PI / 2 : Math.PI / 2); + } else if (dy == 0) { + angle = (float) (dx < 0 ? Math.PI : 0); + } else { + angle = (float) Math.atan(dy / dx); + if (dx > 0) { + if (dy < 0) { + angle += Math.PI * 2; + } + } else { + angle += Math.PI; + } + } + + // convert to 0-1. value and get color + data[index++] = getGradientColor((float) (angle / (2 * Math.PI))); + } + } + + image.setRGB(0 /*startX*/, 0 /*startY*/, w, h, data, 0 /*offset*/, w /*scansize*/); + + return image.getRaster(); + } + + } + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Typeface_Accessor.java b/tools/layoutlib/bridge/src/android/graphics/Typeface_Accessor.java new file mode 100644 index 000000000000..adad2ac55f27 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Typeface_Accessor.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +/** + * Class allowing access to package-protected methods/fields. + */ +public class Typeface_Accessor { + + public static void resetDefaults() { + Typeface.sDefaults = null; + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java new file mode 100644 index 000000000000..8701cc8068a6 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.layoutlib.bridge.impl.FontLoader; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.content.res.AssetManager; + +import java.awt.Font; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Delegate implementing the native methods of android.graphics.Typeface + * + * Through the layoutlib_create tool, the original native methods of Typeface have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original Typeface class. + * + * @see DelegateManager + * + */ +public final class Typeface_Delegate { + + private static final String SYSTEM_FONTS = "/system/fonts/"; + + // ---- delegate manager ---- + private static final DelegateManager<Typeface_Delegate> sManager = + new DelegateManager<Typeface_Delegate>(Typeface_Delegate.class); + + // ---- delegate helper data ---- + private static final String DEFAULT_FAMILY = "sans-serif"; + + private static FontLoader sFontLoader; + private static final List<Typeface_Delegate> sPostInitDelegate = + new ArrayList<Typeface_Delegate>(); + + // ---- delegate data ---- + + private final String mFamily; + private int mStyle; + private List<Font> mFonts; + + + // ---- Public Helper methods ---- + + public static synchronized void init(FontLoader fontLoader) { + sFontLoader = fontLoader; + + for (Typeface_Delegate delegate : sPostInitDelegate) { + delegate.init(); + } + sPostInitDelegate.clear(); + } + + public static Typeface_Delegate getDelegate(int nativeTypeface) { + return sManager.getDelegate(nativeTypeface); + } + + public static List<Font> getFonts(Typeface typeface) { + return getFonts(typeface.native_instance); + } + + public static List<Font> getFonts(int native_int) { + Typeface_Delegate delegate = sManager.getDelegate(native_int); + if (delegate == null) { + return null; + } + + return delegate.getFonts(); + } + + public List<Font> getFonts() { + return mFonts; + } + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static synchronized int nativeCreate(String familyName, int style) { + if (familyName == null) { + familyName = DEFAULT_FAMILY; + } + + Typeface_Delegate newDelegate = new Typeface_Delegate(familyName, style); + if (sFontLoader != null) { + newDelegate.init(); + } else { + // font loader has not been initialized yet, add the delegate to a list of delegates + // to init when the font loader is initialized. + // There won't be any rendering before this happens anyway. + sPostInitDelegate.add(newDelegate); + } + + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static synchronized int nativeCreateFromTypeface(int native_instance, int style) { + Typeface_Delegate delegate = sManager.getDelegate(native_instance); + if (delegate == null) { + return 0; + } + + Typeface_Delegate newDelegate = new Typeface_Delegate(delegate.mFamily, style); + if (sFontLoader != null) { + newDelegate.init(); + } else { + // font loader has not been initialized yet, add the delegate to a list of delegates + // to init when the font loader is initialized. + // There won't be any rendering before this happens anyway. + sPostInitDelegate.add(newDelegate); + } + + return sManager.addNewDelegate(newDelegate); + } + + @LayoutlibDelegate + /*package*/ static synchronized int nativeCreateFromAsset(AssetManager mgr, String path) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Typeface.createFromAsset() is not supported.", null /*throwable*/, null /*data*/); + return 0; + } + + @LayoutlibDelegate + /*package*/ static synchronized int nativeCreateFromFile(String path) { + if (path.startsWith(SYSTEM_FONTS) ) { + String relativePath = path.substring(SYSTEM_FONTS.length()); + File f = new File(sFontLoader.getOsFontsLocation(), relativePath); + + try { + Font font = Font.createFont(Font.TRUETYPE_FONT, f); + if (font != null) { + Typeface_Delegate newDelegate = new Typeface_Delegate(font); + return sManager.addNewDelegate(newDelegate); + } + } catch (Exception e) { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN, + String.format("Unable to load font %1$s", relativePath), + null /*throwable*/, null /*data*/); + } + } else { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, + "Typeface.createFromFile() can only work with platform fonts located in " + + SYSTEM_FONTS, + null /*throwable*/, null /*data*/); + } + + + // return a copy of the base font + return nativeCreate(null, 0); + } + + @LayoutlibDelegate + /*package*/ static void nativeUnref(int native_instance) { + sManager.removeJavaReferenceFor(native_instance); + } + + @LayoutlibDelegate + /*package*/ static int nativeGetStyle(int native_instance) { + Typeface_Delegate delegate = sManager.getDelegate(native_instance); + if (delegate == null) { + return 0; + } + + return delegate.mStyle; + } + + // ---- Private delegate/helper methods ---- + + private Typeface_Delegate(String family, int style) { + mFamily = family; + mStyle = style; + } + + private Typeface_Delegate(Font font) { + mFamily = font.getFamily(); + mStyle = Typeface.NORMAL; + + mFonts = sFontLoader.getFallbackFonts(mStyle); + + // insert the font glyph first. + mFonts.add(0, font); + } + + private void init() { + mFonts = sFontLoader.getFont(mFamily, mStyle); + } +} diff --git a/tools/layoutlib/bridge/src/android/graphics/Xfermode_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Xfermode_Delegate.java new file mode 100644 index 000000000000..962d69cb6e8d --- /dev/null +++ b/tools/layoutlib/bridge/src/android/graphics/Xfermode_Delegate.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.awt.Composite; + +/** + * Delegate implementing the native methods of android.graphics.Xfermode + * + * Through the layoutlib_create tool, the original native methods of Xfermode have been replaced + * by calls to methods of the same name in this delegate class. + * + * This class behaves like the original native implementation, but in Java, keeping previously + * native data into its own objects and mapping them to int that are sent back and forth between + * it and the original Xfermode class. + * + * This also serve as a base class for all Xfermode delegate classes. + * + * @see DelegateManager + * + */ +public abstract class Xfermode_Delegate { + + // ---- delegate manager ---- + protected static final DelegateManager<Xfermode_Delegate> sManager = + new DelegateManager<Xfermode_Delegate>(Xfermode_Delegate.class); + + // ---- delegate helper data ---- + + // ---- delegate data ---- + + // ---- Public Helper methods ---- + + public static Xfermode_Delegate getDelegate(int native_instance) { + return sManager.getDelegate(native_instance); + } + + public abstract Composite getComposite(int alpha); + public abstract boolean isSupported(); + public abstract String getSupportMessage(); + + + // ---- native methods ---- + + @LayoutlibDelegate + /*package*/ static void finalizer(int native_instance) { + sManager.removeJavaReferenceFor(native_instance); + } + + // ---- Private delegate/helper methods ---- + +} diff --git a/tools/layoutlib/bridge/src/android/os/Build_Delegate.java b/tools/layoutlib/bridge/src/android/os/Build_Delegate.java new file mode 100644 index 000000000000..ff82a5e25f55 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/os/Build_Delegate.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.util.Map; + +/** + * Delegate implementing the native methods of android.os.Build + * + * Through the layoutlib_create tool, the original native methods of Build have been replaced + * by calls to methods of the same name in this delegate class. + * + * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager} + * around to map int to instance of the delegate. + * + */ +public class Build_Delegate { + + @LayoutlibDelegate + /*package*/ static String getString(String property) { + Map<String, String> properties = Bridge.getPlatformProperties(); + String value = properties.get(property); + if (value != null) { + return value; + } + + return Build.UNKNOWN; + } + +} diff --git a/tools/layoutlib/bridge/src/android/os/HandlerThread_Delegate.java b/tools/layoutlib/bridge/src/android/os/HandlerThread_Delegate.java new file mode 100644 index 000000000000..afbe97c06ebb --- /dev/null +++ b/tools/layoutlib/bridge/src/android/os/HandlerThread_Delegate.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.impl.RenderAction; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Delegate overriding selected methods of android.os.HandlerThread + * + * Through the layoutlib_create tool, selected methods of Handler have been replaced + * by calls to methods of the same name in this delegate class. + * + * + */ +public class HandlerThread_Delegate { + + private static Map<BridgeContext, List<HandlerThread>> sThreads = + new HashMap<BridgeContext, List<HandlerThread>>(); + + public static void cleanUp(BridgeContext context) { + List<HandlerThread> list = sThreads.get(context); + if (list != null) { + for (HandlerThread thread : list) { + thread.quit(); + } + + list.clear(); + sThreads.remove(context); + } + } + + // -------- Delegate methods + + @LayoutlibDelegate + /*package*/ static void run(HandlerThread theThread) { + // record the thread so that it can be quit() on clean up. + BridgeContext context = RenderAction.getCurrentContext(); + List<HandlerThread> list = sThreads.get(context); + if (list == null) { + list = new ArrayList<HandlerThread>(); + sThreads.put(context, list); + } + + list.add(theThread); + + // ---- START DEFAULT IMPLEMENTATION. + + theThread.mTid = Process.myTid(); + Looper.prepare(); + synchronized (theThread) { + theThread.mLooper = Looper.myLooper(); + theThread.notifyAll(); + } + Process.setThreadPriority(theThread.mPriority); + theThread.onLooperPrepared(); + Looper.loop(); + theThread.mTid = -1; + } +} diff --git a/tools/layoutlib/bridge/src/android/os/Handler_Delegate.java b/tools/layoutlib/bridge/src/android/os/Handler_Delegate.java new file mode 100644 index 000000000000..2152c8ad19ae --- /dev/null +++ b/tools/layoutlib/bridge/src/android/os/Handler_Delegate.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + + +/** + * Delegate overriding selected methods of android.os.Handler + * + * Through the layoutlib_create tool, selected methods of Handler have been replaced + * by calls to methods of the same name in this delegate class. + * + * + */ +public class Handler_Delegate { + + // -------- Delegate methods + + @LayoutlibDelegate + /*package*/ static boolean sendMessageAtTime(Handler handler, Message msg, long uptimeMillis) { + // get the callback + IHandlerCallback callback = sCallbacks.get(); + if (callback != null) { + callback.sendMessageAtTime(handler, msg, uptimeMillis); + } + return true; + } + + // -------- Delegate implementation + + public interface IHandlerCallback { + void sendMessageAtTime(Handler handler, Message msg, long uptimeMillis); + } + + private final static ThreadLocal<IHandlerCallback> sCallbacks = + new ThreadLocal<IHandlerCallback>(); + + public static void setCallback(IHandlerCallback callback) { + sCallbacks.set(callback); + } + +} diff --git a/tools/layoutlib/bridge/src/android/os/Looper_Accessor.java b/tools/layoutlib/bridge/src/android/os/Looper_Accessor.java new file mode 100644 index 000000000000..09f3e47d7a14 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/os/Looper_Accessor.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.os; + +import java.lang.reflect.Field; + +/** + * Class allowing access to package-protected methods/fields. + */ +public class Looper_Accessor { + + public static void cleanupThread() { + // clean up the looper + Looper.sThreadLocal.remove(); + try { + Field sMainLooper = Looper.class.getDeclaredField("sMainLooper"); + sMainLooper.setAccessible(true); + sMainLooper.set(null, null); + } catch (SecurityException e) { + catchReflectionException(); + } catch (IllegalArgumentException e) { + catchReflectionException(); + } catch (NoSuchFieldException e) { + catchReflectionException(); + } catch (IllegalAccessException e) { + catchReflectionException(); + } + + } + + private static void catchReflectionException() { + assert(false); + } +} diff --git a/tools/layoutlib/bridge/src/android/os/ServiceManager.java b/tools/layoutlib/bridge/src/android/os/ServiceManager.java new file mode 100644 index 000000000000..6a68ee29c9cb --- /dev/null +++ b/tools/layoutlib/bridge/src/android/os/ServiceManager.java @@ -0,0 +1,72 @@ +/* + * 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 android.os; + +import java.util.Map; + +public final class ServiceManager { + + /** + * Returns a reference to a service with the given name. + * + * @param name the name of the service to get + * @return a reference to the service, or <code>null</code> if the service doesn't exist + */ + public static IBinder getService(String name) { + return null; + } + + /** + * Place a new @a service called @a name into the service + * manager. + * + * @param name the name of the new service + * @param service the service object + */ + public static void addService(String name, IBinder service) { + // pass + } + + /** + * Retrieve an existing service called @a name from the + * service manager. Non-blocking. + */ + public static IBinder checkService(String name) { + return null; + } + + /** + * Return a list of all currently running services. + */ + public static String[] listServices() throws RemoteException { + // actual implementation returns null sometimes, so it's ok + // to return null instead of an empty list. + return null; + } + + /** + * This is only intended to be called when the process is first being brought + * up and bound by the activity manager. There is only one thread in the process + * at that time, so no locking is done. + * + * @param cache the cache of service references + * @hide + */ + public static void initServiceCache(Map<String, IBinder> cache) { + // pass + } +} diff --git a/tools/layoutlib/bridge/src/android/os/SystemClock_Delegate.java b/tools/layoutlib/bridge/src/android/os/SystemClock_Delegate.java new file mode 100644 index 000000000000..fd594f79c237 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/os/SystemClock_Delegate.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.os.SystemClock + * + * Through the layoutlib_create tool, the original native methods of SystemClock have been replaced + * by calls to methods of the same name in this delegate class. + * + * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager} + * around to map int to instance of the delegate. + * + */ +public class SystemClock_Delegate { + private static long sBootTime = System.currentTimeMillis(); + private static long sBootTimeNano = System.nanoTime(); + + @LayoutlibDelegate + /*package*/ static boolean setCurrentTimeMillis(long millis) { + return true; + } + + /** + * Returns milliseconds since boot, not counting time spent in deep sleep. + * <b>Note:</b> This value may get reset occasionally (before it would + * otherwise wrap around). + * + * @return milliseconds of non-sleep uptime since boot. + */ + @LayoutlibDelegate + /*package*/ static long uptimeMillis() { + return System.currentTimeMillis() - sBootTime; + } + + /** + * Returns milliseconds since boot, including time spent in sleep. + * + * @return elapsed milliseconds since boot. + */ + @LayoutlibDelegate + /*package*/ static long elapsedRealtime() { + return System.currentTimeMillis() - sBootTime; + } + + /** + * Returns nanoseconds since boot, including time spent in sleep. + * + * @return elapsed nanoseconds since boot. + */ + @LayoutlibDelegate + /*package*/ static long elapsedRealtimeNanos() { + return System.nanoTime() - sBootTimeNano; + } + + /** + * Returns milliseconds running in the current thread. + * + * @return elapsed milliseconds in the thread + */ + @LayoutlibDelegate + /*package*/ static long currentThreadTimeMillis() { + return System.currentTimeMillis(); + } + + /** + * Returns microseconds running in the current thread. + * + * @return elapsed microseconds in the thread + * + * @hide + */ + @LayoutlibDelegate + /*package*/ static long currentThreadTimeMicro() { + return System.currentTimeMillis() * 1000; + } + + /** + * Returns current wall time in microseconds. + * + * @return elapsed microseconds in wall time + * + * @hide + */ + @LayoutlibDelegate + /*package*/ static long currentTimeMicro() { + return elapsedRealtime() * 1000; + } +} diff --git a/tools/layoutlib/bridge/src/android/text/AndroidBidi_Delegate.java b/tools/layoutlib/bridge/src/android/text/AndroidBidi_Delegate.java new file mode 100644 index 000000000000..52b8f348adf0 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/text/AndroidBidi_Delegate.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + + +/** + * Delegate used to provide new implementation for the native methods of {@link AndroidBidi} + * + * Through the layoutlib_create tool, the original methods of AndroidBidi have been replaced + * by calls to methods of the same name in this delegate class. + * + */ +public class AndroidBidi_Delegate { + + @LayoutlibDelegate + /*package*/ static int runBidi(int dir, char[] chs, byte[] chInfo, int n, boolean haveInfo) { + // return the equivalent of Layout.DIR_LEFT_TO_RIGHT + // TODO: actually figure the direction. + return 0; + } +} diff --git a/tools/layoutlib/bridge/src/android/text/format/DateFormat_Delegate.java b/tools/layoutlib/bridge/src/android/text/format/DateFormat_Delegate.java new file mode 100644 index 000000000000..8cd1a695eabc --- /dev/null +++ b/tools/layoutlib/bridge/src/android/text/format/DateFormat_Delegate.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2013 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.text.format; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.content.Context; + + +/** + * Delegate used to provide new implementation for the native methods of {@link DateFormat} + * + * Through the layoutlib_create tool, the original methods of DateFormat have been replaced + * by calls to methods of the same name in this delegate class. + * + */ +public class DateFormat_Delegate { + + @LayoutlibDelegate + /*package*/ static boolean is24HourFormat(Context context) { + return false; + } +} diff --git a/tools/layoutlib/bridge/src/android/util/BridgeXmlPullAttributes.java b/tools/layoutlib/bridge/src/android/util/BridgeXmlPullAttributes.java new file mode 100644 index 000000000000..6ac5b02916ae --- /dev/null +++ b/tools/layoutlib/bridge/src/android/util/BridgeXmlPullAttributes.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. + */ + +package android.util; + +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.internal.util.XmlUtils; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.BridgeConstants; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.resources.ResourceType; + +import org.xmlpull.v1.XmlPullParser; + +/** + * 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.getResourceId(ResourceType.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().getResourceId(ResourceType.ATTR, name); + if (v != null) { + return v.intValue(); + } + } + + return 0; + } + + @Override + public int getAttributeListValue(String namespace, String attribute, + String[] options, int defaultValue) { + String value = getAttributeValue(namespace, attribute); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToList(value, options, defaultValue); + } + + return defaultValue; + } + + @Override + public boolean getAttributeBooleanValue(String namespace, String attribute, + boolean defaultValue) { + String value = getAttributeValue(namespace, attribute); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToBoolean(value, defaultValue); + } + + return defaultValue; + } + + @Override + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { + String value = getAttributeValue(namespace, attribute); + + return resolveResourceValue(value, defaultValue); + } + + @Override + public int getAttributeIntValue(String namespace, String attribute, + int defaultValue) { + String value = getAttributeValue(namespace, attribute); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToInt(value, defaultValue); + } + + return defaultValue; + } + + @Override + public int getAttributeUnsignedIntValue(String namespace, String attribute, + int defaultValue) { + String value = getAttributeValue(namespace, attribute); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToUnsignedInt(value, defaultValue); + } + + return defaultValue; + } + + @Override + public float getAttributeFloatValue(String namespace, String attribute, + float defaultValue) { + String s = getAttributeValue(namespace, attribute); + if (s != null) { + ResourceValue r = getResourceValue(s); + + if (r != null) { + s = r.getValue(); + } + + return Float.parseFloat(s); + } + + return defaultValue; + } + + @Override + public int getAttributeListValue(int index, + String[] options, int defaultValue) { + return XmlUtils.convertValueToList( + getAttributeValue(index), options, defaultValue); + } + + @Override + public boolean getAttributeBooleanValue(int index, boolean defaultValue) { + String value = getAttributeValue(index); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToBoolean(value, defaultValue); + } + + return defaultValue; + } + + @Override + public int getAttributeResourceValue(int index, int defaultValue) { + String value = getAttributeValue(index); + + return resolveResourceValue(value, defaultValue); + } + + @Override + public int getAttributeIntValue(int index, int defaultValue) { + String value = getAttributeValue(index); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToInt(value, defaultValue); + } + + return defaultValue; + } + + @Override + public int getAttributeUnsignedIntValue(int index, int defaultValue) { + String value = getAttributeValue(index); + if (value != null) { + ResourceValue r = getResourceValue(value); + + if (r != null) { + value = r.getValue(); + } + + return XmlUtils.convertValueToUnsignedInt(value, defaultValue); + } + + return defaultValue; + } + + @Override + public float getAttributeFloatValue(int index, float defaultValue) { + String s = getAttributeValue(index); + if (s != null) { + ResourceValue r = getResourceValue(s); + + if (r != null) { + s = r.getValue(); + } + + return Float.parseFloat(s); + } + + return defaultValue; + } + + // -- private helper methods + + /** + * Returns a resolved {@link ResourceValue} from a given value. + */ + private ResourceValue getResourceValue(String value) { + // now look for this particular value + RenderResources resources = mContext.getRenderResources(); + return resources.resolveResValue(resources.findResValue(value, mPlatformFile)); + } + + /** + * Resolves and return a value to its associated integer. + */ + private int resolveResourceValue(String value, int defaultValue) { + ResourceValue resource = getResourceValue(value); + if (resource != null) { + Integer id = null; + if (mPlatformFile || resource.isFramework()) { + id = Bridge.getResourceId(resource.getResourceType(), resource.getName()); + } else { + id = mContext.getProjectCallback().getResourceId( + resource.getResourceType(), resource.getName()); + } + + if (id != null) { + return id; + } + } + + return defaultValue; + } +} diff --git a/tools/layoutlib/bridge/src/android/util/FloatMath_Delegate.java b/tools/layoutlib/bridge/src/android/util/FloatMath_Delegate.java new file mode 100644 index 000000000000..8b4c60b5fead --- /dev/null +++ b/tools/layoutlib/bridge/src/android/util/FloatMath_Delegate.java @@ -0,0 +1,132 @@ +/* + * 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; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of android.util.FloatMath + * + * Through the layoutlib_create tool, the original native methods of FloatMath have been replaced + * by calls to methods of the same name in this delegate class. + * + * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager} + * around to map int to instance of the delegate. + * + */ +/*package*/ final class FloatMath_Delegate { + + /** Prevents instantiation. */ + private FloatMath_Delegate() {} + + /** + * 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 + */ + @LayoutlibDelegate + /*package*/ 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 + */ + @LayoutlibDelegate + /*package*/ 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 + */ + @LayoutlibDelegate + /*package*/ 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 + */ + @LayoutlibDelegate + /*package*/ 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 + */ + @LayoutlibDelegate + /*package*/ static float sqrt(float value) { + return (float)Math.sqrt(value); + } + + /** + * Returns the closest float approximation of the raising "e" to the power + * of the argument. + * + * @param value to compute the exponential of + * @return the exponential of value + */ + @LayoutlibDelegate + /*package*/ static float exp(float value) { + return (float)Math.exp(value); + } + + /** + * Returns the closest float approximation of the result of raising {@code + * x} to the power of {@code y}. + * + * @param x the base of the operation. + * @param y the exponent of the operation. + * @return {@code x} to the power of {@code y}. + */ + @LayoutlibDelegate + /*package*/ static float pow(float x, float y) { + return (float)Math.pow(x, y); + } + + /** + * Returns {@code sqrt(}<i>{@code x}</i><sup>{@code 2}</sup>{@code +} <i> + * {@code y}</i><sup>{@code 2}</sup>{@code )}. + * + * @param x a float number + * @param y a float number + * @return the hypotenuse + */ + @LayoutlibDelegate + /*package*/ static float hypot(float x, float y) { + return (float)Math.sqrt(x*x + y*y); + } +} diff --git a/tools/layoutlib/bridge/src/android/util/Log_Delegate.java b/tools/layoutlib/bridge/src/android/util/Log_Delegate.java new file mode 100644 index 000000000000..7f432abdda9f --- /dev/null +++ b/tools/layoutlib/bridge/src/android/util/Log_Delegate.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +class Log_Delegate { + // to replicate prefix visible when using 'adb logcat' + private static char priorityChar(int priority) { + switch (priority) { + case Log.VERBOSE: + return 'V'; + case Log.DEBUG: + return 'D'; + case Log.INFO: + return 'I'; + case Log.WARN: + return 'W'; + case Log.ERROR: + return 'E'; + case Log.ASSERT: + return 'A'; + default: + return '?'; + } + } + + @LayoutlibDelegate + static int println_native(int bufID, int priority, String tag, String msgs) { + String prefix = priorityChar(priority) + "/" + tag + ": "; + for (String msg: msgs.split("\n")) { + System.out.println(prefix + msg); + } + return 0; + } + +} diff --git a/tools/layoutlib/bridge/src/android/util/LruCache.java b/tools/layoutlib/bridge/src/android/util/LruCache.java new file mode 100644 index 000000000000..520860655abd --- /dev/null +++ b/tools/layoutlib/bridge/src/android/util/LruCache.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * BEGIN LAYOUTLIB CHANGE + * This is a custom version that doesn't use the non standard LinkedHashMap#eldest. + * END LAYOUTLIB CHANGE + * + * A cache that holds strong references to a limited number of values. Each time + * a value is accessed, it is moved to the head of a queue. When a value is + * added to a full cache, the value at the end of that queue is evicted and may + * become eligible for garbage collection. + * + * <p>If your cached values hold resources that need to be explicitly released, + * override {@link #entryRemoved}. + * + * <p>If a cache miss should be computed on demand for the corresponding keys, + * override {@link #create}. This simplifies the calling code, allowing it to + * assume a value will always be returned, even when there's a cache miss. + * + * <p>By default, the cache size is measured in the number of entries. Override + * {@link #sizeOf} to size the cache in different units. For example, this cache + * is limited to 4MiB of bitmaps: + * <pre> {@code + * int cacheSize = 4 * 1024 * 1024; // 4MiB + * LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) { + * protected int sizeOf(String key, Bitmap value) { + * return value.getByteCount(); + * } + * }}</pre> + * + * <p>This class is thread-safe. Perform multiple cache operations atomically by + * synchronizing on the cache: <pre> {@code + * synchronized (cache) { + * if (cache.get(key) == null) { + * cache.put(key, value); + * } + * }}</pre> + * + * <p>This class does not allow null to be used as a key or value. A return + * value of null from {@link #get}, {@link #put} or {@link #remove} is + * unambiguous: the key was not in the cache. + * + * <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part + * of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's + * Support Package</a> for earlier releases. + */ +public class LruCache<K, V> { + private final LinkedHashMap<K, V> map; + + /** Size of this cache in units. Not necessarily the number of elements. */ + private int size; + private int maxSize; + + private int putCount; + private int createCount; + private int evictionCount; + private int hitCount; + private int missCount; + + /** + * @param maxSize for caches that do not override {@link #sizeOf}, this is + * the maximum number of entries in the cache. For all other caches, + * this is the maximum sum of the sizes of the entries in this cache. + */ + public LruCache(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + this.maxSize = maxSize; + this.map = new LinkedHashMap<K, V>(0, 0.75f, true); + } + + /** + * Sets the size of the cache. + * @param maxSize The new maximum size. + * + * @hide + */ + public void resize(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + + synchronized (this) { + this.maxSize = maxSize; + } + trimToSize(maxSize); + } + + /** + * Returns the value for {@code key} if it exists in the cache or can be + * created by {@code #create}. If a value was returned, it is moved to the + * head of the queue. This returns null if a value is not cached and cannot + * be created. + */ + public final V get(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V mapValue; + synchronized (this) { + mapValue = map.get(key); + if (mapValue != null) { + hitCount++; + return mapValue; + } + missCount++; + } + + /* + * Attempt to create a value. This may take a long time, and the map + * may be different when create() returns. If a conflicting value was + * added to the map while create() was working, we leave that value in + * the map and release the created value. + */ + + V createdValue = create(key); + if (createdValue == null) { + return null; + } + + synchronized (this) { + createCount++; + mapValue = map.put(key, createdValue); + + if (mapValue != null) { + // There was a conflict so undo that last put + map.put(key, mapValue); + } else { + size += safeSizeOf(key, createdValue); + } + } + + if (mapValue != null) { + entryRemoved(false, key, createdValue, mapValue); + return mapValue; + } else { + trimToSize(maxSize); + return createdValue; + } + } + + /** + * Caches {@code value} for {@code key}. The value is moved to the head of + * the queue. + * + * @return the previous value mapped by {@code key}. + */ + public final V put(K key, V value) { + if (key == null || value == null) { + throw new NullPointerException("key == null || value == null"); + } + + V previous; + synchronized (this) { + putCount++; + size += safeSizeOf(key, value); + previous = map.put(key, value); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, value); + } + + trimToSize(maxSize); + return previous; + } + + /** + * @param maxSize the maximum size of the cache before returning. May be -1 + * to evict even 0-sized elements. + */ + private void trimToSize(int maxSize) { + while (true) { + K key; + V value; + synchronized (this) { + if (size < 0 || (map.isEmpty() && size != 0)) { + throw new IllegalStateException(getClass().getName() + + ".sizeOf() is reporting inconsistent results!"); + } + + if (size <= maxSize) { + break; + } + + // BEGIN LAYOUTLIB CHANGE + // get the last item in the linked list. + // This is not efficient, the goal here is to minimize the changes + // compared to the platform version. + Map.Entry<K, V> toEvict = null; + for (Map.Entry<K, V> entry : map.entrySet()) { + toEvict = entry; + } + // END LAYOUTLIB CHANGE + + if (toEvict == null) { + break; + } + + key = toEvict.getKey(); + value = toEvict.getValue(); + map.remove(key); + size -= safeSizeOf(key, value); + evictionCount++; + } + + entryRemoved(true, key, value, null); + } + } + + /** + * Removes the entry for {@code key} if it exists. + * + * @return the previous value mapped by {@code key}. + */ + public final V remove(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V previous; + synchronized (this) { + previous = map.remove(key); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, null); + } + + return previous; + } + + /** + * Called for entries that have been evicted or removed. This method is + * invoked when a value is evicted to make space, removed by a call to + * {@link #remove}, or replaced by a call to {@link #put}. The default + * implementation does nothing. + * + * <p>The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + * @param evicted true if the entry is being removed to make space, false + * if the removal was caused by a {@link #put} or {@link #remove}. + * @param newValue the new value for {@code key}, if it exists. If non-null, + * this removal was caused by a {@link #put}. Otherwise it was caused by + * an eviction or a {@link #remove}. + */ + protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} + + /** + * Called after a cache miss to compute a value for the corresponding key. + * Returns the computed value or null if no value can be computed. The + * default implementation returns null. + * + * <p>The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + * <p>If a value for {@code key} exists in the cache when this method + * returns, the created value will be released with {@link #entryRemoved} + * and discarded. This can occur when multiple threads request the same key + * at the same time (causing multiple values to be created), or when one + * thread calls {@link #put} while another is creating a value for the same + * key. + */ + protected V create(K key) { + return null; + } + + private int safeSizeOf(K key, V value) { + int result = sizeOf(key, value); + if (result < 0) { + throw new IllegalStateException("Negative size: " + key + "=" + value); + } + return result; + } + + /** + * Returns the size of the entry for {@code key} and {@code value} in + * user-defined units. The default implementation returns 1 so that size + * is the number of entries and max size is the maximum number of entries. + * + * <p>An entry's size must not change while it is in the cache. + */ + protected int sizeOf(K key, V value) { + return 1; + } + + /** + * Clear the cache, calling {@link #entryRemoved} on each removed entry. + */ + public final void evictAll() { + trimToSize(-1); // -1 will evict 0-sized elements + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the number + * of entries in the cache. For all other caches, this returns the sum of + * the sizes of the entries in this cache. + */ + public synchronized final int size() { + return size; + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the maximum + * number of entries in the cache. For all other caches, this returns the + * maximum sum of the sizes of the entries in this cache. + */ + public synchronized final int maxSize() { + return maxSize; + } + + /** + * Returns the number of times {@link #get} returned a value that was + * already present in the cache. + */ + public synchronized final int hitCount() { + return hitCount; + } + + /** + * Returns the number of times {@link #get} returned null or required a new + * value to be created. + */ + public synchronized final int missCount() { + return missCount; + } + + /** + * Returns the number of times {@link #create(Object)} returned a value. + */ + public synchronized final int createCount() { + return createCount; + } + + /** + * Returns the number of times {@link #put} was called. + */ + public synchronized final int putCount() { + return putCount; + } + + /** + * Returns the number of values that have been evicted. + */ + public synchronized final int evictionCount() { + return evictionCount; + } + + /** + * Returns a copy of the current contents of the cache, ordered from least + * recently accessed to most recently accessed. + */ + public synchronized final Map<K, V> snapshot() { + return new LinkedHashMap<K, V>(map); + } + + @Override public synchronized final String toString() { + int accesses = hitCount + missCount; + int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; + return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", + maxSize, hitCount, missCount, hitPercent); + } +} diff --git a/tools/layoutlib/bridge/src/android/view/AttachInfo_Accessor.java b/tools/layoutlib/bridge/src/android/view/AttachInfo_Accessor.java new file mode 100644 index 000000000000..4901f72b23d6 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/AttachInfo_Accessor.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import com.android.layoutlib.bridge.android.BridgeWindow; +import com.android.layoutlib.bridge.android.BridgeWindowSession; + +import android.content.Context; +import android.os.Handler; +import android.view.View.AttachInfo; + +/** + * Class allowing access to package-protected methods/fields. + */ +public class AttachInfo_Accessor { + + public static void setAttachInfo(View view) { + Context context = view.getContext(); + WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + ViewRootImpl root = new ViewRootImpl(context, display); + AttachInfo info = new AttachInfo(new BridgeWindowSession(), new BridgeWindow(), + display, root, new Handler(), null); + info.mHasWindowFocus = true; + info.mWindowVisibility = View.VISIBLE; + info.mInTouchMode = false; // this is so that we can display selections. + info.mHardwareAccelerated = false; + view.dispatchAttachedToWindow(info, 0); + } + + public static void dispatchOnPreDraw(View view) { + view.mAttachInfo.mTreeObserver.dispatchOnPreDraw(); + } +} 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 000000000000..941f1ce6ef22 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java @@ -0,0 +1,272 @@ +/* + * 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.ide.common.rendering.api.IProjectCallback; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.rendering.api.MergeCookie; +import com.android.ide.common.rendering.api.ResourceReference; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; +import com.android.layoutlib.bridge.impl.ParserFactory; +import com.android.resources.ResourceType; +import com.android.util.Pair; + +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.InflateException; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.io.File; + +/** + * Custom implementation of {@link LayoutInflater} to handle custom views. + */ +public final class BridgeInflater extends LayoutInflater { + + private final IProjectCallback mProjectCallback; + private boolean mIsInMerge = false; + private ResourceReference mResourceReference; + + /** + * 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(View parent, String name, AttributeSet attrs) { + View view = null; + try { + view = super.createViewFromTag(parent, 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; + + ResourceValue value = null; + + Pair<ResourceType, String> layoutInfo = Bridge.resolveResourceId(resource); + if (layoutInfo != null) { + value = bridgeContext.getRenderResources().getFrameworkResource( + ResourceType.LAYOUT, layoutInfo.getSecond()); + } else { + layoutInfo = mProjectCallback.resolveResourceId(resource); + + if (layoutInfo != null) { + value = bridgeContext.getRenderResources().getProjectResource( + ResourceType.LAYOUT, layoutInfo.getSecond()); + } + } + + if (value != null) { + File f = new File(value.getValue()); + if (f.isFile()) { + try { + XmlPullParser parser = ParserFactory.create(f); + + BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser( + parser, bridgeContext, false); + + return inflate(bridgeParser, root); + } catch (Exception e) { + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, + "Failed to parse file " + f.getAbsolutePath(), e, null /*data*/); + + return null; + } + } + } + } + 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) { + BridgeXmlBlockParser parser = (BridgeXmlBlockParser) attrs; + + // get the view key + Object viewKey = parser.getViewCookie(); + + if (viewKey == null) { + int currentDepth = parser.getDepth(); + + // test whether we are in an included file or in a adapter binding view. + BridgeXmlBlockParser previousParser = bc.getPreviousParser(); + if (previousParser != null) { + // looks like we inside an embedded layout. + // only apply the cookie of the calling node (<include>) if we are at the + // top level of the embedded layout. If there is a merge tag, then + // skip it and look for the 2nd level + int testDepth = mIsInMerge ? 2 : 1; + if (currentDepth == testDepth) { + viewKey = previousParser.getViewCookie(); + // if we are in a merge, wrap the cookie in a MergeCookie. + if (viewKey != null && mIsInMerge) { + viewKey = new MergeCookie(viewKey); + } + } + } else if (mResourceReference != null && currentDepth == 1) { + // else if there's a resource reference, this means we are in an adapter + // binding case. Set the resource ref as the view cookie only for the top + // level view. + viewKey = mResourceReference; + } + } + + if (viewKey != null) { + bc.addViewKey(view, viewKey); + } + } + } + } + + public void setIsInMerge(boolean isInMerge) { + mIsInMerge = isInMerge; + } + + public void setResourceReference(ResourceReference reference) { + mResourceReference = reference; + } + + @Override + public LayoutInflater cloneInContext(Context newContext) { + return new BridgeInflater(this, newContext); + } +} diff --git a/tools/layoutlib/bridge/src/android/view/Choreographer_Delegate.java b/tools/layoutlib/bridge/src/android/view/Choreographer_Delegate.java new file mode 100644 index 000000000000..f75ee5030674 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/Choreographer_Delegate.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012 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.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate used to provide new implementation of a select few methods of {@link Choreographer} + * + * Through the layoutlib_create tool, the original methods of Choreographer have been + * replaced by calls to methods of the same name in this delegate class. + * + */ +public class Choreographer_Delegate { + + @LayoutlibDelegate + public static float getRefreshRate() { + return 60.f; + } +} diff --git a/tools/layoutlib/bridge/src/android/view/Display_Delegate.java b/tools/layoutlib/bridge/src/android/view/Display_Delegate.java new file mode 100644 index 000000000000..53dc821f65d2 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/Display_Delegate.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + + +/** + * Delegate used to provide new implementation of a select few methods of {@link Display} + * + * Through the layoutlib_create tool, the original methods of Display have been replaced + * by calls to methods of the same name in this delegate class. + * + */ +public class Display_Delegate { + + @LayoutlibDelegate + static void updateDisplayInfoLocked(Display theDisplay) { + // do nothing + } + +} diff --git a/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java b/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java new file mode 100644 index 000000000000..f0c3a758fa3e --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java @@ -0,0 +1,502 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.graphics.Point; +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethodClient; + +import android.content.res.CompatibilityInfo; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.IBinder; +import android.os.IRemoteCallback; +import android.os.RemoteException; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.Gravity; +import android.view.IApplicationToken; +import android.view.IInputFilter; +import android.view.IOnKeyguardExitResult; +import android.view.IRotationWatcher; +import android.view.IWindowManager; +import android.view.IWindowSession; + +import java.util.List; + +/** + * Basic implementation of {@link IWindowManager} so that {@link Display} (and + * {@link Display_Delegate}) can return a valid instance. + */ +public class IWindowManagerImpl implements IWindowManager { + + private final Configuration mConfig; + private final DisplayMetrics mMetrics; + private final int mRotation; + private final boolean mHasNavigationBar; + + public IWindowManagerImpl(Configuration config, DisplayMetrics metrics, int rotation, + boolean hasNavigationBar) { + mConfig = config; + mMetrics = metrics; + mRotation = rotation; + mHasNavigationBar = hasNavigationBar; + } + + // custom API. + + public DisplayMetrics getMetrics() { + return mMetrics; + } + + // ---- implementation of IWindowManager that we care about ---- + + @Override + public int getRotation() throws RemoteException { + return mRotation; + } + + @Override + public boolean hasNavigationBar() { + return mHasNavigationBar; + } + + // ---- unused implementation of IWindowManager ---- + + @Override + public void addAppToken(int arg0, IApplicationToken arg1, int arg2, int arg3, int arg4, + boolean arg5, boolean arg6, int arg7) + throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void addWindowToken(IBinder arg0, int arg1) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void clearForcedDisplaySize(int displayId) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void clearForcedDisplayDensity(int displayId) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void setOverscan(int displayId, int left, int top, int right, int bottom) + throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void closeSystemDialogs(String arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void startFreezingScreen(int exitAnim, int enterAnim) { + // TODO Auto-generated method stub + } + + @Override + public void stopFreezingScreen() { + // TODO Auto-generated method stub + } + + @Override + public void disableKeyguard(IBinder arg0, String arg1) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void executeAppTransition() throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void exitKeyguardSecurely(IOnKeyguardExitResult arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void freezeRotation(int arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public float getAnimationScale(int arg0) throws RemoteException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public float[] getAnimationScales() throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getAppOrientation(IApplicationToken arg0) throws RemoteException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getPendingAppTransition() throws RemoteException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public boolean inKeyguardRestrictedInputMode() throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean inputMethodClientHasFocus(IInputMethodClient arg0) throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isKeyguardLocked() throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isKeyguardSecure() throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isViewServerRunning() throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public IWindowSession openSession(IInputMethodClient arg0, IInputContext arg1) + throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void overridePendingAppTransition(String arg0, int arg1, int arg2, + IRemoteCallback startedCallback) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void overridePendingAppTransitionScaleUp(int startX, int startY, int startWidth, + int startHeight) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void overridePendingAppTransitionThumb(Bitmap srcThumb, int startX, int startY, + IRemoteCallback startedCallback, boolean scaleUp) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void pauseKeyDispatching(IBinder arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void prepareAppTransition(int arg0, boolean arg1) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void reenableKeyguard(IBinder arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void removeAppToken(IBinder arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void removeWindowToken(IBinder arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void resumeKeyDispatching(IBinder arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public Bitmap screenshotApplications(IBinder arg0, int displayId, int arg1, int arg2) + throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void setAnimationScale(int arg0, float arg1) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void setAnimationScales(float[] arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void setAppGroupId(IBinder arg0, int arg1) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void setAppOrientation(IApplicationToken arg0, int arg1) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void setAppStartingWindow(IBinder arg0, String arg1, int arg2, CompatibilityInfo arg3, + CharSequence arg4, int arg5, int arg6, int arg7, int arg8, IBinder arg9, boolean arg10) + throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void setAppVisibility(IBinder arg0, boolean arg1) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void setAppWillBeHidden(IBinder arg0) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void setEventDispatching(boolean arg0) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void setFocusedApp(IBinder arg0, boolean arg1) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void getInitialDisplaySize(int displayId, Point size) { + // TODO Auto-generated method stub + } + + @Override + public void getBaseDisplaySize(int displayId, Point size) { + // TODO Auto-generated method stub + } + + @Override + public void setForcedDisplaySize(int displayId, int arg0, int arg1) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public int getInitialDisplayDensity(int displayId) { + return -1; + } + + @Override + public int getBaseDisplayDensity(int displayId) { + return -1; + } + + @Override + public void setForcedDisplayDensity(int displayId, int density) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void setInTouchMode(boolean arg0) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void setNewConfiguration(Configuration arg0) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void updateRotation(boolean arg0, boolean arg1) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void setStrictModeVisualIndicatorPreference(String arg0) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void showStrictModeViolation(boolean arg0) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void startAppFreezingScreen(IBinder arg0, int arg1) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public boolean startViewServer(int arg0) throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void statusBarVisibilityChanged(int arg0) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void stopAppFreezingScreen(IBinder arg0, boolean arg1) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public boolean stopViewServer() throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void thawRotation() throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public Configuration updateOrientationFromAppTokens(Configuration arg0, IBinder arg1) + throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public int watchRotation(IRotationWatcher arg0) throws RemoteException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public void removeRotationWatcher(IRotationWatcher arg0) throws RemoteException { + } + + @Override + public boolean waitForWindowDrawn(IBinder token, IRemoteCallback callback) { + return false; + } + + @Override + public IBinder asBinder() { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getPreferredOptionsPanelGravity() throws RemoteException { + return Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; + } + + @Override + public void dismissKeyguard() { + } + + @Override + public void lockNow(Bundle options) { + // TODO Auto-generated method stub + } + + @Override + public boolean isSafeModeEnabled() { + return false; + } + + @Override + public void showAssistant() { + + } + + @Override + public IBinder getFocusedWindowToken() { + // TODO Auto-generated method stub + return null; + } + + @Override + public void setInputFilter(IInputFilter filter) throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public void getWindowFrame(IBinder token, Rect outFrame) { + // TODO Auto-generated method stub + } + + @Override + public void setMagnificationCallbacks(IMagnificationCallbacks callbacks) { + // TODO Auto-generated method stub + } + + @Override + public void setMagnificationSpec(MagnificationSpec spec) { + // TODO Auto-generated method stub + } + + @Override + public MagnificationSpec getCompatibleMagnificationSpecForWindow(IBinder windowToken) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isRotationFrozen() throws RemoteException { + // TODO Auto-generated method stub + return false; + } +} diff --git a/tools/layoutlib/bridge/src/android/view/LayoutInflater_Delegate.java b/tools/layoutlib/bridge/src/android/view/LayoutInflater_Delegate.java new file mode 100644 index 000000000000..3db3a1b0857b --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/LayoutInflater_Delegate.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.util.AttributeSet; +import android.util.Xml; + +import java.io.IOException; + +/** + * Delegate used to provide new implementation of a select few methods of {@link LayoutInflater} + * + * Through the layoutlib_create tool, the original methods of LayoutInflater have been replaced + * by calls to methods of the same name in this delegate class. + * + */ +public class LayoutInflater_Delegate { + + private static final String TAG_MERGE = "merge"; + + public static boolean sIsInInclude = false; + + /** + * Recursive method used to descend down the xml hierarchy and instantiate + * views, instantiate their children, and then call onFinishInflate(). + * + * This implementation just records the merge status before calling the default implementation. + */ + @LayoutlibDelegate + /*package*/ static void rInflate(LayoutInflater thisInflater, + XmlPullParser parser, View parent, final AttributeSet attrs, + boolean finishInflate) throws XmlPullParserException, IOException { + + if (finishInflate == false) { + // this is a merge rInflate! + if (thisInflater instanceof BridgeInflater) { + ((BridgeInflater) thisInflater).setIsInMerge(true); + } + } + + // ---- START DEFAULT IMPLEMENTATION. + + thisInflater.rInflate_Original(parser, parent, attrs, finishInflate); + + // ---- END DEFAULT IMPLEMENTATION. + + if (finishInflate == false) { + // this is a merge rInflate! + if (thisInflater instanceof BridgeInflater) { + ((BridgeInflater) thisInflater).setIsInMerge(false); + } + } + } + + @LayoutlibDelegate + public static void parseInclude( + LayoutInflater thisInflater, + XmlPullParser parser, View parent, AttributeSet attrs) + throws XmlPullParserException, IOException { + + int type; + + if (parent instanceof ViewGroup) { + final int layout = attrs.getAttributeResourceValue(null, "layout", 0); + if (layout == 0) { + final String value = attrs.getAttributeValue(null, "layout"); + if (value == null) { + throw new InflateException("You must specifiy a layout in the" + + " include tag: <include layout=\"@layout/layoutID\" />"); + } else { + throw new InflateException("You must specifiy a valid layout " + + "reference. The layout ID " + value + " is not valid."); + } + } else { + final XmlResourceParser childParser = + thisInflater.getContext().getResources().getLayout(layout); + + try { + final AttributeSet childAttrs = Xml.asAttributeSet(childParser); + + while ((type = childParser.next()) != XmlPullParser.START_TAG && + type != XmlPullParser.END_DOCUMENT) { + // Empty. + } + + if (type != XmlPullParser.START_TAG) { + throw new InflateException(childParser.getPositionDescription() + + ": No start tag found!"); + } + + final String childName = childParser.getName(); + + if (TAG_MERGE.equals(childName)) { + // Inflate all children. + thisInflater.rInflate(childParser, parent, childAttrs, false); + } else { + final View view = thisInflater.createViewFromTag(parent, childName, childAttrs); + final ViewGroup group = (ViewGroup) parent; + + // We try to load the layout params set in the <include /> tag. If + // they don't exist, we will rely on the layout params set in the + // included XML file. + // During a layoutparams generation, a runtime exception is thrown + // if either layout_width or layout_height is missing. We catch + // this exception and set localParams accordingly: true means we + // successfully loaded layout params from the <include /> tag, + // false means we need to rely on the included layout params. + ViewGroup.LayoutParams params = null; + try { + // ---- START CHANGES + sIsInInclude = true; + // ---- END CHANGES + + params = group.generateLayoutParams(attrs); + + } catch (RuntimeException e) { + // ---- START CHANGES + sIsInInclude = false; + // ---- END CHANGES + + params = group.generateLayoutParams(childAttrs); + } finally { + // ---- START CHANGES + sIsInInclude = false; + // ---- END CHANGES + + if (params != null) { + view.setLayoutParams(params); + } + } + + // Inflate all children. + thisInflater.rInflate(childParser, view, childAttrs, true); + + // Attempt to override the included layout's android:id with the + // one set on the <include /> tag itself. + TypedArray a = thisInflater.mContext.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.View, 0, 0); + int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID); + // While we're at it, let's try to override android:visibility. + int visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -1); + a.recycle(); + + if (id != View.NO_ID) { + view.setId(id); + } + + switch (visibility) { + case 0: + view.setVisibility(View.VISIBLE); + break; + case 1: + view.setVisibility(View.INVISIBLE); + break; + case 2: + view.setVisibility(View.GONE); + break; + } + + group.addView(view); + } + } finally { + childParser.close(); + } + } + } else { + throw new InflateException("<include /> can only be used inside of a ViewGroup"); + } + + final int currentDepth = parser.getDepth(); + while (((type = parser.next()) != XmlPullParser.END_TAG || + parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) { + // Empty + } + } + + +} 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 000000000000..6aa4b3b2eb26 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/SurfaceView.java @@ -0,0 +1,112 @@ +/* + * 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() { + + @Override + public boolean isCreating() { + return false; + } + + @Override + public void addCallback(Callback callback) { + } + + @Override + public void removeCallback(Callback callback) { + } + + @Override + public void setFixedSize(int width, int height) { + } + + @Override + public void setSizeFromLayout() { + } + + @Override + public void setFormat(int format) { + } + + @Override + public void setType(int type) { + } + + @Override + public void setKeepScreenOn(boolean screenOn) { + } + + @Override + public Canvas lockCanvas() { + return null; + } + + @Override + public Canvas lockCanvas(Rect dirty) { + return null; + } + + @Override + public void unlockCanvasAndPost(Canvas canvas) { + } + + @Override + public Surface getSurface() { + return null; + } + + @Override + public Rect getSurfaceFrame() { + return null; + } + }; +} + diff --git a/tools/layoutlib/bridge/src/android/view/ViewConfiguration_Accessor.java b/tools/layoutlib/bridge/src/android/view/ViewConfiguration_Accessor.java new file mode 100644 index 000000000000..c3533e0182c4 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/ViewConfiguration_Accessor.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +/** + * Class allowing access to package-protected methods/fields. + */ +public class ViewConfiguration_Accessor { + + public static void clearConfigurations() { + // clear the stored ViewConfiguration since the map is per density and not per context. + ViewConfiguration.sConfigurations.clear(); + } + +} diff --git a/tools/layoutlib/bridge/src/android/view/ViewRootImpl_Delegate.java b/tools/layoutlib/bridge/src/android/view/ViewRootImpl_Delegate.java new file mode 100644 index 000000000000..14b84ef5b20c --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/ViewRootImpl_Delegate.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2012 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.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate used to provide new implementation of a select few methods of {@link ViewRootImpl} + * + * Through the layoutlib_create tool, the original methods of ViewRootImpl have been replaced + * by calls to methods of the same name in this delegate class. + * + */ +public class ViewRootImpl_Delegate { + + @LayoutlibDelegate + /*package*/ static boolean isInTouchMode() { + return false; // this allows displaying selection. + } +} diff --git a/tools/layoutlib/bridge/src/android/view/View_Delegate.java b/tools/layoutlib/bridge/src/android/view/View_Delegate.java new file mode 100644 index 000000000000..8215f7c7a214 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/View_Delegate.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate used to provide new implementation of a select few methods of {@link View} + * + * Through the layoutlib_create tool, the original methods of View have been replaced + * by calls to methods of the same name in this delegate class. + * + */ +public class View_Delegate { + + @LayoutlibDelegate + /*package*/ static boolean isInEditMode(View thisView) { + return true; + } +} diff --git a/tools/layoutlib/bridge/src/android/view/WindowManagerGlobal_Delegate.java b/tools/layoutlib/bridge/src/android/view/WindowManagerGlobal_Delegate.java new file mode 100644 index 000000000000..2606e55e5dbe --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/WindowManagerGlobal_Delegate.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2012 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.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate used to provide new implementation of a select few methods of + * {@link WindowManagerGlobal} + * + * Through the layoutlib_create tool, the original methods of WindowManagerGlobal have been + * replaced by calls to methods of the same name in this delegate class. + * + */ +public class WindowManagerGlobal_Delegate { + + private static IWindowManager sService; + + @LayoutlibDelegate + public static IWindowManager getWindowManagerService() { + return sService; + } + + // ---- internal implementation stuff ---- + + public static void setWindowManagerService(IWindowManager service) { + sService = service; + } +} diff --git a/tools/layoutlib/bridge/src/android/view/accessibility/AccessibilityManager.java b/tools/layoutlib/bridge/src/android/view/accessibility/AccessibilityManager.java new file mode 100644 index 000000000000..1fd78369e5a8 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/accessibility/AccessibilityManager.java @@ -0,0 +1,137 @@ +/* + * 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 android.view.accessibility; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.Context; +import android.content.pm.ServiceInfo; +import android.view.IWindow; +import android.view.View; + +import java.util.Collections; +import java.util.List; + +/** + * System level service that serves as an event dispatch for {@link AccessibilityEvent}s. + * Such events are generated when something notable happens in the user interface, + * for example an {@link android.app.Activity} starts, the focus or selection of a + * {@link android.view.View} changes etc. Parties interested in handling accessibility + * events implement and register an accessibility service which extends + * {@link android.accessibilityservice.AccessibilityService}. + * + * @see AccessibilityEvent + * @see android.accessibilityservice.AccessibilityService + * @see android.content.Context#getSystemService + */ +public final class AccessibilityManager { + private static AccessibilityManager sInstance = new AccessibilityManager(); + + /** + * Listener for the accessibility state. + */ + public interface AccessibilityStateChangeListener { + + /** + * Called back on change in the accessibility state. + * + * @param enabled Whether accessibility is enabled. + */ + public void onAccessibilityStateChanged(boolean enabled); + } + + /** + * Get an AccessibilityManager instance (create one if necessary). + * + * @hide + */ + public static AccessibilityManager getInstance(Context context) { + return sInstance; + } + + /** + * Create an instance. + * + * @param context A {@link Context}. + */ + private AccessibilityManager() { + } + + /** + * Returns if the {@link AccessibilityManager} is enabled. + * + * @return True if this {@link AccessibilityManager} is enabled, false otherwise. + */ + public boolean isEnabled() { + return false; + } + + /** + * Sends an {@link AccessibilityEvent}. If this {@link AccessibilityManager} is not + * enabled the call is a NOOP. + * + * @param event The {@link AccessibilityEvent}. + * + * @throws IllegalStateException if a client tries to send an {@link AccessibilityEvent} + * while accessibility is not enabled. + */ + public void sendAccessibilityEvent(AccessibilityEvent event) { + } + + /** + * Requests interruption of the accessibility feedback from all accessibility services. + */ + public void interrupt() { + } + + /** + * Returns the {@link ServiceInfo}s of the installed accessibility services. + * + * @return An unmodifiable list with {@link ServiceInfo}s. + */ + public List<ServiceInfo> getAccessibilityServiceList() { + // normal implementation does this in some case, so let's do the same + // (unmodifiableList wrapped around null). + List<ServiceInfo> services = null; + return Collections.unmodifiableList(services); + } + + public List<AccessibilityServiceInfo> getInstalledAccessibilityServiceList() { + // normal implementation does this in some case, so let's do the same + // (unmodifiableList wrapped around null). + List<AccessibilityServiceInfo> services = null; + return Collections.unmodifiableList(services); + } + + public boolean addAccessibilityStateChangeListener( + AccessibilityStateChangeListener listener) { + return true; + } + + public boolean removeAccessibilityStateChangeListener( + AccessibilityStateChangeListener listener) { + return true; + } + + public int addAccessibilityInteractionConnection(IWindow windowToken, + IAccessibilityInteractionConnection connection) { + return View.NO_ID; + } + + public void removeAccessibilityInteractionConnection(IWindow windowToken) { + } + +} diff --git a/tools/layoutlib/bridge/src/android/view/inputmethod/InputMethodManager_Accessor.java b/tools/layoutlib/bridge/src/android/view/inputmethod/InputMethodManager_Accessor.java new file mode 100644 index 000000000000..dc4f9c868185 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/inputmethod/InputMethodManager_Accessor.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +/** + * Class allowing access to package-protected methods/fields. + */ +public class InputMethodManager_Accessor { + + public static void resetInstance() { + InputMethodManager.sInstance = null; + } +} diff --git a/tools/layoutlib/bridge/src/android/view/inputmethod/InputMethodManager_Delegate.java b/tools/layoutlib/bridge/src/android/view/inputmethod/InputMethodManager_Delegate.java new file mode 100644 index 000000000000..7c98847a4cf3 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/inputmethod/InputMethodManager_Delegate.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.inputmethod; + +import com.android.layoutlib.bridge.android.BridgeIInputMethodManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.content.Context; +import android.os.Looper; + + +/** + * Delegate used to provide new implementation of a select few methods of {@link InputMethodManager} + * + * Through the layoutlib_create tool, the original methods of InputMethodManager have been replaced + * by calls to methods of the same name in this delegate class. + * + */ +public class InputMethodManager_Delegate { + + // ---- Overridden methods ---- + + @LayoutlibDelegate + /*package*/ static InputMethodManager getInstance() { + synchronized (InputMethodManager.class) { + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm == null) { + imm = new InputMethodManager( + new BridgeIInputMethodManager(), Looper.getMainLooper()); + InputMethodManager.sInstance = imm; + } + return imm; + } + } +} 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 000000000000..3b6618831113 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/webkit/WebView.java @@ -0,0 +1,254 @@ +/* + * 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 View getZoomControls() { + return null; + } + + public boolean zoomIn() { + return false; + } + + public boolean zoomOut() { + return false; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/internal/policy/PolicyManager.java b/tools/layoutlib/bridge/src/com/android/internal/policy/PolicyManager.java new file mode 100644 index 000000000000..0100dc591039 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/internal/policy/PolicyManager.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2012 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.internal.policy; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.impl.RenderAction; + +import android.content.Context; +import android.view.BridgeInflater; +import android.view.FallbackEventHandler; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.Window; +import android.view.WindowManagerPolicy; + +/** + * Custom implementation of PolicyManager that does nothing to run in LayoutLib. + * + */ +public class PolicyManager { + + public static Window makeNewWindow(Context context) { + // this will likely crash somewhere beyond so we log it. + Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, + "Call to PolicyManager.makeNewWindow is not supported", null); + return null; + } + + public static LayoutInflater makeNewLayoutInflater(Context context) { + return new BridgeInflater(context, RenderAction.getCurrentContext().getProjectCallback()); + } + + public static WindowManagerPolicy makeNewWindowManager() { + // this will likely crash somewhere beyond so we log it. + Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, + "Call to PolicyManager.makeNewWindowManager is not supported", null); + return null; + } + + public static FallbackEventHandler makeNewFallbackEventHandler(Context context) { + return new FallbackEventHandler() { + @Override + public void setView(View v) { + } + + @Override + public void preDispatchKeyEvent(KeyEvent event) { + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + return false; + } + }; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/internal/textservice/ITextServicesManager_Stub_Delegate.java b/tools/layoutlib/bridge/src/com/android/internal/textservice/ITextServicesManager_Stub_Delegate.java new file mode 100644 index 000000000000..3017292d8337 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/internal/textservice/ITextServicesManager_Stub_Delegate.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.textservice; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.view.textservice.SpellCheckerInfo; +import android.view.textservice.SpellCheckerSubtype; + + +/** + * Delegate used to provide new implementation of a select few methods of + * {@link ITextServicesManager$Stub} + * + * Through the layoutlib_create tool, the original methods of Stub have been replaced + * by calls to methods of the same name in this delegate class. + * + */ +public class ITextServicesManager_Stub_Delegate { + + @LayoutlibDelegate + public static ITextServicesManager asInterface(IBinder obj) { + // ignore the obj and return a fake interface implementation + return new FakeTextServicesManager(); + } + + private static class FakeTextServicesManager implements ITextServicesManager { + + @Override + public void finishSpellCheckerService(ISpellCheckerSessionListener arg0) + throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public SpellCheckerInfo getCurrentSpellChecker(String arg0) throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public SpellCheckerSubtype getCurrentSpellCheckerSubtype(String arg0, boolean arg1) + throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public SpellCheckerInfo[] getEnabledSpellCheckers() throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void getSpellCheckerService(String arg0, String arg1, + ITextServicesSessionListener arg2, ISpellCheckerSessionListener arg3, Bundle arg4) + throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public boolean isSpellCheckerEnabled() throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void setCurrentSpellChecker(String arg0, String arg1) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void setCurrentSpellCheckerSubtype(String arg0, int arg1) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void setSpellCheckerEnabled(boolean arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public IBinder asBinder() { + // TODO Auto-generated method stub + return null; + } + + } + } diff --git a/tools/layoutlib/bridge/src/com/android/internal/util/XmlUtils_Delegate.java b/tools/layoutlib/bridge/src/com/android/internal/util/XmlUtils_Delegate.java new file mode 100644 index 000000000000..bf998b8737cd --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/internal/util/XmlUtils_Delegate.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + + +/** + * Delegate used to provide new implementation of a select few methods of {@link XmlUtils} + * + * Through the layoutlib_create tool, the original methods of XmlUtils have been replaced + * by calls to methods of the same name in this delegate class. + * + */ +public class XmlUtils_Delegate { + + @LayoutlibDelegate + /*package*/ static final int convertValueToInt(CharSequence charSeq, int defaultValue) { + if (null == charSeq) + return defaultValue; + + String nm = charSeq.toString(); + + // This code is copied from the original implementation. The issue is that + // The Dalvik libraries are able to handle Integer.parse("XXXXXXXX", 16) where XXXXXXX + // is > 80000000 but the Java VM cannot. + + int sign = 1; + int index = 0; + int len = nm.length(); + int base = 10; + + if ('-' == nm.charAt(0)) { + sign = -1; + index++; + } + + if ('0' == nm.charAt(index)) { + // Quick check for a zero by itself + if (index == (len - 1)) + return 0; + + char c = nm.charAt(index + 1); + + if ('x' == c || 'X' == c) { + index += 2; + base = 16; + } else { + index++; + base = 8; + } + } + else if ('#' == nm.charAt(index)) { + index++; + base = 16; + } + + return ((int)Long.parseLong(nm.substring(index), base)) * sign; + } +} 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 000000000000..42257c560a7f --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java @@ -0,0 +1,621 @@ +/* + * 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 static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; +import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; + +import com.android.ide.common.rendering.api.Capability; +import com.android.ide.common.rendering.api.DrawableParams; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.rendering.api.RenderSession; +import com.android.ide.common.rendering.api.Result; +import com.android.ide.common.rendering.api.Result.Status; +import com.android.ide.common.rendering.api.SessionParams; +import com.android.layoutlib.bridge.impl.FontLoader; +import com.android.layoutlib.bridge.impl.RenderDrawable; +import com.android.layoutlib.bridge.impl.RenderSessionImpl; +import com.android.layoutlib.bridge.util.DynamicIdMap; +import com.android.ninepatch.NinePatchChunk; +import com.android.resources.ResourceType; +import com.android.tools.layoutlib.create.MethodAdapter; +import com.android.tools.layoutlib.create.OverrideMethod; +import com.android.util.Pair; + +import android.content.res.BridgeAssetManager; +import android.graphics.Bitmap; +import android.graphics.Typeface_Accessor; +import android.graphics.Typeface_Delegate; +import android.os.Looper; +import android.os.Looper_Accessor; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import java.io.File; +import java.lang.ref.SoftReference; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Main entry point of the LayoutLib Bridge. + * <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call + * {@link #createScene(SceneParams)} + */ +public final class Bridge extends com.android.ide.common.rendering.api.Bridge { + + public static class StaticMethodNotImplementedException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public StaticMethodNotImplementedException(String msg) { + super(msg); + } + } + + /** + * Lock to ensure only one rendering/inflating happens at a time. + * This is due to some singleton in the Android framework. + */ + private final static ReentrantLock sLock = new ReentrantLock(); + + /** + * Maps from id to resource type/name. This is for com.android.internal.R + */ + private final static Map<Integer, Pair<ResourceType, String>> sRMap = + new HashMap<Integer, Pair<ResourceType, String>>(); + + /** + * Same as sRMap except for int[] instead of int resources. This is for android.R only. + */ + private final static Map<IntArray, String> sRArrayMap = new HashMap<IntArray, String>(); + /** + * Reverse map compared to sRMap, resource type -> (resource name -> id). + * This is for com.android.internal.R. + */ + private final static Map<ResourceType, Map<String, Integer>> sRevRMap = + new EnumMap<ResourceType, Map<String,Integer>>(ResourceType.class); + + // framework resources are defined as 0x01XX#### where XX is the resource type (layout, + // drawable, etc...). Using FF as the type allows for 255 resource types before we get a + // collision which should be fine. + private final static int DYNAMIC_ID_SEED_START = 0x01ff0000; + private final static DynamicIdMap sDynamicIds = new DynamicIdMap(DYNAMIC_ID_SEED_START); + + private final static Map<Object, Map<String, SoftReference<Bitmap>>> sProjectBitmapCache = + new HashMap<Object, Map<String, SoftReference<Bitmap>>>(); + private final static Map<Object, Map<String, SoftReference<NinePatchChunk>>> sProject9PatchCache = + new HashMap<Object, Map<String, SoftReference<NinePatchChunk>>>(); + + private final static Map<String, SoftReference<Bitmap>> sFrameworkBitmapCache = + new HashMap<String, SoftReference<Bitmap>>(); + private final static Map<String, SoftReference<NinePatchChunk>> sFramework9PatchCache = + new HashMap<String, SoftReference<NinePatchChunk>>(); + + private static Map<String, Map<String, Integer>> sEnumValueMap; + private static Map<String, String> sPlatformProperties; + + /** + * int[] wrapper to use as keys in maps. + */ + private final static class IntArray { + private int[] mArray; + + private IntArray() { + // do nothing + } + + private IntArray(int[] a) { + mArray = a; + } + + private void set(int[] a) { + mArray = a; + } + + @Override + public int hashCode() { + return Arrays.hashCode(mArray); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + + IntArray other = (IntArray) obj; + if (!Arrays.equals(mArray, other.mArray)) return false; + return true; + } + } + + /** Instance of IntArrayWrapper to be reused in {@link #resolveResourceId(int[])}. */ + private final static IntArray sIntArrayWrapper = new IntArray(); + + /** + * A default log than prints to stdout/stderr. + */ + private final static LayoutLog sDefaultLog = new LayoutLog() { + @Override + public void error(String tag, String message, Object data) { + System.err.println(message); + } + + @Override + public void error(String tag, String message, Throwable throwable, Object data) { + System.err.println(message); + } + + @Override + public void warning(String tag, String message, Object data) { + System.out.println(message); + } + }; + + /** + * Current log. + */ + private static LayoutLog sCurrentLog = sDefaultLog; + + private EnumSet<Capability> mCapabilities; + + @Override + public int getApiLevel() { + return com.android.ide.common.rendering.api.Bridge.API_CURRENT; + } + + @Override + public EnumSet<Capability> getCapabilities() { + return mCapabilities; + } + + @Override + public boolean init(Map<String,String> platformProperties, + File fontLocation, + Map<String, Map<String, Integer>> enumValueMap, + LayoutLog log) { + sPlatformProperties = platformProperties; + sEnumValueMap = enumValueMap; + + // don't use EnumSet.allOf(), because the bridge doesn't come with its specific version + // of layoutlib_api. It is provided by the client which could have a more recent version + // with newer, unsupported capabilities. + mCapabilities = EnumSet.of( + Capability.UNBOUND_RENDERING, + Capability.CUSTOM_BACKGROUND_COLOR, + Capability.RENDER, + Capability.LAYOUT_ONLY, + Capability.EMBEDDED_LAYOUT, + Capability.VIEW_MANIPULATION, + Capability.PLAY_ANIMATION, + Capability.ANIMATED_VIEW_MANIPULATION, + Capability.ADAPTER_BINDING, + Capability.EXTENDED_VIEWINFO, + Capability.FIXED_SCALABLE_NINE_PATCH); + + + BridgeAssetManager.initSystem(); + + // 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) { + sDefaultLog.error(null, "Missing Stub: " + signature + + (isNative ? " (native)" : ""), null /*data*/); + + 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); + } + } + }); + } + + // load the fonts. + FontLoader fontLoader = FontLoader.create(fontLocation.getAbsolutePath()); + if (fontLoader != null) { + Typeface_Delegate.init(fontLoader); + } else { + log.error(LayoutLog.TAG_BROKEN, + "Failed create FontLoader in layout lib.", null); + return false; + } + + // 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 { + Class<?> r = com.android.internal.R.class; + + for (Class<?> inner : r.getDeclaredClasses()) { + String resTypeName = inner.getSimpleName(); + ResourceType resType = ResourceType.getEnum(resTypeName); + if (resType != null) { + Map<String, Integer> fullMap = new HashMap<String, Integer>(); + sRevRMap.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 using an IntArray + // wrapper that properly implements equals and hashcode for the array + // objects, as required by the map contract. + sRArrayMap.put(new IntArray((int[]) f.get(null)), f.getName()); + } else if (type == int.class) { + Integer value = (Integer) f.get(null); + sRMap.put(value, Pair.of(resType, f.getName())); + fullMap.put(f.getName(), value); + } else { + assert false; + } + } + } + } + } + } catch (Throwable throwable) { + if (log != null) { + log.error(LayoutLog.TAG_BROKEN, + "Failed to load com.android.internal.R from the layout library jar", + throwable); + } + return false; + } + + return true; + } + + @Override + public boolean dispose() { + BridgeAssetManager.clearSystem(); + + // dispose of the default typeface. + Typeface_Accessor.resetDefaults(); + + return true; + } + + /** + * Starts a layout session by inflating and rendering it. The method returns a + * {@link RenderSession} on which further actions can be taken. + * + * @param params the {@link SessionParams} object with all the information necessary to create + * the scene. + * @return a new {@link RenderSession} object that contains the result of the layout. + * @since 5 + */ + @Override + public RenderSession createSession(SessionParams params) { + try { + Result lastResult = SUCCESS.createResult(); + RenderSessionImpl scene = new RenderSessionImpl(params); + try { + prepareThread(); + lastResult = scene.init(params.getTimeout()); + if (lastResult.isSuccess()) { + lastResult = scene.inflate(); + if (lastResult.isSuccess()) { + lastResult = scene.render(true /*freshRender*/); + } + } + } finally { + scene.release(); + cleanupThread(); + } + + return new BridgeRenderSession(scene, lastResult); + } catch (Throwable t) { + // get the real cause of the exception. + Throwable t2 = t; + while (t2.getCause() != null) { + t2 = t.getCause(); + } + return new BridgeRenderSession(null, + ERROR_UNKNOWN.createResult(t2.getMessage(), t)); + } + } + + @Override + public Result renderDrawable(DrawableParams params) { + try { + Result lastResult = SUCCESS.createResult(); + RenderDrawable action = new RenderDrawable(params); + try { + prepareThread(); + lastResult = action.init(params.getTimeout()); + if (lastResult.isSuccess()) { + lastResult = action.render(); + } + } finally { + action.release(); + cleanupThread(); + } + + return lastResult; + } catch (Throwable t) { + // get the real cause of the exception. + Throwable t2 = t; + while (t2.getCause() != null) { + t2 = t.getCause(); + } + return ERROR_UNKNOWN.createResult(t2.getMessage(), t); + } + } + + @Override + public void clearCaches(Object projectKey) { + if (projectKey != null) { + sProjectBitmapCache.remove(projectKey); + sProject9PatchCache.remove(projectKey); + } + } + + @Override + public Result getViewParent(Object viewObject) { + if (viewObject instanceof View) { + return Status.SUCCESS.createResult(((View)viewObject).getParent()); + } + + throw new IllegalArgumentException("viewObject is not a View"); + } + + @Override + public Result getViewIndex(Object viewObject) { + if (viewObject instanceof View) { + View view = (View) viewObject; + ViewParent parentView = view.getParent(); + + if (parentView instanceof ViewGroup) { + Status.SUCCESS.createResult(((ViewGroup) parentView).indexOfChild(view)); + } + + return Status.SUCCESS.createResult(); + } + + throw new IllegalArgumentException("viewObject is not a View"); + } + + /** + * Returns the lock for the bridge + */ + public static ReentrantLock getLock() { + return sLock; + } + + /** + * Prepares the current thread for rendering. + * + * Note that while this can be called several time, the first call to {@link #cleanupThread()} + * will do the clean-up, and make the thread unable to do further scene actions. + */ + public static void prepareThread() { + // 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.prepareMainLooper(); + } + } + + /** + * Cleans up thread-specific data. After this, the thread cannot be used for scene actions. + * <p> + * Note that it doesn't matter how many times {@link #prepareThread()} was called, a single + * call to this will prevent the thread from doing further scene actions + */ + public static void cleanupThread() { + // clean up the looper + Looper_Accessor.cleanupThread(); + } + + public static LayoutLog getLog() { + return sCurrentLog; + } + + public static void setLog(LayoutLog log) { + // check only the thread currently owning the lock can do this. + if (sLock.isHeldByCurrentThread() == false) { + throw new IllegalStateException("scene must be acquired first. see #acquire(long)"); + } + + if (log != null) { + sCurrentLog = log; + } else { + sCurrentLog = sDefaultLog; + } + } + + /** + * Returns details of a framework resource from its integer value. + * @param value the integer value + * @return a Pair containing the resource type and name, or null if the id + * does not match any resource. + */ + public static Pair<ResourceType, String> resolveResourceId(int value) { + Pair<ResourceType, String> pair = sRMap.get(value); + if (pair == null) { + pair = sDynamicIds.resolveId(value); + if (pair == null) { + //System.out.println(String.format("Missing id: %1$08X (%1$d)", value)); + } + } + return pair; + } + + /** + * Returns the name of a framework resource whose value is an int array. + * @param array + */ + public static String resolveResourceId(int[] array) { + sIntArrayWrapper.set(array); + return sRArrayMap.get(sIntArrayWrapper); + } + + /** + * 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 getResourceId(ResourceType type, String name) { + Map<String, Integer> map = sRevRMap.get(type); + Integer value = null; + if (map != null) { + value = map.get(name); + } + + if (value == null) { + value = sDynamicIds.getId(type, name); + } + + return value; + } + + /** + * Returns the list of possible enums for a given attribute name. + */ + public static Map<String, Integer> getEnumValues(String attributeName) { + if (sEnumValueMap != null) { + return sEnumValueMap.get(attributeName); + } + + return null; + } + + /** + * Returns the platform build properties. + */ + public static Map<String, String> getPlatformProperties() { + return sPlatformProperties; + } + + /** + * 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. + */ + public static Bitmap getCachedBitmap(String value, Object projectKey) { + if (projectKey != null) { + Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey); + if (map != null) { + SoftReference<Bitmap> ref = map.get(value); + if (ref != null) { + return ref.get(); + } + } + } else { + SoftReference<Bitmap> ref = sFrameworkBitmapCache.get(value); + if (ref != null) { + return ref.get(); + } + } + + return null; + } + + /** + * 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. + */ + public static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) { + if (projectKey != null) { + Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey); + + if (map == null) { + map = new HashMap<String, SoftReference<Bitmap>>(); + sProjectBitmapCache.put(projectKey, map); + } + + map.put(value, new SoftReference<Bitmap>(bmp)); + } else { + sFrameworkBitmapCache.put(value, new SoftReference<Bitmap>(bmp)); + } + } + + /** + * Returns the 9 patch chunk 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. + */ + public static NinePatchChunk getCached9Patch(String value, Object projectKey) { + if (projectKey != null) { + Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey); + + if (map != null) { + SoftReference<NinePatchChunk> ref = map.get(value); + if (ref != null) { + return ref.get(); + } + } + } else { + SoftReference<NinePatchChunk> ref = sFramework9PatchCache.get(value); + if (ref != null) { + return ref.get(); + } + } + + return null; + } + + /** + * Sets a 9 patch chunk 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. + */ + public static void setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey) { + if (projectKey != null) { + Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey); + + if (map == null) { + map = new HashMap<String, SoftReference<NinePatchChunk>>(); + sProject9PatchCache.put(projectKey, map); + } + + map.put(value, new SoftReference<NinePatchChunk>(ninePatch)); + } else { + sFramework9PatchCache.put(value, new SoftReference<NinePatchChunk>(ninePatch)); + } + } +} 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 000000000000..eb9e7f1dafb3 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeConstants.java @@ -0,0 +1,51 @@ +/* + * 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"; + + /** App auto namespace */ + public final static String NS_APP_RES_AUTO = "http://schemas.android.com/apk/res-auto"; + + public final static String R = "com.android.internal.R"; + + + public final static String MATCH_PARENT = "match_parent"; + 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/BridgeRenderSession.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java new file mode 100644 index 000000000000..f9f4b3a86b3e --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge; + +import com.android.ide.common.rendering.api.IAnimationListener; +import com.android.ide.common.rendering.api.ILayoutPullParser; +import com.android.ide.common.rendering.api.RenderParams; +import com.android.ide.common.rendering.api.RenderSession; +import com.android.ide.common.rendering.api.Result; +import com.android.ide.common.rendering.api.ViewInfo; +import com.android.layoutlib.bridge.impl.RenderSessionImpl; + +import android.view.View; +import android.view.ViewGroup; + +import java.awt.image.BufferedImage; +import java.util.List; +import java.util.Map; + +/** + * An implementation of {@link RenderSession}. + * + * This is a pretty basic class that does almost nothing. All of the work is done in + * {@link RenderSessionImpl}. + * + */ +public class BridgeRenderSession extends RenderSession { + + private final RenderSessionImpl mSession; + private Result mLastResult; + + @Override + public Result getResult() { + return mLastResult; + } + + @Override + public BufferedImage getImage() { + return mSession.getImage(); + } + + @Override + public boolean isAlphaChannelImage() { + return mSession.isAlphaChannelImage(); + } + + @Override + public List<ViewInfo> getRootViews() { + return mSession.getViewInfos(); + } + + @Override + public Map<String, String> getDefaultProperties(Object viewObject) { + return mSession.getDefaultProperties(viewObject); + } + + @Override + public Result getProperty(Object objectView, String propertyName) { + // pass + return super.getProperty(objectView, propertyName); + } + + @Override + public Result setProperty(Object objectView, String propertyName, String propertyValue) { + // pass + return super.setProperty(objectView, propertyName, propertyValue); + } + + @Override + public Result render(long timeout) { + try { + Bridge.prepareThread(); + mLastResult = mSession.acquire(timeout); + if (mLastResult.isSuccess()) { + mLastResult = mSession.render(false /*freshRender*/); + } + } finally { + mSession.release(); + Bridge.cleanupThread(); + } + + return mLastResult; + } + + @Override + public Result animate(Object targetObject, String animationName, + boolean isFrameworkAnimation, IAnimationListener listener) { + try { + Bridge.prepareThread(); + mLastResult = mSession.acquire(RenderParams.DEFAULT_TIMEOUT); + if (mLastResult.isSuccess()) { + mLastResult = mSession.animate(targetObject, animationName, isFrameworkAnimation, + listener); + } + } finally { + mSession.release(); + Bridge.cleanupThread(); + } + + return mLastResult; + } + + @Override + public Result insertChild(Object parentView, ILayoutPullParser childXml, int index, + IAnimationListener listener) { + if (parentView instanceof ViewGroup == false) { + throw new IllegalArgumentException("parentView is not a ViewGroup"); + } + + try { + Bridge.prepareThread(); + mLastResult = mSession.acquire(RenderParams.DEFAULT_TIMEOUT); + if (mLastResult.isSuccess()) { + mLastResult = mSession.insertChild((ViewGroup) parentView, childXml, index, + listener); + } + } finally { + mSession.release(); + Bridge.cleanupThread(); + } + + return mLastResult; + } + + + @Override + public Result moveChild(Object parentView, Object childView, int index, + Map<String, String> layoutParams, IAnimationListener listener) { + if (parentView instanceof ViewGroup == false) { + throw new IllegalArgumentException("parentView is not a ViewGroup"); + } + if (childView instanceof View == false) { + throw new IllegalArgumentException("childView is not a View"); + } + + try { + Bridge.prepareThread(); + mLastResult = mSession.acquire(RenderParams.DEFAULT_TIMEOUT); + if (mLastResult.isSuccess()) { + mLastResult = mSession.moveChild((ViewGroup) parentView, (View) childView, index, + layoutParams, listener); + } + } finally { + mSession.release(); + Bridge.cleanupThread(); + } + + return mLastResult; + } + + @Override + public Result removeChild(Object childView, IAnimationListener listener) { + if (childView instanceof View == false) { + throw new IllegalArgumentException("childView is not a View"); + } + + try { + Bridge.prepareThread(); + mLastResult = mSession.acquire(RenderParams.DEFAULT_TIMEOUT); + if (mLastResult.isSuccess()) { + mLastResult = mSession.removeChild((View) childView, listener); + } + } finally { + mSession.release(); + Bridge.cleanupThread(); + } + + return mLastResult; + } + + @Override + public void dispose() { + } + + /*package*/ BridgeRenderSession(RenderSessionImpl scene, Result lastResult) { + mSession = scene; + if (scene != null) { + mSession.setScene(this); + } + mLastResult = lastResult; + } +} 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 000000000000..3d50b2a8c0e7 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/MockView.java @@ -0,0 +1,47 @@ +/* + * 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.view.Gravity; +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); + setGravity(Gravity.CENTER); + } + + @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/android/BridgeContentProvider.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentProvider.java new file mode 100644 index 000000000000..688cc87f20a3 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentProvider.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.android; + +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentValues; +import android.content.IContentProvider; +import android.content.OperationApplicationException; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.ICancellationSignal; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; + +import java.io.FileNotFoundException; +import java.util.ArrayList; + +/** + * Mock implementation of {@link IContentProvider}. + * + * TODO: never return null when the method is not supposed to. Return fake data instead. + */ +public final class BridgeContentProvider implements IContentProvider { + @Override + public ContentProviderResult[] applyBatch(String callingPackage, + ArrayList<ContentProviderOperation> arg0) + throws RemoteException, OperationApplicationException { + // TODO Auto-generated method stub + return null; + } + + @Override + public int bulkInsert(String callingPackage, Uri arg0, ContentValues[] arg1) + throws RemoteException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public Bundle call(String callingPackage, String arg0, String arg1, Bundle arg2) + throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public int delete(String callingPackage, Uri arg0, String arg1, String[] arg2) + throws RemoteException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public String getType(Uri arg0) throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Uri insert(String callingPackage, Uri arg0, ContentValues arg1) throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public AssetFileDescriptor openAssetFile( + String callingPackage, Uri arg0, String arg1, ICancellationSignal signal) + throws RemoteException, FileNotFoundException { + // TODO Auto-generated method stub + return null; + } + + @Override + public ParcelFileDescriptor openFile( + String callingPackage, Uri arg0, String arg1, ICancellationSignal signal) + throws RemoteException, FileNotFoundException { + // TODO Auto-generated method stub + return null; + } + + @Override + public Cursor query(String callingPackage, Uri arg0, String[] arg1, String arg2, String[] arg3, + String arg4, ICancellationSignal arg5) throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public int update(String callingPackage, Uri arg0, ContentValues arg1, String arg2, + String[] arg3) throws RemoteException { + // TODO Auto-generated method stub + return 0; + } + + @Override + public IBinder asBinder() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String[] getStreamTypes(Uri arg0, String arg1) throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public AssetFileDescriptor openTypedAssetFile(String callingPackage, Uri arg0, String arg1, + Bundle arg2, ICancellationSignal signal) throws RemoteException, FileNotFoundException { + // TODO Auto-generated method stub + return null; + } + + @Override + public ICancellationSignal createCancellationSignal() throws RemoteException { + // TODO Auto-generated method stub + return null; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentResolver.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentResolver.java new file mode 100644 index 000000000000..8d259d70121e --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentResolver.java @@ -0,0 +1,120 @@ +/* + * 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.android; + +import android.content.ContentResolver; +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 { + + private BridgeContentProvider mProvider = null; + + public BridgeContentResolver(Context context) { + super(context); + } + + @Override + public IContentProvider acquireProvider(Context c, String name) { + if (mProvider == null) { + mProvider = new BridgeContentProvider(); + } + + return mProvider; + } + + @Override + public IContentProvider acquireExistingProvider(Context c, String name) { + if (mProvider == null) { + mProvider = new BridgeContentProvider(); + } + + return mProvider; + } + + @Override + public boolean releaseProvider(IContentProvider icp) { + // ignore + return false; + } + + @Override + protected IContentProvider acquireUnstableProvider(Context c, String name) { + return acquireProvider(c, name); + } + + @Override + public boolean releaseUnstableProvider(IContentProvider icp) { + return releaseProvider(icp); + } + + /** @hide */ + @Override + public void unstableProviderDied(IContentProvider icp) { + } + + /** + * Stub for the layoutlib bridge content resolver. + */ + @Override + public void registerContentObserver(Uri uri, boolean notifyForDescendents, + ContentObserver observer) { + // pass + } + + /** + * Stub for the layoutlib bridge content resolver. + */ + @Override + public void unregisterContentObserver(ContentObserver observer) { + // pass + } + + /** + * Stub for the layoutlib bridge content resolver. + */ + @Override + public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { + // pass + } + + /** + * Stub for the layoutlib bridge content resolver. + */ + @Override + public void startSync(Uri uri, Bundle extras) { + // pass + } + + /** + * Stub for the layoutlib bridge content resolver. + */ + @Override + public void cancelSync(Uri uri) { + // pass + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java new file mode 100644 index 000000000000..d63dcac1f218 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -0,0 +1,1427 @@ +/* + * 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.android; + +import com.android.ide.common.rendering.api.ILayoutPullParser; +import com.android.ide.common.rendering.api.IProjectCallback; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.ResourceReference; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.StyleResourceValue; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.BridgeConstants; +import com.android.layoutlib.bridge.android.view.WindowManagerImpl; +import com.android.layoutlib.bridge.impl.ParserFactory; +import com.android.layoutlib.bridge.impl.Stack; +import com.android.resources.ResourceType; +import com.android.util.Pair; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +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.IntentSender; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.content.res.BridgeResources; +import android.content.res.BridgeTypedArray; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.database.DatabaseErrorHandler; +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.os.PowerManager; +import android.os.UserHandle; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.BridgeInflater; +import android.view.Display; +import android.view.DisplayAdjustments; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.textservice.TextServicesManager; + +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.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +/** + * Custom implementation of Context/Activity to handle non compiled resources. + */ +public final class BridgeContext extends Context { + + private Resources mSystemResources; + private final HashMap<View, Object> mViewKeyMap = new HashMap<View, Object>(); + private final Object mProjectKey; + private final DisplayMetrics mMetrics; + private final RenderResources mRenderResources; + private final Configuration mConfig; + private final ApplicationInfo mApplicationInfo; + private final IProjectCallback mProjectCallback; + private final WindowManager mWindowManager; + + private Resources.Theme mTheme; + + private final Map<Object, Map<String, String>> mDefaultPropMaps = + new IdentityHashMap<Object, Map<String,String>>(); + + // maps for dynamically generated id representing style objects (StyleResourceValue) + private Map<Integer, StyleResourceValue> mDynamicIdToStyleMap; + private Map<StyleResourceValue, 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 mBridgeInflater; + + private BridgeContentResolver mContentResolver; + + private final Stack<BridgeXmlBlockParser> mParserStack = new Stack<BridgeXmlBlockParser>(); + + /** + * @param projectKey An Object identifying the project. This is used for the cache mechanism. + * @param metrics the {@link DisplayMetrics}. + * @param renderResources the configured resources (both framework and projects) for this + * render. + * @param projectCallback + * @param config the Configuration object for this render. + * @param targetSdkVersion the targetSdkVersion of the application. + */ + public BridgeContext(Object projectKey, DisplayMetrics metrics, + RenderResources renderResources, + IProjectCallback projectCallback, + Configuration config, + int targetSdkVersion) { + mProjectKey = projectKey; + mMetrics = metrics; + mProjectCallback = projectCallback; + + mRenderResources = renderResources; + mConfig = config; + + mApplicationInfo = new ApplicationInfo(); + mApplicationInfo.targetSdkVersion = targetSdkVersion; + + mWindowManager = new WindowManagerImpl(mMetrics); + } + + /** + * Initializes the {@link Resources} singleton to be linked to this {@link Context}, its + * {@link DisplayMetrics}, {@link Configuration}, and {@link IProjectCallback}. + * + * @see #disposeResources() + */ + public void initResources() { + AssetManager assetManager = AssetManager.getSystem(); + + mSystemResources = BridgeResources.initSystem( + this, + assetManager, + mMetrics, + mConfig, + mProjectCallback); + mTheme = mSystemResources.newTheme(); + } + + /** + * Disposes the {@link Resources} singleton. + */ + public void disposeResources() { + BridgeResources.disposeSystem(); + } + + public void setBridgeInflater(BridgeInflater inflater) { + mBridgeInflater = 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 DisplayMetrics getMetrics() { + return mMetrics; + } + + public IProjectCallback getProjectCallback() { + return mProjectCallback; + } + + public RenderResources getRenderResources() { + return mRenderResources; + } + + public Map<String, String> getDefaultPropMap(Object key) { + return mDefaultPropMaps.get(key); + } + + public Configuration getConfiguration() { + return mConfig; + } + + /** + * Adds a parser to the stack. + * @param parser the parser to add. + */ + public void pushParser(BridgeXmlBlockParser parser) { + if (ParserFactory.LOG_PARSER) { + System.out.println("PUSH " + parser.getParser().toString()); + } + mParserStack.push(parser); + } + + /** + * Removes the parser at the top of the stack + */ + public void popParser() { + BridgeXmlBlockParser parser = mParserStack.pop(); + if (ParserFactory.LOG_PARSER) { + System.out.println("POPD " + parser.getParser().toString()); + } + } + + /** + * Returns the current parser at the top the of the stack. + * @return a parser or null. + */ + public BridgeXmlBlockParser getCurrentParser() { + return mParserStack.peek(); + } + + /** + * Returns the previous parser. + * @return a parser or null if there isn't any previous parser + */ + public BridgeXmlBlockParser getPreviousParser() { + if (mParserStack.size() < 2) { + return null; + } + return mParserStack.get(mParserStack.size() - 2); + } + + public boolean resolveThemeAttribute(int resid, TypedValue outValue, boolean resolveRefs) { + Pair<ResourceType, String> resourceInfo = Bridge.resolveResourceId(resid); + boolean isFrameworkRes = true; + if (resourceInfo == null) { + resourceInfo = mProjectCallback.resolveResourceId(resid); + isFrameworkRes = false; + } + + if (resourceInfo == null) { + return false; + } + + ResourceValue value = mRenderResources.findItemInTheme(resourceInfo.getSecond(), + isFrameworkRes); + if (resolveRefs) { + value = mRenderResources.resolveResValue(value); + } + + // check if this is a style resource + if (value instanceof StyleResourceValue) { + // get the id that will represent this style. + outValue.resourceId = getDynamicIdByStyle((StyleResourceValue)value); + return true; + } + + + int a; + // if this is a framework value. + if (value.isFramework()) { + // look for idName in the android R classes. + // use 0 a default res value as it's not a valid id value. + a = getFrameworkResourceValue(value.getResourceType(), value.getName(), 0 /*defValue*/); + } else { + // look for idName in the project R class. + // use 0 a default res value as it's not a valid id value. + a = getProjectResourceValue(value.getResourceType(), value.getName(), 0 /*defValue*/); + } + + if (a != 0) { + outValue.resourceId = a; + return true; + } + + return false; + } + + + public ResourceReference resolveId(int id) { + // first get the String related to this id in the framework + Pair<ResourceType, String> resourceInfo = Bridge.resolveResourceId(id); + + if (resourceInfo != null) { + return new ResourceReference(resourceInfo.getSecond(), true); + } + + // didn't find a match in the framework? look in the project. + if (mProjectCallback != null) { + resourceInfo = mProjectCallback.resolveResourceId(id); + + if (resourceInfo != null) { + return new ResourceReference(resourceInfo.getSecond(), false); + } + } + + return null; + } + + public Pair<View, Boolean> inflateView(ResourceReference resource, ViewGroup parent, + boolean attachToRoot, boolean skipCallbackParser) { + boolean isPlatformLayout = resource.isFramework(); + + if (isPlatformLayout == false && skipCallbackParser == false) { + // check if the project callback can provide us with a custom parser. + ILayoutPullParser parser = getParser(resource); + + if (parser != null) { + BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(parser, + this, resource.isFramework()); + try { + pushParser(blockParser); + return Pair.of( + mBridgeInflater.inflate(blockParser, parent, attachToRoot), + true); + } finally { + popParser(); + } + } + } + + ResourceValue resValue; + if (resource instanceof ResourceValue) { + resValue = (ResourceValue) resource; + } else { + if (isPlatformLayout) { + resValue = mRenderResources.getFrameworkResource(ResourceType.LAYOUT, + resource.getName()); + } else { + resValue = mRenderResources.getProjectResource(ResourceType.LAYOUT, + resource.getName()); + } + } + + if (resValue != null) { + + File xml = new File(resValue.getValue()); + if (xml.isFile()) { + // we need to create a pull parser around the layout XML file, and then + // give that to our XmlBlockParser + try { + XmlPullParser parser = ParserFactory.create(xml); + + // set the resource ref to have correct view cookies + mBridgeInflater.setResourceReference(resource); + + BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser(parser, + this, resource.isFramework()); + try { + pushParser(blockParser); + return Pair.of( + mBridgeInflater.inflate(blockParser, parent, attachToRoot), + false); + } finally { + popParser(); + } + } catch (XmlPullParserException e) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + "Failed to configure parser for " + xml, e, null /*data*/); + // we'll return null below. + } catch (FileNotFoundException e) { + // this shouldn't happen since we check above. + } finally { + mBridgeInflater.setResourceReference(null); + } + } else { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + String.format("File %s is missing!", xml), null); + } + } else { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + String.format("Layout %s%s does not exist.", isPlatformLayout ? "android:" : "", + resource.getName()), null); + } + + return Pair.of(null, false); + } + + @SuppressWarnings("deprecation") + private ILayoutPullParser getParser(ResourceReference resource) { + ILayoutPullParser parser; + if (resource instanceof ResourceValue) { + parser = mProjectCallback.getParser((ResourceValue) resource); + } else { + parser = mProjectCallback.getParser(resource.getName()); + } + return parser; + } + + // ------------ Context methods + + @Override + public Resources getResources() { + return mSystemResources; + } + + @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 mBridgeInflater; + } + + if (TEXT_SERVICES_MANAGER_SERVICE.equals(service)) { + // we need to return a valid service to avoid NPE + return TextServicesManager.getInstance(); + } + + if (WINDOW_SERVICE.equals(service)) { + return mWindowManager; + } + + // needed by SearchView + if (INPUT_METHOD_SERVICE.equals(service)) { + return null; + } + + if (POWER_SERVICE.equals(service)) { + return new PowerManager(this, new BridgePowerManager(), new Handler()); + } + + throw new UnsupportedOperationException("Unsupported Service: " + service); + } + + + @Override + public final TypedArray obtainStyledAttributes(int[] attrs) { + return createStyleBasedTypedArray(mRenderResources.getCurrentTheme(), attrs); + } + + @Override + public final TypedArray obtainStyledAttributes(int resid, int[] attrs) + throws Resources.NotFoundException { + // get the StyleResourceValue based on the resId; + StyleResourceValue 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) { + + Map<String, String> defaultPropMap = null; + boolean isPlatformFile = true; + + // Hint: for XmlPullParser, attach source //DEVICE_SRC/dalvik/libcore/xml/src/java + if (set instanceof BridgeXmlBlockParser) { + BridgeXmlBlockParser parser = null; + parser = (BridgeXmlBlockParser)set; + + isPlatformFile = parser.isPlatformFile(); + + Object key = parser.getViewCookie(); + if (key != null) { + defaultPropMap = mDefaultPropMaps.get(key); + if (defaultPropMap == null) { + defaultPropMap = new HashMap<String, String>(); + mDefaultPropMaps.put(key, defaultPropMap); + } + } + + } else if (set instanceof BridgeLayoutParamsMapAttributes) { + // this is only for temp layout params generated dynamically, so this is never + // platform content. + isPlatformFile = false; + } else if (set != null) { // null parser is ok + // really this should not be happening since its instantiated in Bridge + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + "Parser is not a BridgeXmlBlockParser!", null /*data*/); + return null; + } + + List<Pair<String, Boolean>> attributeList = searchAttrs(attrs); + + BridgeTypedArray ta = ((BridgeResources) mSystemResources).newTypeArray(attrs.length, + isPlatformFile); + + // look for a custom style. + String customStyle = null; + if (set != null) { + customStyle = set.getAttributeValue(null /* namespace*/, "style"); + } + + StyleResourceValue customStyleValues = null; + if (customStyle != null) { + ResourceValue item = mRenderResources.findResValue(customStyle, + false /*forceFrameworkOnly*/); + + // resolve it in case it links to something else + item = mRenderResources.resolveResValue(item); + + if (item instanceof StyleResourceValue) { + customStyleValues = (StyleResourceValue)item; + } + } + + // resolve the defStyleAttr value into a IStyleResourceValue + StyleResourceValue defStyleValues = null; + + if (defStyleAttr != 0) { + // get the name from the int. + Pair<String, Boolean> defStyleAttribute = searchAttr(defStyleAttr); + + if (defaultPropMap != null) { + String defStyleName = defStyleAttribute.getFirst(); + if (defStyleAttribute.getSecond()) { + defStyleName = "android:" + defStyleName; + } + defaultPropMap.put("style", defStyleName); + } + + // look for the style in the current theme, and its parent: + ResourceValue item = mRenderResources.findItemInTheme(defStyleAttribute.getFirst(), + defStyleAttribute.getSecond()); + + if (item != null) { + // item is a reference to a style entry. Search for it. + item = mRenderResources.findResValue(item.getValue(), + false /*forceFrameworkOnly*/); + + if (item instanceof StyleResourceValue) { + defStyleValues = (StyleResourceValue)item; + } + } else { + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE_THEME_ATTR, + String.format( + "Failed to find style '%s' in current theme", + defStyleAttribute.getFirst()), + null /*data*/); + } + } else if (defStyleRes != 0) { + boolean isFrameworkRes = true; + Pair<ResourceType, String> value = Bridge.resolveResourceId(defStyleRes); + if (value == null) { + value = mProjectCallback.resolveResourceId(defStyleRes); + isFrameworkRes = false; + } + + if (value != null) { + if (value.getFirst() == ResourceType.STYLE) { + // look for the style in the current theme, and its parent: + ResourceValue item = mRenderResources.findItemInTheme(value.getSecond(), + isFrameworkRes); + if (item != null) { + if (item instanceof StyleResourceValue) { + if (defaultPropMap != null) { + defaultPropMap.put("style", item.getName()); + } + + defStyleValues = (StyleResourceValue)item; + } + } else { + Bridge.getLog().error(null, + String.format( + "Style with id 0x%x (resolved to '%s') does not exist.", + defStyleRes, value.getSecond()), + null /*data*/); + } + } else { + Bridge.getLog().error(null, + String.format( + "Resouce id 0x%x is not of type STYLE (instead %s)", + defStyleRes, value.getFirst().toString()), + null /*data*/); + } + } else { + Bridge.getLog().error(null, + String.format( + "Failed to find style with id 0x%x in current theme", + defStyleRes), + null /*data*/); + } + } + + String appNamespace = mProjectCallback.getNamespace(); + + if (attributeList != null) { + for (int index = 0 ; index < attributeList.size() ; index++) { + Pair<String, Boolean> attribute = attributeList.get(index); + + if (attribute == null) { + continue; + } + + String attrName = attribute.getFirst(); + boolean frameworkAttr = attribute.getSecond().booleanValue(); + String value = null; + if (set != null) { + value = set.getAttributeValue( + frameworkAttr ? BridgeConstants.NS_RESOURCES : appNamespace, + attrName); + + // if this is an app attribute, and the first get fails, try with the + // new res-auto namespace as well + if (frameworkAttr == false && value == null) { + value = set.getAttributeValue(BridgeConstants.NS_APP_RES_AUTO, attrName); + } + } + + // 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) { + ResourceValue resValue = null; + + // look for the value in the custom style first (and its parent if needed) + if (customStyleValues != null) { + resValue = mRenderResources.findItemInStyle(customStyleValues, + attrName, frameworkAttr); + } + + // then look for the value in the default Style (and its parent if needed) + if (resValue == null && defStyleValues != null) { + resValue = mRenderResources.findItemInStyle(defStyleValues, + attrName, frameworkAttr); + } + + // if the item is not present in the defStyle, we look in the main theme (and + // its parent themes) + if (resValue == null) { + resValue = mRenderResources.findItemInTheme(attrName, frameworkAttr); + } + + // if we found a value, we make sure this doesn't reference another value. + // So we resolve it. + if (resValue != null) { + // put the first default value, before the resolution. + if (defaultPropMap != null) { + defaultPropMap.put(attrName, resValue.getValue()); + } + + resValue = mRenderResources.resolveResValue(resValue); + } + + ta.bridgeSetValue(index, attrName, frameworkAttr, 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, attrName, frameworkAttr, + mRenderResources.resolveValue(null, attrName, value, isPlatformFile)); + } + } + } + + ta.sealArray(); + + return ta; + } + + @Override + public Looper getMainLooper() { + return Looper.myLooper(); + } + + + // ------------- 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(StyleResourceValue style, int[] attrs) + throws Resources.NotFoundException { + + List<Pair<String, Boolean>> attributes = searchAttrs(attrs); + + BridgeTypedArray ta = ((BridgeResources) mSystemResources).newTypeArray(attrs.length, + false); + + // for each attribute, get its name so that we can search it in the style + for (int i = 0 ; i < attrs.length ; i++) { + Pair<String, Boolean> attribute = attributes.get(i); + + if (attribute != null) { + // look for the value in the given style + ResourceValue resValue = mRenderResources.findItemInStyle(style, + attribute.getFirst(), attribute.getSecond()); + + if (resValue != null) { + // resolve it to make sure there are no references left. + ta.bridgeSetValue(i, attribute.getFirst(), attribute.getSecond(), + mRenderResources.resolveResValue(resValue)); + } + } + } + + ta.sealArray(); + + return ta; + } + + + /** + * The input int[] attrs is a list of attributes. The returns a list of information about + * each attributes. The information is (name, isFramework) + * <p/> + * + * @param attrs An attribute array reference given to obtainStyledAttributes. + * @return List of attribute information. + */ + private List<Pair<String, Boolean>> searchAttrs(int[] attrs) { + List<Pair<String, Boolean>> results = new ArrayList<Pair<String, Boolean>>(attrs.length); + + // for each attribute, get its name so that we can search it in the style + for (int i = 0 ; i < attrs.length ; i++) { + Pair<ResourceType, String> resolvedResource = Bridge.resolveResourceId(attrs[i]); + boolean isFramework = false; + if (resolvedResource != null) { + isFramework = true; + } else { + resolvedResource = mProjectCallback.resolveResourceId(attrs[i]); + } + + if (resolvedResource != null) { + results.add(Pair.of(resolvedResource.getSecond(), isFramework)); + } else { + results.add(null); + } + } + + return results; + } + + /** + * Searches for the attribute referenced by its internal id. + * + * @param attr An attribute reference given to obtainStyledAttributes such as defStyle. + * @return A (name, isFramework) pair describing the attribute if found. Returns null + * if nothing is found. + */ + public Pair<String, Boolean> searchAttr(int attr) { + Pair<ResourceType, String> info = Bridge.resolveResourceId(attr); + if (info != null) { + return Pair.of(info.getSecond(), Boolean.TRUE); + } + + info = mProjectCallback.resolveResourceId(attr); + if (info != null) { + return Pair.of(info.getSecond(), Boolean.FALSE); + } + + return null; + } + + public int getDynamicIdByStyle(StyleResourceValue resValue) { + if (mDynamicIdToStyleMap == null) { + // create the maps. + mDynamicIdToStyleMap = new HashMap<Integer, StyleResourceValue>(); + mStyleToDynamicIdMap = new HashMap<StyleResourceValue, 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 StyleResourceValue getStyleByDynamicId(int i) { + if (mDynamicIdToStyleMap != null) { + return mDynamicIdToStyleMap.get(i); + } + + return null; + } + + public int getFrameworkResourceValue(ResourceType resType, String resName, int defValue) { + Integer value = Bridge.getResourceId(resType, resName); + if (value != null) { + return value.intValue(); + } + + return defValue; + } + + public int getProjectResourceValue(ResourceType resType, String resName, int defValue) { + if (mProjectCallback != null) { + Integer value = mProjectCallback.getResourceId(resType, resName); + if (value != null) { + return value.intValue(); + } + } + + return defValue; + } + + //------------ NOT OVERRIDEN -------------------- + + @Override + public boolean bindService(Intent arg0, ServiceConnection arg1, int arg2) { + // pass + return false; + } + + @Override + public int checkCallingOrSelfPermission(String arg0) { + // pass + return 0; + } + + @Override + public int checkCallingOrSelfUriPermission(Uri arg0, int arg1) { + // pass + return 0; + } + + @Override + public int checkCallingPermission(String arg0) { + // pass + return 0; + } + + @Override + public int checkCallingUriPermission(Uri arg0, int arg1) { + // pass + return 0; + } + + @Override + public int checkPermission(String arg0, int arg1, int arg2) { + // pass + return 0; + } + + @Override + public int checkUriPermission(Uri arg0, int arg1, int arg2, int arg3) { + // pass + return 0; + } + + @Override + public int checkUriPermission(Uri arg0, String arg1, String arg2, int arg3, + int arg4, int arg5) { + // pass + return 0; + } + + @Override + public void clearWallpaper() { + // pass + + } + + @Override + public Context createPackageContext(String arg0, int arg1) { + // pass + return null; + } + + @Override + public Context createPackageContextAsUser(String arg0, int arg1, UserHandle user) { + // pass + return null; + } + + @Override + public Context createConfigurationContext(Configuration overrideConfiguration) { + // pass + return null; + } + + @Override + public Context createDisplayContext(Display display) { + // pass + return null; + } + + @Override + public String[] databaseList() { + // pass + return null; + } + + @Override + public boolean deleteDatabase(String arg0) { + // pass + return false; + } + + @Override + public boolean deleteFile(String arg0) { + // pass + return false; + } + + @Override + public void enforceCallingOrSelfPermission(String arg0, String arg1) { + // pass + + } + + @Override + public void enforceCallingOrSelfUriPermission(Uri arg0, int arg1, + String arg2) { + // pass + + } + + @Override + public void enforceCallingPermission(String arg0, String arg1) { + // pass + + } + + @Override + public void enforceCallingUriPermission(Uri arg0, int arg1, String arg2) { + // pass + + } + + @Override + public void enforcePermission(String arg0, int arg1, int arg2, String arg3) { + // pass + + } + + @Override + public void enforceUriPermission(Uri arg0, int arg1, int arg2, int arg3, + String arg4) { + // pass + + } + + @Override + public void enforceUriPermission(Uri arg0, String arg1, String arg2, + int arg3, int arg4, int arg5, String arg6) { + // pass + + } + + @Override + public String[] fileList() { + // pass + return null; + } + + @Override + public AssetManager getAssets() { + // pass + return null; + } + + @Override + public File getCacheDir() { + // pass + return null; + } + + @Override + public File getExternalCacheDir() { + // pass + return null; + } + + @Override + public ContentResolver getContentResolver() { + if (mContentResolver == null) { + mContentResolver = new BridgeContentResolver(this); + } + return mContentResolver; + } + + @Override + public File getDatabasePath(String arg0) { + // pass + return null; + } + + @Override + public File getDir(String arg0, int arg1) { + // pass + return null; + } + + @Override + public File getFileStreamPath(String arg0) { + // pass + return null; + } + + @Override + public File getFilesDir() { + // pass + return null; + } + + @Override + public File getExternalFilesDir(String type) { + // pass + return null; + } + + @Override + public String getPackageCodePath() { + // pass + return null; + } + + @Override + public PackageManager getPackageManager() { + // pass + return null; + } + + @Override + public String getPackageName() { + // pass + return null; + } + + @Override + public String getBasePackageName() { + // pass + return null; + } + + @Override + public ApplicationInfo getApplicationInfo() { + return mApplicationInfo; + } + + @Override + public String getPackageResourcePath() { + // pass + return null; + } + + @Override + public File getSharedPrefsFile(String name) { + // pass + return null; + } + + @Override + public SharedPreferences getSharedPreferences(String arg0, int arg1) { + // pass + return null; + } + + @Override + public Drawable getWallpaper() { + // pass + return null; + } + + @Override + public int getWallpaperDesiredMinimumWidth() { + return -1; + } + + @Override + public int getWallpaperDesiredMinimumHeight() { + return -1; + } + + @Override + public void grantUriPermission(String arg0, Uri arg1, int arg2) { + // pass + + } + + @Override + public FileInputStream openFileInput(String arg0) throws FileNotFoundException { + // pass + return null; + } + + @Override + public FileOutputStream openFileOutput(String arg0, int arg1) throws FileNotFoundException { + // pass + return null; + } + + @Override + public SQLiteDatabase openOrCreateDatabase(String arg0, int arg1, CursorFactory arg2) { + // pass + return null; + } + + @Override + public SQLiteDatabase openOrCreateDatabase(String arg0, int arg1, + CursorFactory arg2, DatabaseErrorHandler arg3) { + // pass + return null; + } + + @Override + public Drawable peekWallpaper() { + // pass + return null; + } + + @Override + public Intent registerReceiver(BroadcastReceiver arg0, IntentFilter arg1) { + // pass + return null; + } + + @Override + public Intent registerReceiver(BroadcastReceiver arg0, IntentFilter arg1, + String arg2, Handler arg3) { + // pass + return null; + } + + @Override + public Intent registerReceiverAsUser(BroadcastReceiver arg0, UserHandle arg0p5, + IntentFilter arg1, String arg2, Handler arg3) { + // pass + return null; + } + + @Override + public void removeStickyBroadcast(Intent arg0) { + // pass + + } + + @Override + public void revokeUriPermission(Uri arg0, int arg1) { + // pass + + } + + @Override + public void sendBroadcast(Intent arg0) { + // pass + + } + + @Override + public void sendBroadcast(Intent arg0, String arg1) { + // pass + + } + + @Override + public void sendBroadcast(Intent intent, String receiverPermission, int appOp) { + // pass + } + + @Override + public void sendOrderedBroadcast(Intent arg0, String arg1) { + // pass + + } + + @Override + public void sendOrderedBroadcast(Intent arg0, String arg1, + BroadcastReceiver arg2, Handler arg3, int arg4, String arg5, + Bundle arg6) { + // pass + + } + + @Override + public void sendOrderedBroadcast(Intent intent, String receiverPermission, int appOp, + BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, + String initialData, Bundle initialExtras) { + // pass + } + + @Override + public void sendBroadcastAsUser(Intent intent, UserHandle user) { + // pass + } + + @Override + public void sendBroadcastAsUser(Intent intent, UserHandle user, + String receiverPermission) { + // pass + } + + @Override + public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, + String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler, + int initialCode, String initialData, Bundle initialExtras) { + // pass + } + + @Override + public void sendStickyBroadcast(Intent arg0) { + // pass + + } + + @Override + public void sendStickyOrderedBroadcast(Intent intent, + BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, + Bundle initialExtras) { + // pass + } + + @Override + public void sendStickyBroadcastAsUser(Intent intent, UserHandle user) { + // pass + } + + @Override + public void sendStickyOrderedBroadcastAsUser(Intent intent, + UserHandle user, BroadcastReceiver resultReceiver, + Handler scheduler, int initialCode, String initialData, + Bundle initialExtras) { + // pass + } + + @Override + public void removeStickyBroadcastAsUser(Intent intent, UserHandle user) { + // pass + } + + @Override + public void setTheme(int arg0) { + // pass + + } + + @Override + public void setWallpaper(Bitmap arg0) throws IOException { + // pass + + } + + @Override + public void setWallpaper(InputStream arg0) throws IOException { + // pass + + } + + @Override + public void startActivity(Intent arg0) { + // pass + } + + @Override + public void startActivity(Intent arg0, Bundle arg1) { + // pass + } + + @Override + public void startIntentSender(IntentSender intent, + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags) + throws IntentSender.SendIntentException { + // pass + } + + @Override + public void startIntentSender(IntentSender intent, + Intent fillInIntent, int flagsMask, int flagsValues, int extraFlags, + Bundle options) throws IntentSender.SendIntentException { + // pass + } + + @Override + public boolean startInstrumentation(ComponentName arg0, String arg1, + Bundle arg2) { + // pass + return false; + } + + @Override + public ComponentName startService(Intent arg0) { + // pass + return null; + } + + @Override + public boolean stopService(Intent arg0) { + // pass + return false; + } + + @Override + public ComponentName startServiceAsUser(Intent arg0, UserHandle arg1) { + // pass + return null; + } + + @Override + public boolean stopServiceAsUser(Intent arg0, UserHandle arg1) { + // pass + return false; + } + + @Override + public void unbindService(ServiceConnection arg0) { + // pass + + } + + @Override + public void unregisterReceiver(BroadcastReceiver arg0) { + // pass + + } + + @Override + public Context getApplicationContext() { + return this; + } + + @Override + public void startActivities(Intent[] arg0) { + // pass + + } + + @Override + public void startActivities(Intent[] arg0, Bundle arg1) { + // pass + + } + + @Override + public boolean isRestricted() { + return false; + } + + @Override + public File getObbDir() { + Bridge.getLog().error(LayoutLog.TAG_UNSUPPORTED, "OBB not supported", null); + return null; + } + + @Override + public DisplayAdjustments getDisplayAdjustments(int displayId) { + // pass + return null; + } + + /** + * @hide + */ + @Override + public int getUserId() { + return 0; // not used + } + + @Override + public File[] getExternalFilesDirs(String type) { + // pass + return new File[0]; + } + + @Override + public File[] getObbDirs() { + // pass + return new File[0]; + } + + @Override + public File[] getExternalCacheDirs() { + // pass + return new File[0]; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java new file mode 100644 index 000000000000..3cf5ed515fda --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.android; + +import com.android.internal.view.IInputContext; +import com.android.internal.view.IInputMethodClient; +import com.android.internal.view.IInputMethodManager; +import com.android.internal.view.InputBindResult; + +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.text.style.SuggestionSpan; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import java.util.List; + +/** + * Basic implementation of IInputMethodManager that does nothing. + * + */ +public class BridgeIInputMethodManager implements IInputMethodManager { + + @Override + public void addClient(IInputMethodClient arg0, IInputContext arg1, int arg2, int arg3) + throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void finishInput(IInputMethodClient arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public InputMethodSubtype getCurrentInputMethodSubtype() throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public List<InputMethodInfo> getEnabledInputMethodList() throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String arg0, + boolean arg1) throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public List<InputMethodInfo> getInputMethodList() throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public InputMethodSubtype getLastInputMethodSubtype() throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public List getShortcutInputMethodsAndSubtypes() throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void hideMySoftInput(IBinder arg0, int arg1) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public boolean hideSoftInput(IInputMethodClient arg0, int arg1, ResultReceiver arg2) + throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean notifySuggestionPicked(SuggestionSpan arg0, String arg1, int arg2) + throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void registerSuggestionSpansForNotification(SuggestionSpan[] arg0) + throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void removeClient(IInputMethodClient arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void setAdditionalInputMethodSubtypes(String arg0, InputMethodSubtype[] arg1) + throws RemoteException { + // TODO Auto-generated method stub + } + + @Override + public boolean setCurrentInputMethodSubtype(InputMethodSubtype arg0) throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void setImeWindowStatus(IBinder arg0, int arg1, int arg2) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void setInputMethod(IBinder arg0, String arg1) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void setInputMethodAndSubtype(IBinder arg0, String arg1, InputMethodSubtype arg2) + throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public boolean setInputMethodEnabled(String arg0, boolean arg1) throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void showInputMethodAndSubtypeEnablerFromClient(IInputMethodClient arg0, String arg1) + throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void showInputMethodPickerFromClient(IInputMethodClient arg0) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public void showMySoftInput(IBinder arg0, int arg1) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public boolean showSoftInput(IInputMethodClient arg0, int arg1, ResultReceiver arg2) + throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public InputBindResult startInput(IInputMethodClient client, IInputContext inputContext, + EditorInfo attribute, int controlFlags) throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean switchToLastInputMethod(IBinder arg0) throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean switchToNextInputMethod(IBinder arg0, boolean arg1) throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean shouldOfferSwitchingToNextInputMethod(IBinder arg0) throws RemoteException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void updateStatusIcon(IBinder arg0, String arg1, int arg2) throws RemoteException { + // TODO Auto-generated method stub + + } + + @Override + public InputBindResult windowGainedFocus(IInputMethodClient client, IBinder windowToken, + int controlFlags, int softInputMode, int windowFlags, EditorInfo attribute, + IInputContext inputContext) throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public IBinder asBinder() { + // TODO Auto-generated method stub + return null; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeLayoutParamsMapAttributes.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeLayoutParamsMapAttributes.java new file mode 100644 index 000000000000..f5912e7bdb24 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeLayoutParamsMapAttributes.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.android; + +import com.android.layoutlib.bridge.BridgeConstants; + +import android.util.AttributeSet; + +import java.util.Map; + +/** + * An implementation of the {@link AttributeSet} interface on top of a map of attribute in the form + * of (name, value). + * + * This is meant to be called only from {@link BridgeContext#obtainStyledAttributes(AttributeSet, int[], int, int)} + * in the case of LayoutParams and therefore isn't a full implementation. + */ +public class BridgeLayoutParamsMapAttributes implements AttributeSet { + + private final Map<String, String> mAttributes; + + public BridgeLayoutParamsMapAttributes(Map<String, String> attributes) { + mAttributes = attributes; + } + + @Override + public String getAttributeValue(String namespace, String name) { + if (BridgeConstants.NS_RESOURCES.equals(namespace)) { + return mAttributes.get(name); + } + + return null; + } + + // ---- the following methods are not called from + // BridgeContext#obtainStyledAttributes(AttributeSet, int[], int, int) + // Should they ever be called, we'll just implement them on a need basis. + + @Override + public int getAttributeCount() { + throw new UnsupportedOperationException(); + } + + @Override + public String getAttributeName(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public String getAttributeValue(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public String getPositionDescription() { + throw new UnsupportedOperationException(); + } + + @Override + public int getAttributeNameResource(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public int getAttributeListValue(String namespace, String attribute, + String[] options, int defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getAttributeBooleanValue(String namespace, String attribute, + boolean defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public int getAttributeResourceValue(String namespace, String attribute, + int defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public int getAttributeIntValue(String namespace, String attribute, + int defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public int getAttributeUnsignedIntValue(String namespace, String attribute, + int defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public float getAttributeFloatValue(String namespace, String attribute, + float defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public int getAttributeListValue(int index, + String[] options, int defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getAttributeBooleanValue(int index, boolean defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public int getAttributeResourceValue(int index, int defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public int getAttributeIntValue(int index, int defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public int getAttributeUnsignedIntValue(int index, int defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public float getAttributeFloatValue(int index, float defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public String getIdAttribute() { + throw new UnsupportedOperationException(); + } + + @Override + public String getClassAttribute() { + throw new UnsupportedOperationException(); + } + + @Override + public int getIdAttributeResourceValue(int defaultValue) { + throw new UnsupportedOperationException(); + } + + @Override + public int getStyleAttribute() { + throw new UnsupportedOperationException(); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java new file mode 100644 index 000000000000..6fd5acc4e04b --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2012 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.android; + +import android.os.IBinder; +import android.os.IPowerManager; +import android.os.RemoteException; +import android.os.WorkSource; + +/** + * Fake implementation of IPowerManager. + * + */ +public class BridgePowerManager implements IPowerManager { + + @Override + public boolean isScreenOn() throws RemoteException { + return true; + } + + @Override + public IBinder asBinder() { + // pass for now. + return null; + } + + @Override + public void acquireWakeLock(IBinder arg0, int arg1, String arg2, String arg2_5, WorkSource arg3) + throws RemoteException { + // pass for now. + } + + @Override + public void crash(String arg0) throws RemoteException { + // pass for now. + } + + @Override + public void goToSleep(long arg0, int arg1) throws RemoteException { + // pass for now. + } + + @Override + public void nap(long arg0) throws RemoteException { + // pass for now. + } + + @Override + public void reboot(boolean confirm, String reason, boolean wait) { + // pass for now. + } + + @Override + public void shutdown(boolean confirm, boolean wait) { + // pass for now. + } + + @Override + public void releaseWakeLock(IBinder arg0, int arg1) throws RemoteException { + // pass for now. + } + + @Override + public void setAttentionLight(boolean arg0, int arg1) throws RemoteException { + // pass for now. + } + + @Override + public void setTemporaryScreenAutoBrightnessAdjustmentSettingOverride(float arg0) throws RemoteException { + // pass for now. + } + + @Override + public void setTemporaryScreenBrightnessSettingOverride(int arg0) throws RemoteException { + // pass for now. + } + + @Override + public void setMaximumScreenOffTimeoutFromDeviceAdmin(int arg0) throws RemoteException { + // pass for now. + } + + @Override + public void setStayOnSetting(int arg0) throws RemoteException { + // pass for now. + } + + @Override + public void updateWakeLockWorkSource(IBinder arg0, WorkSource arg1) throws RemoteException { + // pass for now. + } + + @Override + public boolean isWakeLockLevelSupported(int level) throws RemoteException { + // pass for now. + return true; + } + + @Override + public void userActivity(long time, int event, int flags) throws RemoteException { + // pass for now. + } + + @Override + public void wakeUp(long time) throws RemoteException { + // pass for now. + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java new file mode 100644 index 000000000000..df576d242291 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.android; + +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.view.DragEvent; +import android.view.IWindow; + +/** + * Implementation of {@link IWindow} to pass to the AttachInfo. + */ +public final class BridgeWindow implements IWindow { + + @Override + public void dispatchAppVisibility(boolean arg0) throws RemoteException { + // pass for now. + } + + @Override + public void dispatchGetNewSurface() throws RemoteException { + // pass for now. + } + + @Override + public void executeCommand(String arg0, String arg1, ParcelFileDescriptor arg2) + throws RemoteException { + // pass for now. + } + + @Override + public void resized(Rect arg1, Rect arg1p5, Rect arg2, Rect arg3, + boolean arg4, Configuration arg5) throws RemoteException { + // pass for now. + } + + @Override + public void moved(int arg0, int arg1) throws RemoteException { + // pass for now. + } + + @Override + public void dispatchScreenState(boolean on) throws RemoteException { + // pass for now. + } + + @Override + public void windowFocusChanged(boolean arg0, boolean arg1) throws RemoteException { + // pass for now. + } + + @Override + public void dispatchWallpaperOffsets(float x, float y, float xStep, float yStep, + boolean sync) { + // pass for now. + } + + @Override + public void dispatchWallpaperCommand(String action, int x, int y, + int z, Bundle extras, boolean sync) { + // pass for now. + } + + @Override + public void closeSystemDialogs(String reason) { + // pass for now. + } + + @Override + public void dispatchDragEvent(DragEvent event) { + // pass for now. + } + + @Override + public void dispatchSystemUiVisibilityChanged(int seq, int globalUi, + int localValue, int localChanges) { + // pass for now. + } + + @Override + public void doneAnimating() { + } + + @Override + public IBinder asBinder() { + // pass for now. + return null; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java new file mode 100644 index 000000000000..09e68785231d --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.android; + +import android.content.ClipData; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.graphics.Region; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.view.IWindow; +import android.view.IWindowId; +import android.view.IWindowSession; +import android.view.InputChannel; +import android.view.Surface; +import android.view.SurfaceView; +import android.view.WindowManager.LayoutParams; + +/** + * Implementation of {@link IWindowSession} so that mSession is not null in + * the {@link SurfaceView}. + */ +public final class BridgeWindowSession implements IWindowSession { + + @Override + public int add(IWindow arg0, int seq, LayoutParams arg1, int arg2, Rect arg3, + InputChannel outInputchannel) + throws RemoteException { + // pass for now. + return 0; + } + + @Override + public int addToDisplay(IWindow arg0, int seq, LayoutParams arg1, int arg2, int displayId, + Rect arg3, InputChannel outInputchannel) + throws RemoteException { + // pass for now. + return 0; + } + + @Override + public int addWithoutInputChannel(IWindow arg0, int seq, LayoutParams arg1, int arg2, + Rect arg3) + throws RemoteException { + // pass for now. + return 0; + } + + @Override + public int addToDisplayWithoutInputChannel(IWindow arg0, int seq, LayoutParams arg1, int arg2, + int displayId, Rect arg3) + throws RemoteException { + // pass for now. + return 0; + } + + @Override + public void finishDrawing(IWindow arg0) throws RemoteException { + // pass for now. + } + + @Override + public boolean getInTouchMode() throws RemoteException { + // pass for now. + return false; + } + + @Override + public boolean performHapticFeedback(IWindow window, int effectId, boolean always) { + // pass for now. + return false; + } + @Override + public int relayout(IWindow arg0, int seq, LayoutParams arg1, int arg2, int arg3, int arg4, + int arg4_5, Rect arg5Z, Rect arg5, Rect arg6, Rect arg7, Configuration arg7b, + Surface arg8) throws RemoteException { + // pass for now. + return 0; + } + + @Override + public void performDeferredDestroy(IWindow window) { + // pass for now. + } + + @Override + public boolean outOfMemory(IWindow window) throws RemoteException { + return false; + } + + @Override + public void getDisplayFrame(IWindow window, Rect outDisplayFrame) { + // pass for now. + } + + @Override + public void remove(IWindow arg0) throws RemoteException { + // pass for now. + } + + @Override + public void setInTouchMode(boolean arg0) throws RemoteException { + // pass for now. + } + + @Override + public void setTransparentRegion(IWindow arg0, Region arg1) throws RemoteException { + // pass for now. + } + + @Override + public void setInsets(IWindow window, int touchable, Rect contentInsets, + Rect visibleInsets, Region touchableRegion) { + // pass for now. + } + + @Override + public IBinder prepareDrag(IWindow window, int flags, + int thumbnailWidth, int thumbnailHeight, Surface outSurface) + throws RemoteException { + // pass for now + return null; + } + + @Override + public boolean performDrag(IWindow window, IBinder dragToken, + float touchX, float touchY, float thumbCenterX, float thumbCenterY, + ClipData data) + throws RemoteException { + // pass for now + return false; + } + + @Override + public void reportDropResult(IWindow window, boolean consumed) throws RemoteException { + // pass for now + } + + @Override + public void dragRecipientEntered(IWindow window) throws RemoteException { + // pass for now + } + + @Override + public void dragRecipientExited(IWindow window) throws RemoteException { + // pass for now + } + + @Override + public void setWallpaperPosition(IBinder window, float x, float y, + float xStep, float yStep) { + // pass for now. + } + + @Override + public void wallpaperOffsetsComplete(IBinder window) { + // pass for now. + } + + @Override + public Bundle sendWallpaperCommand(IBinder window, String action, int x, int y, + int z, Bundle extras, boolean sync) { + // pass for now. + return null; + } + + @Override + public void wallpaperCommandComplete(IBinder window, Bundle result) { + // pass for now. + } + + @Override + public void setUniverseTransform(IBinder window, float alpha, float offx, float offy, + float dsdx, float dtdx, float dsdy, float dtdy) { + // pass for now. + } + + @Override + public IBinder asBinder() { + // pass for now. + return null; + } + + @Override + public void onRectangleOnScreenRequested(IBinder window, Rect rectangle, boolean immediate) { + // pass for now. + } + + @Override + public IWindowId getWindowId(IBinder window) throws RemoteException { + // pass for now. + return null; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeXmlBlockParser.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeXmlBlockParser.java new file mode 100644 index 000000000000..ac8712eaa025 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeXmlBlockParser.java @@ -0,0 +1,494 @@ +/* + * 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.android; + + +import com.android.ide.common.rendering.api.ILayoutPullParser; +import com.android.layoutlib.bridge.impl.ParserFactory; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.res.XmlResourceParser; +import android.util.AttributeSet; +import android.util.BridgeXmlPullAttributes; + +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 + * XmlPullAttributes (for the {@link AttributeSet} part). + */ +public class BridgeXmlBlockParser implements XmlResourceParser { + + private final XmlPullParser mParser; + private final BridgeXmlPullAttributes mAttrib; + private final BridgeContext mContext; + private final boolean mPlatformFile; + + private boolean mStarted = false; + private int mEventType = START_DOCUMENT; + + private boolean mPopped = true; // default to true in case it's not pushed. + + /** + * 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) { + if (ParserFactory.LOG_PARSER) { + System.out.println("CRTE " + parser.toString()); + } + + mParser = parser; + mContext = context; + mPlatformFile = platformFile; + mAttrib = new BridgeXmlPullAttributes(parser, context, mPlatformFile); + + if (mContext != null) { + mContext.pushParser(this); + mPopped = false; + } + } + + public XmlPullParser getParser() { + return mParser; + } + + public boolean isPlatformFile() { + return mPlatformFile; + } + + public Object getViewCookie() { + if (mParser instanceof ILayoutPullParser) { + return ((ILayoutPullParser)mParser).getViewCookie(); + } + + return null; + } + + public void ensurePopped() { + if (mContext != null && mPopped == false) { + mContext.popParser(); + mPopped = true; + } + } + + // ------- XmlResourceParser implementation + + @Override + 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); + } + + @Override + public boolean getFeature(String name) { + if (FEATURE_PROCESS_NAMESPACES.equals(name)) { + return true; + } + if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name)) { + return true; + } + return false; + } + + @Override + public void setProperty(String name, Object value) throws XmlPullParserException { + throw new XmlPullParserException("setProperty() not supported"); + } + + @Override + public Object getProperty(String name) { + return null; + } + + @Override + public void setInput(Reader in) throws XmlPullParserException { + mParser.setInput(in); + } + + @Override + public void setInput(InputStream inputStream, String inputEncoding) + throws XmlPullParserException { + mParser.setInput(inputStream, inputEncoding); + } + + @Override + public void defineEntityReplacementText(String entityName, + String replacementText) throws XmlPullParserException { + throw new XmlPullParserException( + "defineEntityReplacementText() not supported"); + } + + @Override + public String getNamespacePrefix(int pos) throws XmlPullParserException { + throw new XmlPullParserException("getNamespacePrefix() not supported"); + } + + @Override + public String getInputEncoding() { + return null; + } + + @Override + public String getNamespace(String prefix) { + throw new RuntimeException("getNamespace() not supported"); + } + + @Override + public int getNamespaceCount(int depth) throws XmlPullParserException { + throw new XmlPullParserException("getNamespaceCount() not supported"); + } + + @Override + public String getPositionDescription() { + return "Binary XML file line #" + getLineNumber(); + } + + @Override + public String getNamespaceUri(int pos) throws XmlPullParserException { + throw new XmlPullParserException("getNamespaceUri() not supported"); + } + + @Override + public int getColumnNumber() { + return -1; + } + + @Override + public int getDepth() { + return mParser.getDepth(); + } + + @Override + public String getText() { + return mParser.getText(); + } + + @Override + public int getLineNumber() { + return mParser.getLineNumber(); + } + + @Override + public int getEventType() { + return mEventType; + } + + @Override + public boolean isWhitespace() throws XmlPullParserException { + // Original comment: whitespace was stripped by aapt. + return mParser.isWhitespace(); + } + + @Override + public String getPrefix() { + throw new RuntimeException("getPrefix not supported"); + } + + @Override + 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; + } + + @Override + public String getNamespace() { + return mParser.getNamespace(); + } + + @Override + public String getName() { + return mParser.getName(); + } + + @Override + public String getAttributeNamespace(int index) { + return mParser.getAttributeNamespace(index); + } + + @Override + public String getAttributeName(int index) { + return mParser.getAttributeName(index); + } + + @Override + public String getAttributePrefix(int index) { + throw new RuntimeException("getAttributePrefix not supported"); + } + + @Override + public boolean isEmptyElementTag() { + // XXX Need to detect this. + return false; + } + + @Override + public int getAttributeCount() { + return mParser.getAttributeCount(); + } + + @Override + public String getAttributeValue(int index) { + return mParser.getAttributeValue(index); + } + + @Override + public String getAttributeType(int index) { + return "CDATA"; + } + + @Override + public boolean isAttributeDefault(int index) { + return false; + } + + @Override + public int nextToken() throws XmlPullParserException, IOException { + return next(); + } + + @Override + public String getAttributeValue(String namespace, String name) { + return mParser.getAttributeValue(namespace, name); + } + + @Override + public int next() throws XmlPullParserException, IOException { + if (!mStarted) { + mStarted = true; + + if (ParserFactory.LOG_PARSER) { + System.out.println("STRT " + mParser.toString()); + } + + return START_DOCUMENT; + } + + int ev = mParser.next(); + + if (ParserFactory.LOG_PARSER) { + System.out.println("NEXT " + mParser.toString() + " " + + eventTypeToString(mEventType) + " -> " + eventTypeToString(ev)); + } + + if (ev == END_TAG && mParser.getDepth() == 1) { + // done with parser remove it from the context stack. + ensurePopped(); + + if (ParserFactory.LOG_PARSER) { + System.out.println(""); + } + } + + mEventType = ev; + return ev; + } + + public static String eventTypeToString(int eventType) { + switch (eventType) { + case START_DOCUMENT: + return "START_DOC"; + case END_DOCUMENT: + return "END_DOC"; + case START_TAG: + return "START_TAG"; + case END_TAG: + return "END_TAG"; + case TEXT: + return "TEXT"; + case CDSECT: + return "CDSECT"; + case ENTITY_REF: + return "ENTITY_REF"; + case IGNORABLE_WHITESPACE: + return "IGNORABLE_WHITESPACE"; + case PROCESSING_INSTRUCTION: + return "PROCESSING_INSTRUCTION"; + case COMMENT: + return "COMMENT"; + case DOCDECL: + return "DOCDECL"; + } + + return "????"; + } + + @Override + 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()); + } + + @Override + 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); + } + } + + @Override + 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 + + + @Override + public void close() { + // pass + } + + @Override + public boolean getAttributeBooleanValue(int index, boolean defaultValue) { + return mAttrib.getAttributeBooleanValue(index, defaultValue); + } + + @Override + public boolean getAttributeBooleanValue(String namespace, String attribute, + boolean defaultValue) { + return mAttrib.getAttributeBooleanValue(namespace, attribute, defaultValue); + } + + @Override + public float getAttributeFloatValue(int index, float defaultValue) { + return mAttrib.getAttributeFloatValue(index, defaultValue); + } + + @Override + public float getAttributeFloatValue(String namespace, String attribute, float defaultValue) { + return mAttrib.getAttributeFloatValue(namespace, attribute, defaultValue); + } + + @Override + public int getAttributeIntValue(int index, int defaultValue) { + return mAttrib.getAttributeIntValue(index, defaultValue); + } + + @Override + public int getAttributeIntValue(String namespace, String attribute, int defaultValue) { + return mAttrib.getAttributeIntValue(namespace, attribute, defaultValue); + } + + @Override + public int getAttributeListValue(int index, String[] options, int defaultValue) { + return mAttrib.getAttributeListValue(index, options, defaultValue); + } + + @Override + public int getAttributeListValue(String namespace, String attribute, + String[] options, int defaultValue) { + return mAttrib.getAttributeListValue(namespace, attribute, options, defaultValue); + } + + @Override + public int getAttributeNameResource(int index) { + return mAttrib.getAttributeNameResource(index); + } + + @Override + public int getAttributeResourceValue(int index, int defaultValue) { + return mAttrib.getAttributeResourceValue(index, defaultValue); + } + + @Override + public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) { + return mAttrib.getAttributeResourceValue(namespace, attribute, defaultValue); + } + + @Override + public int getAttributeUnsignedIntValue(int index, int defaultValue) { + return mAttrib.getAttributeUnsignedIntValue(index, defaultValue); + } + + @Override + public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) { + return mAttrib.getAttributeUnsignedIntValue(namespace, attribute, defaultValue); + } + + @Override + public String getClassAttribute() { + return mAttrib.getClassAttribute(); + } + + @Override + public String getIdAttribute() { + return mAttrib.getIdAttribute(); + } + + @Override + public int getIdAttributeResourceValue(int defaultValue) { + return mAttrib.getIdAttributeResourceValue(defaultValue); + } + + @Override + public int getStyleAttribute() { + return mAttrib.getStyleAttribute(); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/view/WindowManagerImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/view/WindowManagerImpl.java new file mode 100644 index 000000000000..9a633bf27f74 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/view/WindowManagerImpl.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2012 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.android.view; + +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.DisplayInfo; +import android.view.View; +import android.view.WindowManager; + +public class WindowManagerImpl implements WindowManager { + + private final DisplayMetrics mMetrics; + private final Display mDisplay; + + public WindowManagerImpl(DisplayMetrics metrics) { + mMetrics = metrics; + + DisplayInfo info = new DisplayInfo(); + info.logicalHeight = mMetrics.heightPixels; + info.logicalWidth = mMetrics.widthPixels; + mDisplay = new Display(null, Display.DEFAULT_DISPLAY, info, null); + } + + @Override + public Display getDefaultDisplay() { + return mDisplay; + } + + + @Override + public void addView(View arg0, android.view.ViewGroup.LayoutParams arg1) { + // pass + } + + @Override + public void removeView(View arg0) { + // pass + } + + @Override + public void updateViewLayout(View arg0, android.view.ViewGroup.LayoutParams arg1) { + // pass + } + + + @Override + public void removeViewImmediate(View arg0) { + // pass + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java new file mode 100644 index 000000000000..ea9d8d929bc8 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.bars; + +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.StyleResourceValue; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; +import com.android.layoutlib.bridge.impl.ParserFactory; +import com.android.layoutlib.bridge.impl.ResourceHelper; +import com.android.resources.Density; +import com.android.resources.ResourceType; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Bitmap; +import android.graphics.Bitmap_Delegate; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Base "bar" class for the window decor around the the edited layout. + * This is basically an horizontal layout that loads a given layout on creation (it is read + * through {@link Class#getResourceAsStream(String)}). + * + * The given layout should be a merge layout so that all the children belong to this class directly. + * + * It also provides a few utility methods to configure the content of the layout. + */ +abstract class CustomBar extends LinearLayout { + + protected abstract TextView getStyleableTextView(); + + protected CustomBar(Context context, Density density, int orientation, String layoutPath, + String name) throws XmlPullParserException { + super(context); + setOrientation(orientation); + if (orientation == LinearLayout.HORIZONTAL) { + setGravity(Gravity.CENTER_VERTICAL); + } else { + setGravity(Gravity.CENTER_HORIZONTAL); + } + + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + + XmlPullParser parser = ParserFactory.create(getClass().getResourceAsStream(layoutPath), + name); + + BridgeXmlBlockParser bridgeParser = new BridgeXmlBlockParser( + parser, (BridgeContext) context, false /*platformFile*/); + + try { + inflater.inflate(bridgeParser, this, true); + } finally { + bridgeParser.ensurePopped(); + } + } + + private InputStream getIcon(String iconName, Density[] densityInOut, String[] pathOut, + boolean tryOtherDensities) { + // current density + Density density = densityInOut[0]; + + // bitmap url relative to this class + pathOut[0] = "/bars/" + density.getResourceValue() + "/" + iconName; + + InputStream stream = getClass().getResourceAsStream(pathOut[0]); + if (stream == null && tryOtherDensities) { + for (Density d : Density.values()) { + if (d != density) { + densityInOut[0] = d; + stream = getIcon(iconName, densityInOut, pathOut, false /*tryOtherDensities*/); + if (stream != null) { + return stream; + } + } + } + } + + return stream; + } + + protected void loadIcon(int index, String iconName, Density density) { + View child = getChildAt(index); + if (child instanceof ImageView) { + ImageView imageView = (ImageView) child; + + String[] pathOut = new String[1]; + Density[] densityInOut = new Density[] { density }; + InputStream stream = getIcon(iconName, densityInOut, pathOut, + true /*tryOtherDensities*/); + density = densityInOut[0]; + + if (stream != null) { + // look for a cached bitmap + Bitmap bitmap = Bridge.getCachedBitmap(pathOut[0], true /*isFramework*/); + if (bitmap == null) { + try { + bitmap = Bitmap_Delegate.createBitmap(stream, false /*isMutable*/, density); + Bridge.setCachedBitmap(pathOut[0], bitmap, true /*isFramework*/); + } catch (IOException e) { + return; + } + } + + if (bitmap != null) { + BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), + bitmap); + imageView.setImageDrawable(drawable); + } + } + } + } + + protected void loadIcon(int index, String iconReference) { + ResourceValue value = getResourceValue(iconReference); + if (value != null) { + loadIcon(index, value); + } + } + + protected void loadIconById(int id, String iconReference) { + ResourceValue value = getResourceValue(iconReference); + if (value != null) { + loadIconById(id, value); + } + } + + + protected Drawable loadIcon(int index, ResourceType type, String name) { + BridgeContext bridgeContext = (BridgeContext) mContext; + RenderResources res = bridgeContext.getRenderResources(); + + // find the resource + ResourceValue value = res.getFrameworkResource(type, name); + + // resolve it if needed + value = res.resolveResValue(value); + return loadIcon(index, value); + } + + private Drawable loadIcon(int index, ResourceValue value) { + View child = getChildAt(index); + if (child instanceof ImageView) { + ImageView imageView = (ImageView) child; + + return loadIcon(imageView, value); + } + + return null; + } + + private Drawable loadIconById(int id, ResourceValue value) { + View child = findViewById(id); + if (child instanceof ImageView) { + ImageView imageView = (ImageView) child; + + return loadIcon(imageView, value); + } + + return null; + } + + + private Drawable loadIcon(ImageView imageView, ResourceValue value) { + Drawable drawable = ResourceHelper.getDrawable(value, (BridgeContext) mContext); + if (drawable != null) { + imageView.setImageDrawable(drawable); + } + + return drawable; + } + + protected TextView setText(int index, String stringReference) { + View child = getChildAt(index); + if (child instanceof TextView) { + TextView textView = (TextView) child; + setText(textView, stringReference); + return textView; + } + + return null; + } + + protected TextView setTextById(int id, String stringReference) { + View child = findViewById(id); + if (child instanceof TextView) { + TextView textView = (TextView) child; + setText(textView, stringReference); + return textView; + } + + return null; + } + + private void setText(TextView textView, String stringReference) { + ResourceValue value = getResourceValue(stringReference); + if (value != null) { + textView.setText(value.getValue()); + } else { + textView.setText(stringReference); + } + } + + protected void setStyle(String themeEntryName) { + + BridgeContext bridgeContext = (BridgeContext) mContext; + RenderResources res = bridgeContext.getRenderResources(); + + ResourceValue value = res.findItemInTheme(themeEntryName, true /*isFrameworkAttr*/); + value = res.resolveResValue(value); + + if (value instanceof StyleResourceValue == false) { + return; + } + + StyleResourceValue style = (StyleResourceValue) value; + + // get the background + ResourceValue backgroundValue = res.findItemInStyle(style, "background", + true /*isFrameworkAttr*/); + backgroundValue = res.resolveResValue(backgroundValue); + if (backgroundValue != null) { + Drawable d = ResourceHelper.getDrawable(backgroundValue, bridgeContext); + if (d != null) { + setBackground(d); + } + } + + TextView textView = getStyleableTextView(); + if (textView != null) { + // get the text style + ResourceValue textStyleValue = res.findItemInStyle(style, "titleTextStyle", + true /*isFrameworkAttr*/); + textStyleValue = res.resolveResValue(textStyleValue); + if (textStyleValue instanceof StyleResourceValue) { + StyleResourceValue textStyle = (StyleResourceValue) textStyleValue; + + ResourceValue textSize = res.findItemInStyle(textStyle, "textSize", + true /*isFrameworkAttr*/); + textSize = res.resolveResValue(textSize); + + if (textSize != null) { + TypedValue out = new TypedValue(); + if (ResourceHelper.parseFloatAttribute("textSize", textSize.getValue(), out, + true /*requireUnit*/)) { + textView.setTextSize( + out.getDimension(bridgeContext.getResources().getDisplayMetrics())); + } + } + + + ResourceValue textColor = res.findItemInStyle(textStyle, "textColor", + true /*isFrameworkAttr*/); + textColor = res.resolveResValue(textColor); + if (textColor != null) { + ColorStateList stateList = ResourceHelper.getColorStateList( + textColor, bridgeContext); + if (stateList != null) { + textView.setTextColor(stateList); + } + } + } + } + } + + private ResourceValue getResourceValue(String reference) { + BridgeContext bridgeContext = (BridgeContext) mContext; + RenderResources res = bridgeContext.getRenderResources(); + + // find the resource + ResourceValue value = res.findResValue(reference, false /*isFramework*/); + + // resolve it if needed + return res.resolveResValue(value); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FakeActionBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FakeActionBar.java new file mode 100644 index 000000000000..226649dbc406 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/FakeActionBar.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.bars; + +import com.android.resources.Density; + +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Context; +import android.widget.LinearLayout; +import android.widget.TextView; + +public class FakeActionBar extends CustomBar { + + private TextView mTextView; + + public FakeActionBar(Context context, Density density, String label, String icon) + throws XmlPullParserException { + super(context, density, LinearLayout.HORIZONTAL, "/bars/action_bar.xml", "action_bar.xml"); + + // Cannot access the inside items through id because no R.id values have been + // created for them. + // We do know the order though. + loadIconById(android.R.id.home, icon); + mTextView = setText(1, label); + + setStyle("actionBarStyle"); + } + + @Override + protected TextView getStyleableTextView() { + return mTextView; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java new file mode 100644 index 000000000000..cc90d6b33abe --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.bars; + +import com.android.resources.Density; + +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Context; +import android.widget.LinearLayout; +import android.widget.TextView; + +public class NavigationBar extends CustomBar { + + public NavigationBar(Context context, Density density, int orientation) throws XmlPullParserException { + super(context, density, orientation, "/bars/navigation_bar.xml", "navigation_bar.xml"); + + setBackgroundColor(0xFF000000); + + // Cannot access the inside items through id because no R.id values have been + // created for them. + // We do know the order though. + // 0 is a spacer. + int back = 1; + int recent = 3; + if (orientation == LinearLayout.VERTICAL) { + back = 3; + recent = 1; + } + + loadIcon(back, "ic_sysbar_back.png", density); + loadIcon(2, "ic_sysbar_home.png", density); + loadIcon(recent, "ic_sysbar_recent.png", density); + } + + @Override + protected TextView getStyleableTextView() { + return null; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/StatusBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/StatusBar.java new file mode 100644 index 000000000000..5c084121d123 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/StatusBar.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.bars; + +import com.android.resources.Density; +import com.android.resources.ResourceType; + +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LevelListDrawable; +import android.view.Gravity; +import android.widget.LinearLayout; +import android.widget.TextView; + +public class StatusBar extends CustomBar { + + public StatusBar(Context context, Density density) throws XmlPullParserException { + super(context, density, LinearLayout.HORIZONTAL, "/bars/status_bar.xml", "status_bar.xml"); + + // FIXME: use FILL_H? + setGravity(Gravity.START | Gravity.TOP | Gravity.RIGHT); + setBackgroundColor(0xFF000000); + + // Cannot access the inside items through id because no R.id values have been + // created for them. + // We do know the order though. + // 0 is the spacer + loadIcon(1, "stat_sys_wifi_signal_4_fully.png", density); + Drawable drawable = loadIcon(2, ResourceType.DRAWABLE, "stat_sys_battery_charge"); + if (drawable instanceof LevelListDrawable) { + ((LevelListDrawable) drawable).setLevel(100); + } + } + + @Override + protected TextView getStyleableTextView() { + return null; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/TitleBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/TitleBar.java new file mode 100644 index 000000000000..c27859f901c1 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/TitleBar.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.bars; + +import com.android.resources.Density; + +import org.xmlpull.v1.XmlPullParserException; + +import android.content.Context; +import android.widget.LinearLayout; +import android.widget.TextView; + +public class TitleBar extends CustomBar { + + private TextView mTextView; + + public TitleBar(Context context, Density density, String label) + throws XmlPullParserException { + super(context, density, LinearLayout.HORIZONTAL, "/bars/title_bar.xml", "title_bar.xml"); + + // Cannot access the inside items through id because no R.id values have been + // created for them. + // We do know the order though. + mTextView = setText(0, label); + + setStyle("windowTitleBackgroundStyle"); + } + + @Override + protected TextView getStyleableTextView() { + return mTextView; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/DelegateManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/DelegateManager.java new file mode 100644 index 000000000000..ae1217d5c5d8 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/DelegateManager.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl; + +import com.android.layoutlib.bridge.util.Debug; +import com.android.layoutlib.bridge.util.SparseWeakArray; + +import android.util.SparseArray; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * Manages native delegates. + * + * This is used in conjunction with layoublib_create: certain Android java classes are mere + * wrappers around a heavily native based implementation, and we need a way to run these classes + * in our Eclipse rendering framework without bringing all the native code from the Android + * platform. + * + * Thus we instruct layoutlib_create to modify the bytecode of these classes to replace their + * native methods by "delegate calls". + * + * For example, a native method android.graphics.Matrix.init(...) will actually become + * a call to android.graphics.Matrix_Delegate.init(...). + * + * The Android java classes that use native code uses an int (Java side) to reference native + * objects. This int is generally directly the pointer to the C structure counterpart. + * Typically a creation method will return such an int, and then this int will be passed later + * to a Java method to identify the C object to manipulate. + * + * Since we cannot use the Java object reference as the int directly, DelegateManager manages the + * int -> Delegate class link. + * + * Native methods usually always have the int as parameters. The first thing the delegate method + * will do is call {@link #getDelegate(int)} to get the Java object matching the int. + * + * Typical native init methods are returning a new int back to the Java class, so + * {@link #addNewDelegate(Object)} does the same. + * + * The JNI references are counted, so we do the same through a {@link WeakReference}. Because + * the Java object needs to count as a reference (even though it only holds an int), we use the + * following mechanism: + * + * - {@link #addNewDelegate(Object)} and {@link #removeJavaReferenceFor(int)} adds and removes + * the delegate to/from a list. This list hold the reference and prevents the GC from reclaiming + * the delegate. + * + * - {@link #addNewDelegate(Object)} also adds the delegate to a {@link SparseArray} that holds a + * {@link WeakReference} to the delegate. This allows the delegate to be deleted automatically + * when nothing references it. This means that any class that holds a delegate (except for the + * Java main class) must not use the int but the Delegate class instead. The integers must + * only be used in the API between the main Java class and the Delegate. + * + * @param <T> the delegate class to manage + */ +public final class DelegateManager<T> { + private final Class<T> mClass; + private final SparseWeakArray<T> mDelegates = new SparseWeakArray<T>(); + /** list used to store delegates when their main object holds a reference to them. + * This is to ensure that the WeakReference in the SparseWeakArray doesn't get GC'ed + * @see #addNewDelegate(Object) + * @see #removeJavaReferenceFor(int) + */ + private final List<T> mJavaReferences = new ArrayList<T>(); + private int mDelegateCounter = 0; + + public DelegateManager(Class<T> theClass) { + mClass = theClass; + } + + /** + * Returns the delegate from the given native int. + * <p> + * If the int is zero, then this will always return null. + * <p> + * If the int is non zero and the delegate is not found, this will throw an assert. + * + * @param native_object the native int. + * @return the delegate or null if not found. + */ + public T getDelegate(int native_object) { + if (native_object > 0) { + T delegate = mDelegates.get(native_object); + + if (Debug.DEBUG) { + if (delegate == null) { + System.out.println("Unknown " + mClass.getSimpleName() + " with int " + + native_object); + } + } + + assert delegate != null; + return delegate; + } + return null; + } + + /** + * Adds a delegate to the manager and returns the native int used to identify it. + * @param newDelegate the delegate to add + * @return a unique native int to identify the delegate + */ + public int addNewDelegate(T newDelegate) { + int native_object = ++mDelegateCounter; + mDelegates.put(native_object, newDelegate); + assert !mJavaReferences.contains(newDelegate); + mJavaReferences.add(newDelegate); + + if (Debug.DEBUG) { + System.out.println("New " + mClass.getSimpleName() + " with int " + native_object); + } + + return native_object; + } + + /** + * Removes the main reference on the given delegate. + * @param native_object the native integer representing the delegate. + */ + public void removeJavaReferenceFor(int native_object) { + T delegate = getDelegate(native_object); + + if (Debug.DEBUG) { + System.out.println("Removing main Java ref on " + mClass.getSimpleName() + + " with int " + native_object); + } + + mJavaReferences.remove(delegate); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/FontLoader.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/FontLoader.java new file mode 100644 index 000000000000..081ce67c7950 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/FontLoader.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.impl; + +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.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +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_SYSTEM = "system_fonts.xml"; + private static final String FONTS_VENDOR = "vendor_fonts.xml"; + private static final String FONTS_FALLBACK = "fallback_fonts.xml"; + + private static final String NODE_FAMILYSET = "familyset"; + private static final String NODE_FAMILY = "family"; + private static final String NODE_NAME = "name"; + private static final String NODE_FILE = "file"; + + private static final String FONT_SUFFIX_NONE = ".ttf"; + private static final String FONT_SUFFIX_REGULAR = "-Regular.ttf"; + private static final String FONT_SUFFIX_BOLD = "-Bold.ttf"; + private static final String FONT_SUFFIX_ITALIC = "-Italic.ttf"; + private static final String FONT_SUFFIX_BOLDITALIC = "-BoldItalic.ttf"; + + // This must match the values of Typeface styles so that we can use them for indices in this + // array. + private static final int[] AWT_STYLES = new int[] { + Font.PLAIN, + Font.BOLD, + Font.ITALIC, + Font.BOLD | Font.ITALIC + }; + private static int[] DERIVE_BOLD_ITALIC = new int[] { + Typeface.ITALIC, Typeface.BOLD, Typeface.NORMAL + }; + private static int[] DERIVE_ITALIC = new int[] { Typeface.NORMAL }; + private static int[] DERIVE_BOLD = new int[] { Typeface.NORMAL }; + + private static final List<FontInfo> mMainFonts = new ArrayList<FontInfo>(); + private static final List<FontInfo> mFallbackFonts = new ArrayList<FontInfo>(); + + private final String mOsFontsLocation; + + public static FontLoader create(String fontOsLocation) { + try { + SAXParserFactory parserFactory = SAXParserFactory.newInstance(); + parserFactory.setNamespaceAware(true); + + // parse the system fonts + FontHandler handler = parseFontFile(parserFactory, fontOsLocation, FONTS_SYSTEM); + List<FontInfo> systemFonts = handler.getFontList(); + + + // parse the fallback fonts + handler = parseFontFile(parserFactory, fontOsLocation, FONTS_FALLBACK); + List<FontInfo> fallbackFonts = handler.getFontList(); + + return new FontLoader(fontOsLocation, systemFonts, fallbackFonts); + } 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 static FontHandler parseFontFile(SAXParserFactory parserFactory, + String fontOsLocation, String fontFileName) + throws ParserConfigurationException, SAXException, IOException, FileNotFoundException { + + SAXParser parser = parserFactory.newSAXParser(); + File f = new File(fontOsLocation, fontFileName); + + FontHandler definitionParser = new FontHandler( + fontOsLocation + File.separator); + parser.parse(new FileInputStream(f), definitionParser); + return definitionParser; + } + + private FontLoader(String fontOsLocation, + List<FontInfo> fontList, List<FontInfo> fallBackList) { + mOsFontsLocation = fontOsLocation; + mMainFonts.addAll(fontList); + mFallbackFonts.addAll(fallBackList); + } + + + public String getOsFontsLocation() { + return mOsFontsLocation; + } + + /** + * Returns a {@link Font} object given a family name and a style value (constant in + * {@link Typeface}). + * @param family the family name + * @param style a 1-item array containing the requested style. Based on the font being read + * the actual style may be different. The array contains the actual style after + * the method returns. + * @return the font object or null if no match could be found. + */ + public synchronized List<Font> getFont(String family, int style) { + List<Font> result = new ArrayList<Font>(); + + if (family == null) { + return result; + } + + + // get the font objects from the main list based on family. + for (FontInfo info : mMainFonts) { + if (info.families.contains(family)) { + result.add(info.font[style]); + break; + } + } + + // add all the fallback fonts for the given style + for (FontInfo info : mFallbackFonts) { + result.add(info.font[style]); + } + + return result; + } + + + public synchronized List<Font> getFallbackFonts(int style) { + List<Font> result = new ArrayList<Font>(); + // add all the fallback fonts + for (FontInfo info : mFallbackFonts) { + result.add(info.font[style]); + } + return result; + } + + + private final static class FontInfo { + final Font[] font = new Font[4]; // Matches the 4 type-face styles. + final Set<String> families; + + FontInfo() { + families = new HashSet<String>(); + } + } + + private final static class FontHandler extends DefaultHandler { + private final String mOsFontsLocation; + + private FontInfo mFontInfo = null; + private final StringBuilder mBuilder = new StringBuilder(); + private List<FontInfo> mFontList = new ArrayList<FontInfo>(); + + private FontHandler(String osFontsLocation) { + super(); + mOsFontsLocation = osFontsLocation; + } + + public List<FontInfo> getFontList() { + return 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 (NODE_FAMILYSET.equals(localName)) { + mFontList = new ArrayList<FontInfo>(); + } else if (NODE_FAMILY.equals(localName)) { + if (mFontList != null) { + mFontInfo = new FontInfo(); + } + } + + mBuilder.setLength(0); + + super.startElement(uri, localName, name, attributes); + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int) + */ + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + mBuilder.append(ch, start, length); + } + + /* (non-Javadoc) + * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String) + */ + @Override + public void endElement(String uri, String localName, String name) throws SAXException { + if (NODE_FAMILY.equals(localName)) { + if (mFontInfo != null) { + // if has a normal font file, add to the list + if (mFontInfo.font[Typeface.NORMAL] != null) { + mFontList.add(mFontInfo); + + // create missing font styles, order is important. + if (mFontInfo.font[Typeface.BOLD_ITALIC] == null) { + computeDerivedFont(Typeface.BOLD_ITALIC, DERIVE_BOLD_ITALIC); + } + if (mFontInfo.font[Typeface.ITALIC] == null) { + computeDerivedFont(Typeface.ITALIC, DERIVE_ITALIC); + } + if (mFontInfo.font[Typeface.BOLD] == null) { + computeDerivedFont(Typeface.BOLD, DERIVE_BOLD); + } + } + + mFontInfo = null; + } + } else if (NODE_NAME.equals(localName)) { + // handle a new name for an existing Font Info + if (mFontInfo != null) { + String family = trimXmlWhitespaces(mBuilder.toString()); + mFontInfo.families.add(family); + } + } else if (NODE_FILE.equals(localName)) { + // handle a new file for an existing Font Info + if (mFontInfo != null) { + String fileName = trimXmlWhitespaces(mBuilder.toString()); + Font font = getFont(fileName); + if (font != null) { + if (fileName.endsWith(FONT_SUFFIX_REGULAR)) { + mFontInfo.font[Typeface.NORMAL] = font; + } else if (fileName.endsWith(FONT_SUFFIX_BOLD)) { + mFontInfo.font[Typeface.BOLD] = font; + } else if (fileName.endsWith(FONT_SUFFIX_ITALIC)) { + mFontInfo.font[Typeface.ITALIC] = font; + } else if (fileName.endsWith(FONT_SUFFIX_BOLDITALIC)) { + mFontInfo.font[Typeface.BOLD_ITALIC] = font; + } else if (fileName.endsWith(FONT_SUFFIX_NONE)) { + mFontInfo.font[Typeface.NORMAL] = font; + } + } + } + } + } + + private Font getFont(String fileName) { + try { + File file = new File(mOsFontsLocation, fileName); + if (file.exists()) { + return Font.createFont(Font.TRUETYPE_FONT, file); + } + } catch (Exception e) { + + } + + return null; + } + + private void computeDerivedFont( int toCompute, int[] basedOnList) { + for (int basedOn : basedOnList) { + if (mFontInfo.font[basedOn] != null) { + mFontInfo.font[toCompute] = + mFontInfo.font[basedOn].deriveFont(AWT_STYLES[toCompute]); + return; + } + } + + // we really shouldn't stop there. This means we don't have a NORMAL font... + assert false; + } + + 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/impl/GcSnapshot.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/GcSnapshot.java new file mode 100644 index 000000000000..21d6b1a67505 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/GcSnapshot.java @@ -0,0 +1,803 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; + +import android.graphics.Bitmap_Delegate; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Paint_Delegate; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.graphics.Region_Delegate; +import android.graphics.Shader_Delegate; +import android.graphics.Xfermode_Delegate; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.util.ArrayList; + +/** + * Class representing a graphics context snapshot, as well as a context stack as a linked list. + * <p> + * This is based on top of {@link Graphics2D} but can operate independently if none are available + * yet when setting transforms and clip information. + * <p> + * This allows for drawing through {@link #draw(Drawable, Paint_Delegate)} and + * {@link #draw(Drawable, Paint_Delegate)} + * + * Handling of layers (created with {@link Canvas#saveLayer(RectF, Paint, int)}) is handled through + * a list of Graphics2D for each layers. The class actually maintains a list of {@link Layer} + * for each layer. Doing a save() will duplicate this list so that each graphics2D object + * ({@link Layer#getGraphics()}) is configured only for the new snapshot. + */ +public class GcSnapshot { + + private final GcSnapshot mPrevious; + private final int mFlags; + + /** list of layers. The first item in the list is always the */ + private final ArrayList<Layer> mLayers = new ArrayList<Layer>(); + + /** temp transform in case transformation are set before a Graphics2D exists */ + private AffineTransform mTransform = null; + /** temp clip in case clipping is set before a Graphics2D exists */ + private Area mClip = null; + + // local layer data + /** a local layer created with {@link Canvas#saveLayer(RectF, Paint, int)}. + * If this is null, this does not mean there's no layer, just that the snapshot is not the + * one that created the layer. + * @see #getLayerSnapshot() + */ + private final Layer mLocalLayer; + private final Paint_Delegate mLocalLayerPaint; + private final Rect mLayerBounds; + + public interface Drawable { + void draw(Graphics2D graphics, Paint_Delegate paint); + } + + /** + * Class containing information about a layer. + * + * This contains graphics, bitmap and layer information. + */ + private static class Layer { + private final Graphics2D mGraphics; + private final Bitmap_Delegate mBitmap; + private final BufferedImage mImage; + /** the flags that were used to configure the layer. This is never changed, and passed + * as is when {@link #makeCopy()} is called */ + private final int mFlags; + /** the original content of the layer when the next object was created. This is not + * passed in {@link #makeCopy()} and instead is recreated when a new layer is added + * (depending on its flags) */ + private BufferedImage mOriginalCopy; + + /** + * Creates a layer with a graphics and a bitmap. This is only used to create + * the base layer. + * + * @param graphics the graphics + * @param bitmap the bitmap + */ + Layer(Graphics2D graphics, Bitmap_Delegate bitmap) { + mGraphics = graphics; + mBitmap = bitmap; + mImage = mBitmap.getImage(); + mFlags = 0; + } + + /** + * Creates a layer with a graphics and an image. If the image belongs to a + * {@link Bitmap_Delegate} (case of the base layer), then + * {@link Layer#Layer(Graphics2D, Bitmap_Delegate)} should be used. + * + * @param graphics the graphics the new graphics for this layer + * @param image the image the image from which the graphics came + * @param flags the flags that were used to save this layer + */ + Layer(Graphics2D graphics, BufferedImage image, int flags) { + mGraphics = graphics; + mBitmap = null; + mImage = image; + mFlags = flags; + } + + /** The Graphics2D, guaranteed to be non null */ + Graphics2D getGraphics() { + return mGraphics; + } + + /** The BufferedImage, guaranteed to be non null */ + BufferedImage getImage() { + return mImage; + } + + /** Returns the layer save flags. This is only valid for additional layers. + * For the base layer this will always return 0; + * For a given layer, all further copies of this {@link Layer} object in new snapshots + * will always return the same value. + */ + int getFlags() { + return mFlags; + } + + Layer makeCopy() { + if (mBitmap != null) { + return new Layer((Graphics2D) mGraphics.create(), mBitmap); + } + + return new Layer((Graphics2D) mGraphics.create(), mImage, mFlags); + } + + /** sets an optional copy of the original content to be used during restore */ + void setOriginalCopy(BufferedImage image) { + mOriginalCopy = image; + } + + BufferedImage getOriginalCopy() { + return mOriginalCopy; + } + + void change() { + if (mBitmap != null) { + mBitmap.change(); + } + } + + /** + * Sets the clip for the graphics2D object associated with the layer. + * This should be used over the normal Graphics2D setClip method. + * + * @param clipShape the shape to use a the clip shape. + */ + void setClip(Shape clipShape) { + // because setClip is only guaranteed to work with rectangle shape, + // first reset the clip to max and then intersect the current (empty) + // clip with the shap. + mGraphics.setClip(null); + mGraphics.clip(clipShape); + } + + /** + * Clips the layer with the given shape. This performs an intersect between the current + * clip shape and the given shape. + * @param shape the new clip shape. + */ + public void clip(Shape shape) { + mGraphics.clip(shape); + } + } + + /** + * Creates the root snapshot associating it with a given bitmap. + * <p> + * If <var>bitmap</var> is null, then {@link GcSnapshot#setBitmap(Bitmap_Delegate)} must be + * called before the snapshot can be used to draw. Transform and clip operations are permitted + * before. + * + * @param image the image to associate to the snapshot or null. + * @return the root snapshot + */ + public static GcSnapshot createDefaultSnapshot(Bitmap_Delegate bitmap) { + GcSnapshot snapshot = new GcSnapshot(); + if (bitmap != null) { + snapshot.setBitmap(bitmap); + } + + return snapshot; + } + + /** + * Saves the current state according to the given flags and returns the new current snapshot. + * <p/> + * This is the equivalent of {@link Canvas#save(int)} + * + * @param flags the save flags. + * @return the new snapshot + * + * @see Canvas#save(int) + */ + public GcSnapshot save(int flags) { + return new GcSnapshot(this, null /*layerbounds*/, null /*paint*/, flags); + } + + /** + * Saves the current state and creates a new layer, and returns the new current snapshot. + * <p/> + * This is the equivalent of {@link Canvas#saveLayer(RectF, Paint, int)} + * + * @param layerBounds the layer bounds + * @param paint the Paint information used to blit the layer back into the layers underneath + * upon restore + * @param flags the save flags. + * @return the new snapshot + * + * @see Canvas#saveLayer(RectF, Paint, int) + */ + public GcSnapshot saveLayer(RectF layerBounds, Paint_Delegate paint, int flags) { + return new GcSnapshot(this, layerBounds, paint, flags); + } + + /** + * Creates the root snapshot. + * {@link #setGraphics2D(Graphics2D)} will have to be called on it when possible. + */ + private GcSnapshot() { + mPrevious = null; + mFlags = 0; + mLocalLayer = null; + mLocalLayerPaint = null; + mLayerBounds = null; + } + + /** + * Creates a new {@link GcSnapshot} on top of another one, with a layer data to be restored + * into the main graphics when {@link #restore()} is called. + * + * @param previous the previous snapshot head. + * @param layerBounds the region of the layer. Optional, if null, this is a normal save() + * @param paint the Paint information used to blit the layer back into the layers underneath + * upon restore + * @param flags the flags regarding what should be saved. + */ + private GcSnapshot(GcSnapshot previous, RectF layerBounds, Paint_Delegate paint, int flags) { + assert previous != null; + mPrevious = previous; + mFlags = flags; + + // make a copy of the current layers before adding the new one. + // This keeps the same BufferedImage reference but creates new Graphics2D for this + // snapshot. + // It does not copy whatever original copy the layers have, as they will be done + // only if the new layer doesn't clip drawing to itself. + for (Layer layer : mPrevious.mLayers) { + mLayers.add(layer.makeCopy()); + } + + if (layerBounds != null) { + // get the current transform + AffineTransform matrix = mLayers.get(0).getGraphics().getTransform(); + + // transform the layerBounds with the current transform and stores it into a int rect + RectF rect2 = new RectF(); + mapRect(matrix, rect2, layerBounds); + mLayerBounds = new Rect(); + rect2.round(mLayerBounds); + + // get the base layer (always at index 0) + Layer baseLayer = mLayers.get(0); + + // create the image for the layer + BufferedImage layerImage = new BufferedImage( + baseLayer.getImage().getWidth(), + baseLayer.getImage().getHeight(), + (mFlags & Canvas.HAS_ALPHA_LAYER_SAVE_FLAG) != 0 ? + BufferedImage.TYPE_INT_ARGB : + BufferedImage.TYPE_INT_RGB); + + // create a graphics for it so that drawing can be done. + Graphics2D layerGraphics = layerImage.createGraphics(); + + // because this layer inherits the current context for transform and clip, + // set them to one from the base layer. + AffineTransform currentMtx = baseLayer.getGraphics().getTransform(); + layerGraphics.setTransform(currentMtx); + + // create a new layer for this new layer and add it to the list at the end. + mLayers.add(mLocalLayer = new Layer(layerGraphics, layerImage, flags)); + + // set the clip on it. + Shape currentClip = baseLayer.getGraphics().getClip(); + mLocalLayer.setClip(currentClip); + + // if the drawing is not clipped to the local layer only, we save the current content + // of all other layers. We are only interested in the part that will actually + // be drawn, so we create as small bitmaps as we can. + // This is so that we can erase the drawing that goes in the layers below that will + // be coming from the layer itself. + if ((mFlags & Canvas.CLIP_TO_LAYER_SAVE_FLAG) == 0) { + int w = mLayerBounds.width(); + int h = mLayerBounds.height(); + for (int i = 0 ; i < mLayers.size() - 1 ; i++) { + Layer layer = mLayers.get(i); + BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = image.createGraphics(); + graphics.drawImage(layer.getImage(), + 0, 0, w, h, + mLayerBounds.left, mLayerBounds.top, + mLayerBounds.right, mLayerBounds.bottom, + null); + graphics.dispose(); + layer.setOriginalCopy(image); + } + } + } else { + mLocalLayer = null; + mLayerBounds = null; + } + + mLocalLayerPaint = paint; + } + + public void dispose() { + for (Layer layer : mLayers) { + layer.getGraphics().dispose(); + } + + if (mPrevious != null) { + mPrevious.dispose(); + } + } + + /** + * Restores the top {@link GcSnapshot}, and returns the next one. + */ + public GcSnapshot restore() { + return doRestore(); + } + + /** + * Restores the {@link GcSnapshot} to <var>saveCount</var>. + * @param saveCount the saveCount or -1 to only restore 1. + * + * @return the new head of the Gc snapshot stack. + */ + public GcSnapshot restoreTo(int saveCount) { + return doRestoreTo(size(), saveCount); + } + + public int size() { + if (mPrevious != null) { + return mPrevious.size() + 1; + } + + return 1; + } + + /** + * Link the snapshot to a Bitmap_Delegate. + * <p/> + * This is only for the case where the snapshot was created with a null image when calling + * {@link #createDefaultSnapshot(Bitmap_Delegate)}, and is therefore not yet linked to + * a previous snapshot. + * <p/> + * If any transform or clip information was set before, they are put into the Graphics object. + * @param bitmap the bitmap to link to. + */ + public void setBitmap(Bitmap_Delegate bitmap) { + // create a new Layer for the bitmap. This will be the base layer. + Graphics2D graphics2D = bitmap.getImage().createGraphics(); + Layer baseLayer = new Layer(graphics2D, bitmap); + + // Set the current transform and clip which can either come from mTransform/mClip if they + // were set when there was no bitmap/layers or from the current base layers if there is + // one already. + + graphics2D.setTransform(getTransform()); + // reset mTransform in case there was one. + mTransform = null; + + baseLayer.setClip(getClip()); + // reset mClip in case there was one. + mClip = null; + + // replace whatever current layers we have with this. + mLayers.clear(); + mLayers.add(baseLayer); + + } + + public void translate(float dx, float dy) { + if (mLayers.size() > 0) { + for (Layer layer : mLayers) { + layer.getGraphics().translate(dx, dy); + } + } else { + if (mTransform == null) { + mTransform = new AffineTransform(); + } + mTransform.translate(dx, dy); + } + } + + public void rotate(double radians) { + if (mLayers.size() > 0) { + for (Layer layer : mLayers) { + layer.getGraphics().rotate(radians); + } + } else { + if (mTransform == null) { + mTransform = new AffineTransform(); + } + mTransform.rotate(radians); + } + } + + public void scale(float sx, float sy) { + if (mLayers.size() > 0) { + for (Layer layer : mLayers) { + layer.getGraphics().scale(sx, sy); + } + } else { + if (mTransform == null) { + mTransform = new AffineTransform(); + } + mTransform.scale(sx, sy); + } + } + + public AffineTransform getTransform() { + if (mLayers.size() > 0) { + // all graphics2D in the list have the same transform + return mLayers.get(0).getGraphics().getTransform(); + } else { + if (mTransform == null) { + mTransform = new AffineTransform(); + } + return mTransform; + } + } + + public void setTransform(AffineTransform transform) { + if (mLayers.size() > 0) { + for (Layer layer : mLayers) { + layer.getGraphics().setTransform(transform); + } + } else { + if (mTransform == null) { + mTransform = new AffineTransform(); + } + mTransform.setTransform(transform); + } + } + + public boolean clip(Shape shape, int regionOp) { + // Simple case of intersect with existing layers. + // Because Graphics2D#setClip works a bit peculiarly, we optimize + // the case of clipping by intersection, as it's supported natively. + if (regionOp == Region.Op.INTERSECT.nativeInt && mLayers.size() > 0) { + for (Layer layer : mLayers) { + layer.clip(shape); + } + + Shape currentClip = getClip(); + return currentClip != null && currentClip.getBounds().isEmpty() == false; + } + + Area area = null; + + if (regionOp == Region.Op.REPLACE.nativeInt) { + area = new Area(shape); + } else { + area = Region_Delegate.combineShapes(getClip(), shape, regionOp); + } + + assert area != null; + + if (mLayers.size() > 0) { + if (area != null) { + for (Layer layer : mLayers) { + layer.setClip(area); + } + } + + Shape currentClip = getClip(); + return currentClip != null && currentClip.getBounds().isEmpty() == false; + } else { + if (area != null) { + mClip = area; + } else { + mClip = new Area(); + } + + return mClip.getBounds().isEmpty() == false; + } + } + + public boolean clipRect(float left, float top, float right, float bottom, int regionOp) { + return clip(new Rectangle2D.Float(left, top, right - left, bottom - top), regionOp); + } + + /** + * Returns the current clip, or null if none have been setup. + */ + public Shape getClip() { + if (mLayers.size() > 0) { + // they all have the same clip + return mLayers.get(0).getGraphics().getClip(); + } else { + return mClip; + } + } + + private GcSnapshot doRestoreTo(int size, int saveCount) { + if (size <= saveCount) { + return this; + } + + // restore the current one first. + GcSnapshot previous = doRestore(); + + if (size == saveCount + 1) { // this was the only one that needed restore. + return previous; + } else { + return previous.doRestoreTo(size - 1, saveCount); + } + } + + /** + * Executes the Drawable's draw method, with a null paint delegate. + * <p/> + * Note that the method can be called several times if there are more than one active layer. + * @param drawable + */ + public void draw(Drawable drawable) { + draw(drawable, null, false /*compositeOnly*/, false /*forceSrcMode*/); + } + + /** + * Executes the Drawable's draw method. + * <p/> + * Note that the method can be called several times if there are more than one active layer. + * @param drawable + * @param paint + * @param compositeOnly whether the paint is used for composite only. This is typically + * the case for bitmaps. + * @param forceSrcMode if true, this overrides the composite to be SRC + */ + public void draw(Drawable drawable, Paint_Delegate paint, boolean compositeOnly, + boolean forceSrcMode) { + // the current snapshot may not have a mLocalLayer (ie it was created on save() instead + // of saveLayer(), but that doesn't mean there's no layer. + // mLayers however saves all the information we need (flags). + if (mLayers.size() == 1) { + // no layer, only base layer. easy case. + drawInLayer(mLayers.get(0), drawable, paint, compositeOnly, forceSrcMode); + } else { + // draw in all the layers until the layer save flags tells us to stop (ie drawing + // in that layer is limited to the layer itself. + int flags; + int i = mLayers.size() - 1; + + do { + Layer layer = mLayers.get(i); + + drawInLayer(layer, drawable, paint, compositeOnly, forceSrcMode); + + // then go to previous layer, only if there are any left, and its flags + // doesn't restrict drawing to the layer itself. + i--; + flags = layer.getFlags(); + } while (i >= 0 && (flags & Canvas.CLIP_TO_LAYER_SAVE_FLAG) == 0); + } + } + + private void drawInLayer(Layer layer, Drawable drawable, Paint_Delegate paint, + boolean compositeOnly, boolean forceSrcMode) { + Graphics2D originalGraphics = layer.getGraphics(); + // get a Graphics2D object configured with the drawing parameters. + Graphics2D configuredGraphics2D = + paint != null ? + createCustomGraphics(originalGraphics, paint, compositeOnly, forceSrcMode) : + (Graphics2D) originalGraphics.create(); + + try { + drawable.draw(configuredGraphics2D, paint); + layer.change(); + } finally { + // dispose Graphics2D object + configuredGraphics2D.dispose(); + } + } + + private GcSnapshot doRestore() { + if (mPrevious != null) { + if (mLocalLayer != null) { + // prepare to blit the layers in which we have draw, in the layer beneath + // them, starting with the top one (which is the current local layer). + int i = mLayers.size() - 1; + int flags; + do { + Layer dstLayer = mLayers.get(i - 1); + + restoreLayer(dstLayer); + + flags = dstLayer.getFlags(); + i--; + } while (i > 0 && (flags & Canvas.CLIP_TO_LAYER_SAVE_FLAG) == 0); + } + + // if this snapshot does not save everything, then set the previous snapshot + // to this snapshot content + + // didn't save the matrix? set the current matrix on the previous snapshot + if ((mFlags & Canvas.MATRIX_SAVE_FLAG) == 0) { + AffineTransform mtx = getTransform(); + for (Layer layer : mPrevious.mLayers) { + layer.getGraphics().setTransform(mtx); + } + } + + // didn't save the clip? set the current clip on the previous snapshot + if ((mFlags & Canvas.CLIP_SAVE_FLAG) == 0) { + Shape clip = getClip(); + for (Layer layer : mPrevious.mLayers) { + layer.setClip(clip); + } + } + } + + for (Layer layer : mLayers) { + layer.getGraphics().dispose(); + } + + return mPrevious; + } + + private void restoreLayer(Layer dstLayer) { + + Graphics2D baseGfx = dstLayer.getImage().createGraphics(); + + // if the layer contains an original copy this means the flags + // didn't restrict drawing to the local layer and we need to make sure the + // layer bounds in the layer beneath didn't receive any drawing. + // so we use the originalCopy to erase the new drawings in there. + BufferedImage originalCopy = dstLayer.getOriginalCopy(); + if (originalCopy != null) { + Graphics2D g = (Graphics2D) baseGfx.create(); + g.setComposite(AlphaComposite.Src); + + g.drawImage(originalCopy, + mLayerBounds.left, mLayerBounds.top, mLayerBounds.right, mLayerBounds.bottom, + 0, 0, mLayerBounds.width(), mLayerBounds.height(), + null); + g.dispose(); + } + + // now draw put the content of the local layer onto the layer, + // using the paint information + Graphics2D g = createCustomGraphics(baseGfx, mLocalLayerPaint, + true /*alphaOnly*/, false /*forceSrcMode*/); + + g.drawImage(mLocalLayer.getImage(), + mLayerBounds.left, mLayerBounds.top, mLayerBounds.right, mLayerBounds.bottom, + mLayerBounds.left, mLayerBounds.top, mLayerBounds.right, mLayerBounds.bottom, + null); + g.dispose(); + + baseGfx.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 createCustomGraphics(Graphics2D original, Paint_Delegate paint, + boolean compositeOnly, boolean forceSrcMode) { + // make new one graphics + Graphics2D g = (Graphics2D) original.create(); + + // configure it + + if (paint.isAntiAliased()) { + g.setRenderingHint( + RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint( + RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + } + + boolean customShader = false; + + // get the shader first, as it'll replace the color if it can be used it. + if (compositeOnly == false) { + Shader_Delegate shaderDelegate = paint.getShader(); + if (shaderDelegate != null) { + if (shaderDelegate.isSupported()) { + java.awt.Paint shaderPaint = shaderDelegate.getJavaPaint(); + assert shaderPaint != null; + if (shaderPaint != null) { + g.setPaint(shaderPaint); + customShader = true; + } + } else { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_SHADER, + shaderDelegate.getSupportMessage(), + null /*throwable*/, null /*data*/); + } + } + + // if no shader, use the paint color + if (customShader == false) { + g.setColor(new Color(paint.getColor(), true /*hasAlpha*/)); + } + + // set the stroke + g.setStroke(paint.getJavaStroke()); + } + + // the alpha for the composite. Always opaque if the normal paint color is used since + // it contains the alpha + int alpha = (compositeOnly || customShader) ? paint.getAlpha() : 0xFF; + + if (forceSrcMode) { + g.setComposite(AlphaComposite.getInstance( + AlphaComposite.SRC, (float) alpha / 255.f)); + } else { + boolean customXfermode = false; + Xfermode_Delegate xfermodeDelegate = paint.getXfermode(); + if (xfermodeDelegate != null) { + if (xfermodeDelegate.isSupported()) { + Composite composite = xfermodeDelegate.getComposite(alpha); + assert composite != null; + if (composite != null) { + g.setComposite(composite); + customXfermode = true; + } + } else { + Bridge.getLog().fidelityWarning(LayoutLog.TAG_XFERMODE, + xfermodeDelegate.getSupportMessage(), + null /*throwable*/, null /*data*/); + } + } + + // if there was no custom xfermode, but we have alpha (due to a shader and a non + // opaque alpha channel in the paint color), then we create an AlphaComposite anyway + // that will handle the alpha. + if (customXfermode == false && alpha != 0xFF) { + g.setComposite(AlphaComposite.getInstance( + AlphaComposite.SRC_OVER, (float) alpha / 255.f)); + } + } + + return g; + } + + private void mapRect(AffineTransform matrix, RectF dst, RectF src) { + // array with 4 corners + float[] corners = new float[] { + src.left, src.top, + src.right, src.top, + src.right, src.bottom, + src.left, src.bottom, + }; + + // apply the transform to them. + matrix.transform(corners, 0, corners, 0, 4); + + // now put the result in the rect. We take the min/max of Xs and min/max of Ys + dst.left = Math.min(Math.min(corners[0], corners[2]), Math.min(corners[4], corners[6])); + dst.right = Math.max(Math.max(corners[0], corners[2]), Math.max(corners[4], corners[6])); + + dst.top = Math.min(Math.min(corners[1], corners[3]), Math.min(corners[5], corners[7])); + dst.bottom = Math.max(Math.max(corners[1], corners[3]), Math.max(corners[5], corners[7])); + } + +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java new file mode 100644 index 000000000000..803849fd696a --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ParserFactory.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl; + + +import org.kxml2.io.KXmlParser; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * A factory for {@link XmlPullParser}. + * + */ +public class ParserFactory { + + private final static String ENCODING = "UTF-8"; //$NON-NLS-1$ + + public final static boolean LOG_PARSER = false; + + public static XmlPullParser create(File f) + throws XmlPullParserException, FileNotFoundException { + InputStream stream = new FileInputStream(f); + return create(stream, f.getName(), f.length()); + } + + public static XmlPullParser create(InputStream stream, String name) + throws XmlPullParserException { + return create(stream, name, -1); + } + + private static XmlPullParser create(InputStream stream, String name, long size) + throws XmlPullParserException { + KXmlParser parser = instantiateParser(name); + + stream = readAndClose(stream, name, size); + + parser.setInput(stream, ENCODING); + return parser; + } + + private static KXmlParser instantiateParser(String name) throws XmlPullParserException { + KXmlParser parser; + if (name != null) { + parser = new CustomParser(name); + } else { + parser = new KXmlParser(); + } + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + return parser; + } + + private static InputStream readAndClose(InputStream stream, String name, long size) + throws XmlPullParserException { + // just a sanity check. It's doubtful we'll have such big files! + if (size > Integer.MAX_VALUE) { + throw new XmlPullParserException("File " + name + " is too big to be parsed"); + } + int intSize = (int) size; + + // create a buffered reader to facilitate reading. + BufferedInputStream bufferedStream = new BufferedInputStream(stream); + try { + int avail; + if (intSize != -1) { + avail = intSize; + } else { + // get the size to read. + avail = bufferedStream.available(); + } + + // create the initial buffer and read it. + byte[] buffer = new byte[avail]; + int read = stream.read(buffer); + + // this is the easy case. + if (read == intSize) { + return new ByteArrayInputStream(buffer); + } + + // check if there is more to read (read() does not necessarily read all that + // available() returned!) + while ((avail = bufferedStream.available()) > 0) { + if (read + avail > buffer.length) { + // just allocate what is needed. We're mostly reading small files + // so it shouldn't be too problematic. + byte[] moreBuffer = new byte[read + avail]; + System.arraycopy(buffer, 0, moreBuffer, 0, read); + buffer = moreBuffer; + } + + read += stream.read(buffer, read, avail); + } + + // return a new stream encapsulating this buffer. + return new ByteArrayInputStream(buffer); + + } catch (IOException e) { + throw new XmlPullParserException("Failed to read " + name, null, e); + } finally { + try { + bufferedStream.close(); + } catch (IOException e) { + } + } + } + + private static class CustomParser extends KXmlParser { + private final String mName; + + CustomParser(String name) { + super(); + mName = name; + } + + @Override + public String toString() { + return mName; + } + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/PlayAnimationThread.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/PlayAnimationThread.java new file mode 100644 index 000000000000..7b701802b9a5 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/PlayAnimationThread.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl; + +import com.android.ide.common.rendering.api.IAnimationListener; +import com.android.ide.common.rendering.api.Result; +import com.android.ide.common.rendering.api.Result.Status; + +import android.animation.AnimationThread; +import android.animation.Animator; + +public class PlayAnimationThread extends AnimationThread { + + private final Animator mAnimator; + + public PlayAnimationThread(Animator animator, RenderSessionImpl scene, String animName, + IAnimationListener listener) { + super(scene, animName, listener); + mAnimator = animator; + } + + @Override + public Result preAnimation() { + // start the animation. This will send a message to the handler right away, so + // the queue is filled when this method returns. + mAnimator.start(); + + return Status.SUCCESS.createResult(); + } + + @Override + public void postAnimation() { + // nothing to be done. + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java new file mode 100644 index 000000000000..b909bec6299e --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl; + +import static com.android.ide.common.rendering.api.Result.Status.ERROR_LOCK_INTERRUPTED; +import static com.android.ide.common.rendering.api.Result.Status.ERROR_TIMEOUT; +import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; + +import com.android.ide.common.rendering.api.HardwareConfig; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.rendering.api.RenderParams; +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.RenderResources.FrameworkResourceIdProvider; +import com.android.ide.common.rendering.api.Result; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.resources.Density; +import com.android.resources.ResourceType; +import com.android.resources.ScreenOrientation; +import com.android.resources.ScreenSize; + +import android.content.res.Configuration; +import android.os.HandlerThread_Delegate; +import android.os.Looper; +import android.util.DisplayMetrics; +import android.view.ViewConfiguration_Accessor; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodManager_Accessor; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Base class for rendering action. + * + * It provides life-cycle methods to init and stop the rendering. + * The most important methods are: + * {@link #init(long)} and {@link #acquire(long)} to start a rendering and {@link #release()} + * after the rendering. + * + * + * @param <T> the {@link RenderParams} implementation + * + */ +public abstract class RenderAction<T extends RenderParams> extends FrameworkResourceIdProvider { + + /** + * The current context being rendered. This is set through {@link #acquire(long)} and + * {@link #init(long)}, and unset in {@link #release()}. + */ + private static BridgeContext sCurrentContext = null; + + private final T mParams; + + private BridgeContext mContext; + + /** + * Creates a renderAction. + * <p> + * This <b>must</b> be followed by a call to {@link RenderAction#init()}, which act as a + * call to {@link RenderAction#acquire(long)} + * + * @param params the RenderParams. This must be a copy that the action can keep + * + */ + protected RenderAction(T params) { + mParams = params; + } + + /** + * Initializes and acquires the scene, creating various Android objects such as context, + * inflater, and parser. + * + * @param timeout the time to wait if another rendering is happening. + * + * @return whether the scene was prepared + * + * @see #acquire(long) + * @see #release() + */ + public Result init(long timeout) { + // acquire the lock. if the result is null, lock was just acquired, otherwise, return + // the result. + Result result = acquireLock(timeout); + if (result != null) { + return result; + } + + HardwareConfig hardwareConfig = mParams.getHardwareConfig(); + + // setup the display Metrics. + DisplayMetrics metrics = new DisplayMetrics(); + metrics.densityDpi = metrics.noncompatDensityDpi = + hardwareConfig.getDensity().getDpiValue(); + + metrics.density = metrics.noncompatDensity = + metrics.densityDpi / (float) DisplayMetrics.DENSITY_DEFAULT; + + metrics.scaledDensity = metrics.noncompatScaledDensity = metrics.density; + + metrics.widthPixels = metrics.noncompatWidthPixels = hardwareConfig.getScreenWidth(); + metrics.heightPixels = metrics.noncompatHeightPixels = hardwareConfig.getScreenHeight(); + metrics.xdpi = metrics.noncompatXdpi = hardwareConfig.getXdpi(); + metrics.ydpi = metrics.noncompatYdpi = hardwareConfig.getYdpi(); + + RenderResources resources = mParams.getResources(); + + // build the context + mContext = new BridgeContext(mParams.getProjectKey(), metrics, resources, + mParams.getProjectCallback(), getConfiguration(), mParams.getTargetSdkVersion()); + + setUp(); + + return SUCCESS.createResult(); + } + + + /** + * Prepares the scene for action. + * <p> + * This call is blocking if another rendering/inflating is currently happening, and will return + * whether the preparation worked. + * + * The preparation can fail if another rendering took too long and the timeout was elapsed. + * + * More than one call to this from the same thread will have no effect and will return + * {@link Result#SUCCESS}. + * + * After scene actions have taken place, only one call to {@link #release()} must be + * done. + * + * @param timeout the time to wait if another rendering is happening. + * + * @return whether the scene was prepared + * + * @see #release() + * + * @throws IllegalStateException if {@link #init(long)} was never called. + */ + public Result acquire(long timeout) { + if (mContext == null) { + throw new IllegalStateException("After scene creation, #init() must be called"); + } + + // acquire the lock. if the result is null, lock was just acquired, otherwise, return + // the result. + Result result = acquireLock(timeout); + if (result != null) { + return result; + } + + setUp(); + + return SUCCESS.createResult(); + } + + /** + * Acquire the lock so that the scene can be acted upon. + * <p> + * This returns null if the lock was just acquired, otherwise it returns + * {@link Result#SUCCESS} if the lock already belonged to that thread, or another + * instance (see {@link Result#getStatus()}) if an error occurred. + * + * @param timeout the time to wait if another rendering is happening. + * @return null if the lock was just acquire or another result depending on the state. + * + * @throws IllegalStateException if the current context is different than the one owned by + * the scene. + */ + private Result acquireLock(long timeout) { + ReentrantLock lock = Bridge.getLock(); + if (lock.isHeldByCurrentThread() == false) { + try { + boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS); + + if (acquired == false) { + return ERROR_TIMEOUT.createResult(); + } + } catch (InterruptedException e) { + return ERROR_LOCK_INTERRUPTED.createResult(); + } + } else { + // This thread holds the lock already. Checks that this wasn't for a different context. + // If this is called by init, mContext will be null and so should sCurrentContext + // anyway + if (mContext != sCurrentContext) { + throw new IllegalStateException("Acquiring different scenes from same thread without releases"); + } + return SUCCESS.createResult(); + } + + return null; + } + + /** + * Cleans up the scene after an action. + */ + public void release() { + ReentrantLock lock = Bridge.getLock(); + + // with the use of finally blocks, it is possible to find ourself calling this + // without a successful call to prepareScene. This test makes sure that unlock() will + // not throw IllegalMonitorStateException. + if (lock.isHeldByCurrentThread()) { + tearDown(); + lock.unlock(); + } + } + + /** + * Sets up the session for rendering. + * <p/> + * The counterpart is {@link #tearDown()}. + */ + private void setUp() { + // make sure the Resources object references the context (and other objects) for this + // scene + mContext.initResources(); + sCurrentContext = mContext; + + // create an InputMethodManager + InputMethodManager.getInstance(); + + LayoutLog currentLog = mParams.getLog(); + Bridge.setLog(currentLog); + mContext.getRenderResources().setFrameworkResourceIdProvider(this); + mContext.getRenderResources().setLogger(currentLog); + } + + /** + * Tear down the session after rendering. + * <p/> + * The counterpart is {@link #setUp()}. + */ + private void tearDown() { + // Make sure to remove static references, otherwise we could not unload the lib + mContext.disposeResources(); + + // quit HandlerThread created during this session. + HandlerThread_Delegate.cleanUp(sCurrentContext); + + // clear the stored ViewConfiguration since the map is per density and not per context. + ViewConfiguration_Accessor.clearConfigurations(); + + // remove the InputMethodManager + InputMethodManager_Accessor.resetInstance(); + + sCurrentContext = null; + + Bridge.setLog(null); + mContext.getRenderResources().setFrameworkResourceIdProvider(null); + mContext.getRenderResources().setLogger(null); + } + + public static BridgeContext getCurrentContext() { + return sCurrentContext; + } + + protected T getParams() { + return mParams; + } + + protected BridgeContext getContext() { + return mContext; + } + + /** + * Returns the log associated with the session. + * @return the log or null if there are none. + */ + public LayoutLog getLog() { + if (mParams != null) { + return mParams.getLog(); + } + + return null; + } + + /** + * Checks that the lock is owned by the current thread and that the current context is the one + * from this scene. + * + * @throws IllegalStateException if the current context is different than the one owned by + * the scene, or if {@link #acquire(long)} was not called. + */ + protected void checkLock() { + ReentrantLock lock = Bridge.getLock(); + if (lock.isHeldByCurrentThread() == false) { + throw new IllegalStateException("scene must be acquired first. see #acquire(long)"); + } + if (sCurrentContext != mContext) { + throw new IllegalStateException("Thread acquired a scene but is rendering a different one"); + } + } + + private Configuration getConfiguration() { + Configuration config = new Configuration(); + + HardwareConfig hardwareConfig = mParams.getHardwareConfig(); + + ScreenSize screenSize = hardwareConfig.getScreenSize(); + if (screenSize != null) { + switch (screenSize) { + case SMALL: + config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_SMALL; + break; + case NORMAL: + config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_NORMAL; + break; + case LARGE: + config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_LARGE; + break; + case XLARGE: + config.screenLayout |= Configuration.SCREENLAYOUT_SIZE_XLARGE; + break; + } + } + + Density density = hardwareConfig.getDensity(); + if (density == null) { + density = Density.MEDIUM; + } + + config.screenWidthDp = hardwareConfig.getScreenWidth() / density.getDpiValue(); + config.screenHeightDp = hardwareConfig.getScreenHeight() / density.getDpiValue(); + if (config.screenHeightDp < config.screenWidthDp) { + config.smallestScreenWidthDp = config.screenHeightDp; + } else { + config.smallestScreenWidthDp = config.screenWidthDp; + } + config.densityDpi = density.getDpiValue(); + + // never run in compat mode: + config.compatScreenWidthDp = config.screenWidthDp; + config.compatScreenHeightDp = config.screenHeightDp; + + ScreenOrientation orientation = hardwareConfig.getOrientation(); + if (orientation != null) { + switch (orientation) { + case PORTRAIT: + config.orientation = Configuration.ORIENTATION_PORTRAIT; + break; + case LANDSCAPE: + config.orientation = Configuration.ORIENTATION_LANDSCAPE; + break; + case SQUARE: + config.orientation = Configuration.ORIENTATION_SQUARE; + break; + } + } else { + config.orientation = Configuration.ORIENTATION_UNDEFINED; + } + + // TODO: fill in more config info. + + return config; + } + + + // --- FrameworkResourceIdProvider methods + + @Override + public Integer getId(ResourceType resType, String resName) { + return Bridge.getResourceId(resType, resName); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java new file mode 100644 index 000000000000..b6771315b09b --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl; + +import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; + +import com.android.ide.common.rendering.api.DrawableParams; +import com.android.ide.common.rendering.api.HardwareConfig; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.Result; +import com.android.ide.common.rendering.api.Result.Status; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.resources.ResourceType; + +import android.graphics.Bitmap; +import android.graphics.Bitmap_Delegate; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.view.AttachInfo_Accessor; +import android.view.View.MeasureSpec; +import android.widget.FrameLayout; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.IOException; + +/** + * Action to render a given Drawable provided through {@link DrawableParams#getDrawable()}. + * + * The class only provides a simple {@link #render()} method, but the full life-cycle of the + * action must be respected. + * + * @see RenderAction + * + */ +public class RenderDrawable extends RenderAction<DrawableParams> { + + public RenderDrawable(DrawableParams params) { + super(new DrawableParams(params)); + } + + public Result render() { + checkLock(); + try { + // get the drawable resource value + DrawableParams params = getParams(); + HardwareConfig hardwareConfig = params.getHardwareConfig(); + ResourceValue drawableResource = params.getDrawable(); + + // resolve it + BridgeContext context = getContext(); + drawableResource = context.getRenderResources().resolveResValue(drawableResource); + + if (drawableResource == null || + drawableResource.getResourceType() != ResourceType.DRAWABLE) { + return Status.ERROR_NOT_A_DRAWABLE.createResult(); + } + + // create a simple FrameLayout + FrameLayout content = new FrameLayout(context); + + // get the actual Drawable object to draw + Drawable d = ResourceHelper.getDrawable(drawableResource, context); + content.setBackground(d); + + // set the AttachInfo on the root view. + AttachInfo_Accessor.setAttachInfo(content); + + + // measure + int w = hardwareConfig.getScreenWidth(); + int h = hardwareConfig.getScreenHeight(); + int w_spec = MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY); + int h_spec = MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY); + content.measure(w_spec, h_spec); + + // now do the layout. + content.layout(0, 0, w, h); + + // preDraw setup + AttachInfo_Accessor.dispatchOnPreDraw(content); + + // draw into a new image + BufferedImage image = getImage(w, h); + + // create an Android bitmap around the BufferedImage + Bitmap bitmap = Bitmap_Delegate.createBitmap(image, + true /*isMutable*/, hardwareConfig.getDensity()); + + // create a Canvas around the Android bitmap + Canvas canvas = new Canvas(bitmap); + canvas.setDensity(hardwareConfig.getDensity().getDpiValue()); + + // and draw + content.draw(canvas); + + return Status.SUCCESS.createResult(image); + } catch (IOException e) { + return ERROR_UNKNOWN.createResult(e.getMessage(), e); + } + } + + protected BufferedImage getImage(int w, int h) { + BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + Graphics2D gc = image.createGraphics(); + gc.setComposite(AlphaComposite.Src); + + gc.setColor(new Color(0x00000000, true)); + gc.fillRect(0, 0, w, h); + + // done + gc.dispose(); + + return image; + } + +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java new file mode 100644 index 000000000000..6011fdbabcdd --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java @@ -0,0 +1,1471 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl; + +import static com.android.ide.common.rendering.api.Result.Status.ERROR_ANIM_NOT_FOUND; +import static com.android.ide.common.rendering.api.Result.Status.ERROR_INFLATION; +import static com.android.ide.common.rendering.api.Result.Status.ERROR_NOT_INFLATED; +import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; +import static com.android.ide.common.rendering.api.Result.Status.ERROR_VIEWGROUP_NO_CHILDREN; +import static com.android.ide.common.rendering.api.Result.Status.SUCCESS; + +import com.android.ide.common.rendering.api.AdapterBinding; +import com.android.ide.common.rendering.api.HardwareConfig; +import com.android.ide.common.rendering.api.IAnimationListener; +import com.android.ide.common.rendering.api.ILayoutPullParser; +import com.android.ide.common.rendering.api.IProjectCallback; +import com.android.ide.common.rendering.api.RenderParams; +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.RenderSession; +import com.android.ide.common.rendering.api.ResourceReference; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.Result; +import com.android.ide.common.rendering.api.Result.Status; +import com.android.ide.common.rendering.api.SessionParams; +import com.android.ide.common.rendering.api.SessionParams.RenderingMode; +import com.android.ide.common.rendering.api.ViewInfo; +import com.android.internal.util.XmlUtils; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes; +import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; +import com.android.layoutlib.bridge.bars.FakeActionBar; +import com.android.layoutlib.bridge.bars.NavigationBar; +import com.android.layoutlib.bridge.bars.StatusBar; +import com.android.layoutlib.bridge.bars.TitleBar; +import com.android.layoutlib.bridge.impl.binding.FakeAdapter; +import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter; +import com.android.resources.ResourceType; +import com.android.resources.ScreenOrientation; +import com.android.util.Pair; + +import org.xmlpull.v1.XmlPullParserException; + +import android.animation.AnimationThread; +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.LayoutTransition; +import android.animation.LayoutTransition.TransitionListener; +import android.app.Fragment_Delegate; +import android.graphics.Bitmap; +import android.graphics.Bitmap_Delegate; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.AttachInfo_Accessor; +import android.view.BridgeInflater; +import android.view.IWindowManager; +import android.view.IWindowManagerImpl; +import android.view.Surface; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewGroup.MarginLayoutParams; +import android.view.WindowManagerGlobal_Delegate; +import android.widget.AbsListView; +import android.widget.AbsSpinner; +import android.widget.AdapterView; +import android.widget.ExpandableListView; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.QuickContactBadge; +import android.widget.TabHost; +import android.widget.TabHost.TabSpec; +import android.widget.TabWidget; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Class implementing the render session. + * + * A session is a stateful representation of a layout file. It is initialized with data coming + * through the {@link Bridge} API to inflate the layout. Further actions and rendering can then + * be done on the layout. + * + */ +public class RenderSessionImpl extends RenderAction<SessionParams> { + + private static final int DEFAULT_TITLE_BAR_HEIGHT = 25; + private static final int DEFAULT_STATUS_BAR_HEIGHT = 25; + + // scene state + private RenderSession mScene; + private BridgeXmlBlockParser mBlockParser; + private BridgeInflater mInflater; + private ResourceValue mWindowBackground; + private ViewGroup mViewRoot; + private FrameLayout mContentRoot; + private Canvas mCanvas; + private int mMeasuredScreenWidth = -1; + private int mMeasuredScreenHeight = -1; + private boolean mIsAlphaChannelImage; + private boolean mWindowIsFloating; + + private int mStatusBarSize; + private int mNavigationBarSize; + private int mNavigationBarOrientation = LinearLayout.HORIZONTAL; + private int mTitleBarSize; + private int mActionBarSize; + + + // information being returned through the API + private BufferedImage mImage; + private List<ViewInfo> mViewInfoList; + + private static final class PostInflateException extends Exception { + private static final long serialVersionUID = 1L; + + public PostInflateException(String message) { + super(message); + } + } + + /** + * Creates a layout scene with all the information coming from the layout bridge API. + * <p> + * This <b>must</b> be followed by a call to {@link RenderSessionImpl#init()}, which act as a + * call to {@link RenderSessionImpl#acquire(long)} + * + * @see LayoutBridge#createScene(com.android.layoutlib.api.SceneParams) + */ + public RenderSessionImpl(SessionParams params) { + super(new SessionParams(params)); + } + + /** + * Initializes and acquires the scene, creating various Android objects such as context, + * inflater, and parser. + * + * @param timeout the time to wait if another rendering is happening. + * + * @return whether the scene was prepared + * + * @see #acquire(long) + * @see #release() + */ + @Override + public Result init(long timeout) { + Result result = super.init(timeout); + if (result.isSuccess() == false) { + return result; + } + + SessionParams params = getParams(); + BridgeContext context = getContext(); + + RenderResources resources = getParams().getResources(); + DisplayMetrics metrics = getContext().getMetrics(); + + // use default of true in case it's not found to use alpha by default + mIsAlphaChannelImage = getBooleanThemeValue(resources, + "windowIsFloating", true /*defaultValue*/); + + mWindowIsFloating = getBooleanThemeValue(resources, "windowIsFloating", + true /*defaultValue*/); + + findBackground(resources); + findStatusBar(resources, metrics); + findActionBar(resources, metrics); + findNavigationBar(resources, metrics); + + // FIXME: find those out, and possibly add them to the render params + boolean hasNavigationBar = true; + IWindowManager iwm = new IWindowManagerImpl(getContext().getConfiguration(), + metrics, Surface.ROTATION_0, + hasNavigationBar); + WindowManagerGlobal_Delegate.setWindowManagerService(iwm); + + // build the inflater and parser. + mInflater = new BridgeInflater(context, params.getProjectCallback()); + context.setBridgeInflater(mInflater); + + mBlockParser = new BridgeXmlBlockParser( + params.getLayoutDescription(), context, false /* platformResourceFlag */); + + return SUCCESS.createResult(); + } + + /** + * Inflates the layout. + * <p> + * {@link #acquire(long)} must have been called before this. + * + * @throws IllegalStateException if the current context is different than the one owned by + * the scene, or if {@link #init(long)} was not called. + */ + public Result inflate() { + checkLock(); + + try { + + SessionParams params = getParams(); + HardwareConfig hardwareConfig = params.getHardwareConfig(); + BridgeContext context = getContext(); + + + // the view group that receives the window background. + ViewGroup backgroundView = null; + + if (mWindowIsFloating || params.isForceNoDecor()) { + backgroundView = mViewRoot = mContentRoot = new FrameLayout(context); + } else { + if (hasSoftwareButtons() && mNavigationBarOrientation == LinearLayout.VERTICAL) { + /* + * This is a special case where the navigation bar is on the right. + +-------------------------------------------------+---+ + | Status bar (always) | | + +-------------------------------------------------+ | + | (Layout with background drawable) | | + | +---------------------------------------------+ | | + | | Title/Action bar (optional) | | | + | +---------------------------------------------+ | | + | | Content, vertical extending | | | + | | | | | + | +---------------------------------------------+ | | + +-------------------------------------------------+---+ + + So we create a horizontal layout, with the nav bar on the right, + and the left part is the normal layout below without the nav bar at + the bottom + */ + LinearLayout topLayout = new LinearLayout(context); + mViewRoot = topLayout; + topLayout.setOrientation(LinearLayout.HORIZONTAL); + + try { + NavigationBar navigationBar = new NavigationBar(context, + hardwareConfig.getDensity(), LinearLayout.VERTICAL); + navigationBar.setLayoutParams( + new LinearLayout.LayoutParams( + mNavigationBarSize, + LayoutParams.MATCH_PARENT)); + topLayout.addView(navigationBar); + } catch (XmlPullParserException e) { + + } + } + + /* + * we're creating the following layout + * + +-------------------------------------------------+ + | Status bar (always) | + +-------------------------------------------------+ + | (Layout with background drawable) | + | +---------------------------------------------+ | + | | Title/Action bar (optional) | | + | +---------------------------------------------+ | + | | Content, vertical extending | | + | | | | + | +---------------------------------------------+ | + +-------------------------------------------------+ + | Navigation bar for soft buttons, maybe see above| + +-------------------------------------------------+ + + */ + + LinearLayout topLayout = new LinearLayout(context); + topLayout.setOrientation(LinearLayout.VERTICAL); + // if we don't already have a view root this is it + if (mViewRoot == null) { + mViewRoot = topLayout; + } else { + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); + layoutParams.weight = 1; + topLayout.setLayoutParams(layoutParams); + + // this is the case of soft buttons + vertical bar. + // this top layout is the first layout in the horizontal layout. see above) + mViewRoot.addView(topLayout, 0); + } + + if (mStatusBarSize > 0) { + // system bar + try { + StatusBar systemBar = new StatusBar(context, hardwareConfig.getDensity()); + systemBar.setLayoutParams( + new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, mStatusBarSize)); + topLayout.addView(systemBar); + } catch (XmlPullParserException e) { + + } + } + + LinearLayout backgroundLayout = new LinearLayout(context); + backgroundView = backgroundLayout; + backgroundLayout.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + layoutParams.weight = 1; + backgroundLayout.setLayoutParams(layoutParams); + topLayout.addView(backgroundLayout); + + + // if the theme says no title/action bar, then the size will be 0 + if (mActionBarSize > 0) { + try { + FakeActionBar actionBar = new FakeActionBar(context, + hardwareConfig.getDensity(), + params.getAppLabel(), params.getAppIcon()); + actionBar.setLayoutParams( + new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, mActionBarSize)); + backgroundLayout.addView(actionBar); + } catch (XmlPullParserException e) { + + } + } else if (mTitleBarSize > 0) { + try { + TitleBar titleBar = new TitleBar(context, + hardwareConfig.getDensity(), params.getAppLabel()); + titleBar.setLayoutParams( + new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, mTitleBarSize)); + backgroundLayout.addView(titleBar); + } catch (XmlPullParserException e) { + + } + } + + // content frame + mContentRoot = new FrameLayout(context); + layoutParams = new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + layoutParams.weight = 1; + mContentRoot.setLayoutParams(layoutParams); + backgroundLayout.addView(mContentRoot); + + if (mNavigationBarOrientation == LinearLayout.HORIZONTAL && + mNavigationBarSize > 0) { + // system bar + try { + NavigationBar navigationBar = new NavigationBar(context, + hardwareConfig.getDensity(), LinearLayout.HORIZONTAL); + navigationBar.setLayoutParams( + new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, mNavigationBarSize)); + topLayout.addView(navigationBar); + } catch (XmlPullParserException e) { + + } + } + } + + + // Sets the project callback (custom view loader) to the fragment delegate so that + // it can instantiate the custom Fragment. + Fragment_Delegate.setProjectCallback(params.getProjectCallback()); + + View view = mInflater.inflate(mBlockParser, mContentRoot); + + // done with the parser, pop it. + context.popParser(); + + Fragment_Delegate.setProjectCallback(null); + + // set the AttachInfo on the root view. + AttachInfo_Accessor.setAttachInfo(mViewRoot); + + // post-inflate process. For now this supports TabHost/TabWidget + postInflateProcess(view, params.getProjectCallback()); + + // get the background drawable + if (mWindowBackground != null && backgroundView != null) { + Drawable d = ResourceHelper.getDrawable(mWindowBackground, context); + backgroundView.setBackground(d); + } + + return SUCCESS.createResult(); + } catch (PostInflateException e) { + return ERROR_INFLATION.createResult(e.getMessage(), e); + } catch (Throwable e) { + // get the real cause of the exception. + Throwable t = e; + while (t.getCause() != null) { + t = t.getCause(); + } + + return ERROR_INFLATION.createResult(t.getMessage(), t); + } + } + + /** + * Renders the scene. + * <p> + * {@link #acquire(long)} must have been called before this. + * + * @param freshRender whether the render is a new one and should erase the existing bitmap (in + * the case where bitmaps are reused). This is typically needed when not playing + * animations.) + * + * @throws IllegalStateException if the current context is different than the one owned by + * the scene, or if {@link #acquire(long)} was not called. + * + * @see RenderParams#getRenderingMode() + * @see RenderSession#render(long) + */ + public Result render(boolean freshRender) { + checkLock(); + + SessionParams params = getParams(); + + try { + if (mViewRoot == null) { + return ERROR_NOT_INFLATED.createResult(); + } + + RenderingMode renderingMode = params.getRenderingMode(); + HardwareConfig hardwareConfig = params.getHardwareConfig(); + + // only do the screen measure when needed. + boolean newRenderSize = false; + if (mMeasuredScreenWidth == -1) { + newRenderSize = true; + mMeasuredScreenWidth = hardwareConfig.getScreenWidth(); + mMeasuredScreenHeight = hardwareConfig.getScreenHeight(); + + if (renderingMode != RenderingMode.NORMAL) { + int widthMeasureSpecMode = renderingMode.isHorizExpand() ? + MeasureSpec.UNSPECIFIED // this lets us know the actual needed size + : MeasureSpec.EXACTLY; + int heightMeasureSpecMode = renderingMode.isVertExpand() ? + MeasureSpec.UNSPECIFIED // this lets us know the actual needed size + : MeasureSpec.EXACTLY; + + // We used to compare the measured size of the content to the screen size but + // this does not work anymore due to the 2 following issues: + // - If the content is in a decor (system bar, title/action bar), the root view + // will not resize even with the UNSPECIFIED because of the embedded layout. + // - If there is no decor, but a dialog frame, then the dialog padding prevents + // comparing the size of the content to the screen frame (as it would not + // take into account the dialog padding). + + // The solution is to first get the content size in a normal rendering, inside + // the decor or the dialog padding. + // Then measure only the content with UNSPECIFIED to see the size difference + // and apply this to the screen size. + + // first measure the full layout, with EXACTLY to get the size of the + // content as it is inside the decor/dialog + Pair<Integer, Integer> exactMeasure = measureView( + mViewRoot, mContentRoot.getChildAt(0), + mMeasuredScreenWidth, MeasureSpec.EXACTLY, + mMeasuredScreenHeight, MeasureSpec.EXACTLY); + + // now measure the content only using UNSPECIFIED (where applicable, based on + // the rendering mode). This will give us the size the content needs. + Pair<Integer, Integer> result = measureView( + mContentRoot, mContentRoot.getChildAt(0), + mMeasuredScreenWidth, widthMeasureSpecMode, + mMeasuredScreenHeight, heightMeasureSpecMode); + + // now look at the difference and add what is needed. + if (renderingMode.isHorizExpand()) { + int measuredWidth = exactMeasure.getFirst(); + int neededWidth = result.getFirst(); + if (neededWidth > measuredWidth) { + mMeasuredScreenWidth += neededWidth - measuredWidth; + } + } + + if (renderingMode.isVertExpand()) { + int measuredHeight = exactMeasure.getSecond(); + int neededHeight = result.getSecond(); + if (neededHeight > measuredHeight) { + mMeasuredScreenHeight += neededHeight - measuredHeight; + } + } + } + } + + // measure again with the size we need + // This must always be done before the call to layout + measureView(mViewRoot, null /*measuredView*/, + mMeasuredScreenWidth, MeasureSpec.EXACTLY, + mMeasuredScreenHeight, MeasureSpec.EXACTLY); + + // now do the layout. + mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); + + if (params.isLayoutOnly()) { + // delete the canvas and image to reset them on the next full rendering + mImage = null; + mCanvas = null; + } else { + AttachInfo_Accessor.dispatchOnPreDraw(mViewRoot); + + // draw the views + // create the BufferedImage into which the layout will be rendered. + boolean newImage = false; + if (newRenderSize || mCanvas == null) { + if (params.getImageFactory() != null) { + mImage = params.getImageFactory().getImage( + mMeasuredScreenWidth, + mMeasuredScreenHeight); + } else { + mImage = new BufferedImage( + mMeasuredScreenWidth, + mMeasuredScreenHeight, + BufferedImage.TYPE_INT_ARGB); + newImage = true; + } + + if (params.isBgColorOverridden()) { + // since we override the content, it's the same as if it was a new image. + newImage = true; + Graphics2D gc = mImage.createGraphics(); + gc.setColor(new Color(params.getOverrideBgColor(), true)); + gc.setComposite(AlphaComposite.Src); + gc.fillRect(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); + gc.dispose(); + } + + // create an Android bitmap around the BufferedImage + Bitmap bitmap = Bitmap_Delegate.createBitmap(mImage, + true /*isMutable*/, hardwareConfig.getDensity()); + + // create a Canvas around the Android bitmap + mCanvas = new Canvas(bitmap); + mCanvas.setDensity(hardwareConfig.getDensity().getDpiValue()); + } + + if (freshRender && newImage == false) { + Graphics2D gc = mImage.createGraphics(); + gc.setComposite(AlphaComposite.Src); + + gc.setColor(new Color(0x00000000, true)); + gc.fillRect(0, 0, + mMeasuredScreenWidth, mMeasuredScreenHeight); + + // done + gc.dispose(); + } + + mViewRoot.draw(mCanvas); + } + + mViewInfoList = startVisitingViews(mViewRoot, 0, params.getExtendedViewInfoMode()); + + // success! + return SUCCESS.createResult(); + } catch (Throwable e) { + // get the real cause of the exception. + Throwable t = e; + while (t.getCause() != null) { + t = t.getCause(); + } + + return ERROR_UNKNOWN.createResult(t.getMessage(), t); + } + } + + /** + * Executes {@link View#measure(int, int)} on a given view with the given parameters (used + * to create measure specs with {@link MeasureSpec#makeMeasureSpec(int, int)}. + * + * if <var>measuredView</var> is non null, the method returns a {@link Pair} of (width, height) + * for the view (using {@link View#getMeasuredWidth()} and {@link View#getMeasuredHeight()}). + * + * @param viewToMeasure the view on which to execute measure(). + * @param measuredView if non null, the view to query for its measured width/height. + * @param width the width to use in the MeasureSpec. + * @param widthMode the MeasureSpec mode to use for the width. + * @param height the height to use in the MeasureSpec. + * @param heightMode the MeasureSpec mode to use for the height. + * @return the measured width/height if measuredView is non-null, null otherwise. + */ + private Pair<Integer, Integer> measureView(ViewGroup viewToMeasure, View measuredView, + int width, int widthMode, int height, int heightMode) { + int w_spec = MeasureSpec.makeMeasureSpec(width, widthMode); + int h_spec = MeasureSpec.makeMeasureSpec(height, heightMode); + viewToMeasure.measure(w_spec, h_spec); + + if (measuredView != null) { + return Pair.of(measuredView.getMeasuredWidth(), measuredView.getMeasuredHeight()); + } + + return null; + } + + /** + * Animate an object + * <p> + * {@link #acquire(long)} must have been called before this. + * + * @throws IllegalStateException if the current context is different than the one owned by + * the scene, or if {@link #acquire(long)} was not called. + * + * @see RenderSession#animate(Object, String, boolean, IAnimationListener) + */ + public Result animate(Object targetObject, String animationName, + boolean isFrameworkAnimation, IAnimationListener listener) { + checkLock(); + + BridgeContext context = getContext(); + + // find the animation file. + ResourceValue animationResource = null; + int animationId = 0; + if (isFrameworkAnimation) { + animationResource = context.getRenderResources().getFrameworkResource( + ResourceType.ANIMATOR, animationName); + if (animationResource != null) { + animationId = Bridge.getResourceId(ResourceType.ANIMATOR, animationName); + } + } else { + animationResource = context.getRenderResources().getProjectResource( + ResourceType.ANIMATOR, animationName); + if (animationResource != null) { + animationId = context.getProjectCallback().getResourceId( + ResourceType.ANIMATOR, animationName); + } + } + + if (animationResource != null) { + try { + Animator anim = AnimatorInflater.loadAnimator(context, animationId); + if (anim != null) { + anim.setTarget(targetObject); + + new PlayAnimationThread(anim, this, animationName, listener).start(); + + return SUCCESS.createResult(); + } + } catch (Exception e) { + // get the real cause of the exception. + Throwable t = e; + while (t.getCause() != null) { + t = t.getCause(); + } + + return ERROR_UNKNOWN.createResult(t.getMessage(), t); + } + } + + return ERROR_ANIM_NOT_FOUND.createResult(); + } + + /** + * Insert a new child into an existing parent. + * <p> + * {@link #acquire(long)} must have been called before this. + * + * @throws IllegalStateException if the current context is different than the one owned by + * the scene, or if {@link #acquire(long)} was not called. + * + * @see RenderSession#insertChild(Object, ILayoutPullParser, int, IAnimationListener) + */ + public Result insertChild(final ViewGroup parentView, ILayoutPullParser childXml, + final int index, IAnimationListener listener) { + checkLock(); + + BridgeContext context = getContext(); + + // create a block parser for the XML + BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( + childXml, context, false /* platformResourceFlag */); + + // inflate the child without adding it to the root since we want to control where it'll + // get added. We do pass the parentView however to ensure that the layoutParams will + // be created correctly. + final View child = mInflater.inflate(blockParser, parentView, false /*attachToRoot*/); + blockParser.ensurePopped(); + + invalidateRenderingSize(); + + if (listener != null) { + new AnimationThread(this, "insertChild", listener) { + + @Override + public Result preAnimation() { + parentView.setLayoutTransition(new LayoutTransition()); + return addView(parentView, child, index); + } + + @Override + public void postAnimation() { + parentView.setLayoutTransition(null); + } + }.start(); + + // always return success since the real status will come through the listener. + return SUCCESS.createResult(child); + } + + // add it to the parentView in the correct location + Result result = addView(parentView, child, index); + if (result.isSuccess() == false) { + return result; + } + + result = render(false /*freshRender*/); + if (result.isSuccess()) { + result = result.getCopyWithData(child); + } + + return result; + } + + /** + * Adds a given view to a given parent at a given index. + * + * @param parent the parent to receive the view + * @param view the view to add to the parent + * @param index the index where to do the add. + * + * @return a Result with {@link Status#SUCCESS} or + * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support + * adding views. + */ + private Result addView(ViewGroup parent, View view, int index) { + try { + parent.addView(view, index); + return SUCCESS.createResult(); + } catch (UnsupportedOperationException e) { + // looks like this is a view class that doesn't support children manipulation! + return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); + } + } + + /** + * Moves a view to a new parent at a given location + * <p> + * {@link #acquire(long)} must have been called before this. + * + * @throws IllegalStateException if the current context is different than the one owned by + * the scene, or if {@link #acquire(long)} was not called. + * + * @see RenderSession#moveChild(Object, Object, int, Map, IAnimationListener) + */ + public Result moveChild(final ViewGroup newParentView, final View childView, final int index, + Map<String, String> layoutParamsMap, final IAnimationListener listener) { + checkLock(); + + invalidateRenderingSize(); + + LayoutParams layoutParams = null; + if (layoutParamsMap != null) { + // need to create a new LayoutParams object for the new parent. + layoutParams = newParentView.generateLayoutParams( + new BridgeLayoutParamsMapAttributes(layoutParamsMap)); + } + + // get the current parent of the view that needs to be moved. + final ViewGroup previousParent = (ViewGroup) childView.getParent(); + + if (listener != null) { + final LayoutParams params = layoutParams; + + // there is no support for animating views across layouts, so in case the new and old + // parent views are different we fake the animation through a no animation thread. + if (previousParent != newParentView) { + new Thread("not animated moveChild") { + @Override + public void run() { + Result result = moveView(previousParent, newParentView, childView, index, + params); + if (result.isSuccess() == false) { + listener.done(result); + } + + // ready to do the work, acquire the scene. + result = acquire(250); + if (result.isSuccess() == false) { + listener.done(result); + return; + } + + try { + result = render(false /*freshRender*/); + if (result.isSuccess()) { + listener.onNewFrame(RenderSessionImpl.this.getSession()); + } + } finally { + release(); + } + + listener.done(result); + } + }.start(); + } else { + new AnimationThread(this, "moveChild", listener) { + + @Override + public Result preAnimation() { + // set up the transition for the parent. + LayoutTransition transition = new LayoutTransition(); + previousParent.setLayoutTransition(transition); + + // tweak the animation durations and start delays (to match the duration of + // animation playing just before). + // Note: Cannot user Animation.setDuration() directly. Have to set it + // on the LayoutTransition. + transition.setDuration(LayoutTransition.DISAPPEARING, 100); + // CHANGE_DISAPPEARING plays after DISAPPEARING + transition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 100); + + transition.setDuration(LayoutTransition.CHANGE_DISAPPEARING, 100); + + transition.setDuration(LayoutTransition.CHANGE_APPEARING, 100); + // CHANGE_APPEARING plays after CHANGE_APPEARING + transition.setStartDelay(LayoutTransition.APPEARING, 100); + + transition.setDuration(LayoutTransition.APPEARING, 100); + + return moveView(previousParent, newParentView, childView, index, params); + } + + @Override + public void postAnimation() { + previousParent.setLayoutTransition(null); + newParentView.setLayoutTransition(null); + } + }.start(); + } + + // always return success since the real status will come through the listener. + return SUCCESS.createResult(layoutParams); + } + + Result result = moveView(previousParent, newParentView, childView, index, layoutParams); + if (result.isSuccess() == false) { + return result; + } + + result = render(false /*freshRender*/); + if (layoutParams != null && result.isSuccess()) { + result = result.getCopyWithData(layoutParams); + } + + return result; + } + + /** + * Moves a View from its current parent to a new given parent at a new given location, with + * an optional new {@link LayoutParams} instance + * + * @param previousParent the previous parent, still owning the child at the time of the call. + * @param newParent the new parent + * @param movedView the view to move + * @param index the new location in the new parent + * @param params an option (can be null) {@link LayoutParams} instance. + * + * @return a Result with {@link Status#SUCCESS} or + * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support + * adding views. + */ + private Result moveView(ViewGroup previousParent, final ViewGroup newParent, + final View movedView, final int index, final LayoutParams params) { + try { + // check if there is a transition on the previousParent. + LayoutTransition previousTransition = previousParent.getLayoutTransition(); + if (previousTransition != null) { + // in this case there is an animation. This means we have to wait for the child's + // parent reference to be null'ed out so that we can add it to the new parent. + // It is technically removed right before the DISAPPEARING animation is done (if + // the animation of this type is not null, otherwise it's after which is impossible + // to handle). + // Because there is no move animation, if the new parent is the same as the old + // parent, we need to wait until the CHANGE_DISAPPEARING animation is done before + // adding the child or the child will appear in its new location before the + // other children have made room for it. + + // add a listener to the transition to be notified of the actual removal. + previousTransition.addTransitionListener(new TransitionListener() { + private int mChangeDisappearingCount = 0; + + @Override + public void startTransition(LayoutTransition transition, ViewGroup container, + View view, int transitionType) { + if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) { + mChangeDisappearingCount++; + } + } + + @Override + public void endTransition(LayoutTransition transition, ViewGroup container, + View view, int transitionType) { + if (transitionType == LayoutTransition.CHANGE_DISAPPEARING) { + mChangeDisappearingCount--; + } + + if (transitionType == LayoutTransition.CHANGE_DISAPPEARING && + mChangeDisappearingCount == 0) { + // add it to the parentView in the correct location + if (params != null) { + newParent.addView(movedView, index, params); + } else { + newParent.addView(movedView, index); + } + } + } + }); + + // remove the view from the current parent. + previousParent.removeView(movedView); + + // and return since adding the view to the new parent is done in the listener. + return SUCCESS.createResult(); + } else { + // standard code with no animation. pretty simple. + previousParent.removeView(movedView); + + // add it to the parentView in the correct location + if (params != null) { + newParent.addView(movedView, index, params); + } else { + newParent.addView(movedView, index); + } + + return SUCCESS.createResult(); + } + } catch (UnsupportedOperationException e) { + // looks like this is a view class that doesn't support children manipulation! + return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); + } + } + + /** + * Removes a child from its current parent. + * <p> + * {@link #acquire(long)} must have been called before this. + * + * @throws IllegalStateException if the current context is different than the one owned by + * the scene, or if {@link #acquire(long)} was not called. + * + * @see RenderSession#removeChild(Object, IAnimationListener) + */ + public Result removeChild(final View childView, IAnimationListener listener) { + checkLock(); + + invalidateRenderingSize(); + + final ViewGroup parent = (ViewGroup) childView.getParent(); + + if (listener != null) { + new AnimationThread(this, "moveChild", listener) { + + @Override + public Result preAnimation() { + parent.setLayoutTransition(new LayoutTransition()); + return removeView(parent, childView); + } + + @Override + public void postAnimation() { + parent.setLayoutTransition(null); + } + }.start(); + + // always return success since the real status will come through the listener. + return SUCCESS.createResult(); + } + + Result result = removeView(parent, childView); + if (result.isSuccess() == false) { + return result; + } + + return render(false /*freshRender*/); + } + + /** + * Removes a given view from its current parent. + * + * @param view the view to remove from its parent + * + * @return a Result with {@link Status#SUCCESS} or + * {@link Status#ERROR_VIEWGROUP_NO_CHILDREN} if the given parent doesn't support + * adding views. + */ + private Result removeView(ViewGroup parent, View view) { + try { + parent.removeView(view); + return SUCCESS.createResult(); + } catch (UnsupportedOperationException e) { + // looks like this is a view class that doesn't support children manipulation! + return ERROR_VIEWGROUP_NO_CHILDREN.createResult(); + } + } + + + private void findBackground(RenderResources resources) { + if (getParams().isBgColorOverridden() == false) { + mWindowBackground = resources.findItemInTheme("windowBackground", + true /*isFrameworkAttr*/); + if (mWindowBackground != null) { + mWindowBackground = resources.resolveResValue(mWindowBackground); + } + } + } + + private boolean hasSoftwareButtons() { + return getParams().getHardwareConfig().hasSoftwareButtons(); + } + + private void findStatusBar(RenderResources resources, DisplayMetrics metrics) { + boolean windowFullscreen = getBooleanThemeValue(resources, + "windowFullscreen", false /*defaultValue*/); + + if (windowFullscreen == false && mWindowIsFloating == false) { + // default value + mStatusBarSize = DEFAULT_STATUS_BAR_HEIGHT; + + // get the real value + ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN, + "status_bar_height"); + + if (value != null) { + TypedValue typedValue = ResourceHelper.getValue("status_bar_height", + value.getValue(), true /*requireUnit*/); + if (typedValue != null) { + // compute the pixel value based on the display metrics + mStatusBarSize = (int)typedValue.getDimension(metrics); + } + } + } + } + + private void findActionBar(RenderResources resources, DisplayMetrics metrics) { + if (mWindowIsFloating) { + return; + } + + boolean windowActionBar = getBooleanThemeValue(resources, + "windowActionBar", true /*defaultValue*/); + + // if there's a value and it's false (default is true) + if (windowActionBar) { + + // default size of the window title bar + mActionBarSize = DEFAULT_TITLE_BAR_HEIGHT; + + // get value from the theme. + ResourceValue value = resources.findItemInTheme("actionBarSize", + true /*isFrameworkAttr*/); + + // resolve it + value = resources.resolveResValue(value); + + if (value != null) { + // get the numerical value, if available + TypedValue typedValue = ResourceHelper.getValue("actionBarSize", value.getValue(), + true /*requireUnit*/); + if (typedValue != null) { + // compute the pixel value based on the display metrics + mActionBarSize = (int)typedValue.getDimension(metrics); + } + } + } else { + // action bar overrides title bar so only look for this one if action bar is hidden + boolean windowNoTitle = getBooleanThemeValue(resources, + "windowNoTitle", false /*defaultValue*/); + + if (windowNoTitle == false) { + + // default size of the window title bar + mTitleBarSize = DEFAULT_TITLE_BAR_HEIGHT; + + // get value from the theme. + ResourceValue value = resources.findItemInTheme("windowTitleSize", + true /*isFrameworkAttr*/); + + // resolve it + value = resources.resolveResValue(value); + + if (value != null) { + // get the numerical value, if available + TypedValue typedValue = ResourceHelper.getValue("windowTitleSize", + value.getValue(), true /*requireUnit*/); + if (typedValue != null) { + // compute the pixel value based on the display metrics + mTitleBarSize = (int)typedValue.getDimension(metrics); + } + } + } + + } + } + + private void findNavigationBar(RenderResources resources, DisplayMetrics metrics) { + if (hasSoftwareButtons() && mWindowIsFloating == false) { + + // default value + mNavigationBarSize = 48; // ?? + + HardwareConfig hardwareConfig = getParams().getHardwareConfig(); + + boolean barOnBottom = true; + + if (hardwareConfig.getOrientation() == ScreenOrientation.LANDSCAPE) { + // compute the dp of the screen. + int shortSize = hardwareConfig.getScreenHeight(); + + // compute in dp + int shortSizeDp = shortSize * DisplayMetrics.DENSITY_DEFAULT / hardwareConfig.getDensity().getDpiValue(); + + if (shortSizeDp < 600) { + // 0-599dp: "phone" UI with bar on the side + barOnBottom = false; + } else { + // 600+dp: "tablet" UI with bar on the bottom + barOnBottom = true; + } + } + + if (barOnBottom) { + mNavigationBarOrientation = LinearLayout.HORIZONTAL; + } else { + mNavigationBarOrientation = LinearLayout.VERTICAL; + } + + // get the real value + ResourceValue value = resources.getFrameworkResource(ResourceType.DIMEN, + barOnBottom ? "navigation_bar_height" : "navigation_bar_width"); + + if (value != null) { + TypedValue typedValue = ResourceHelper.getValue("navigation_bar_height", + value.getValue(), true /*requireUnit*/); + if (typedValue != null) { + // compute the pixel value based on the display metrics + mNavigationBarSize = (int)typedValue.getDimension(metrics); + } + } + } + } + + /** + * Looks for a attribute in the current theme. The attribute is in the android + * namespace. + * + * @param resources the render resources + * @param name the name of the attribute + * @param defaultValue the default value. + * @return the value of the attribute or the default one if not found. + */ + private boolean getBooleanThemeValue(RenderResources resources, + String name, boolean defaultValue) { + + // get the title bar flag from the current theme. + ResourceValue value = resources.findItemInTheme(name, true /*isFrameworkAttr*/); + + // because it may reference something else, we resolve it. + value = resources.resolveResValue(value); + + // if there's no value, return the default. + if (value == null || value.getValue() == null) { + return defaultValue; + } + + return XmlUtils.convertValueToBoolean(value.getValue(), defaultValue); + } + + /** + * Post process on a view hierachy that was just inflated. + * <p/>At the moment this only support TabHost: If {@link TabHost} is detected, look for the + * {@link TabWidget}, and the corresponding {@link FrameLayout} and make new tabs automatically + * based on the content of the {@link FrameLayout}. + * @param view the root view to process. + * @param projectCallback callback to the project. + */ + private void postInflateProcess(View view, IProjectCallback projectCallback) + throws PostInflateException { + if (view instanceof TabHost) { + setupTabHost((TabHost)view, projectCallback); + } else if (view instanceof QuickContactBadge) { + QuickContactBadge badge = (QuickContactBadge) view; + badge.setImageToDefault(); + } else if (view instanceof AdapterView<?>) { + // get the view ID. + int id = view.getId(); + + BridgeContext context = getContext(); + + // get a ResourceReference from the integer ID. + ResourceReference listRef = context.resolveId(id); + + if (listRef != null) { + SessionParams params = getParams(); + AdapterBinding binding = params.getAdapterBindings().get(listRef); + + // if there was no adapter binding, trying to get it from the call back. + if (binding == null) { + binding = params.getProjectCallback().getAdapterBinding(listRef, + context.getViewKey(view), view); + } + + if (binding != null) { + + if (view instanceof AbsListView) { + if ((binding.getFooterCount() > 0 || binding.getHeaderCount() > 0) && + view instanceof ListView) { + ListView list = (ListView) view; + + boolean skipCallbackParser = false; + + int count = binding.getHeaderCount(); + for (int i = 0 ; i < count ; i++) { + Pair<View, Boolean> pair = context.inflateView( + binding.getHeaderAt(i), + list, false /*attachToRoot*/, skipCallbackParser); + if (pair.getFirst() != null) { + list.addHeaderView(pair.getFirst()); + } + + skipCallbackParser |= pair.getSecond(); + } + + count = binding.getFooterCount(); + for (int i = 0 ; i < count ; i++) { + Pair<View, Boolean> pair = context.inflateView( + binding.getFooterAt(i), + list, false /*attachToRoot*/, skipCallbackParser); + if (pair.getFirst() != null) { + list.addFooterView(pair.getFirst()); + } + + skipCallbackParser |= pair.getSecond(); + } + } + + if (view instanceof ExpandableListView) { + ((ExpandableListView) view).setAdapter( + new FakeExpandableAdapter( + listRef, binding, params.getProjectCallback())); + } else { + ((AbsListView) view).setAdapter( + new FakeAdapter( + listRef, binding, params.getProjectCallback())); + } + } else if (view instanceof AbsSpinner) { + ((AbsSpinner) view).setAdapter( + new FakeAdapter( + listRef, binding, params.getProjectCallback())); + } + } + } + } else if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup)view; + final int count = group.getChildCount(); + for (int c = 0 ; c < count ; c++) { + View child = group.getChildAt(c); + postInflateProcess(child, projectCallback); + } + } + } + + /** + * Sets up a {@link TabHost} object. + * @param tabHost the TabHost to setup. + * @param projectCallback The project callback object to access the project R class. + * @throws PostInflateException + */ + private void setupTabHost(TabHost tabHost, IProjectCallback projectCallback) + throws PostInflateException { + // look for the TabWidget, and the FrameLayout. They have their own specific names + View v = tabHost.findViewById(android.R.id.tabs); + + if (v == null) { + throw new PostInflateException( + "TabHost requires a TabWidget with id \"android:id/tabs\".\n"); + } + + if ((v instanceof TabWidget) == false) { + throw new PostInflateException(String.format( + "TabHost requires a TabWidget with id \"android:id/tabs\".\n" + + "View found with id 'tabs' is '%s'", v.getClass().getCanonicalName())); + } + + v = tabHost.findViewById(android.R.id.tabcontent); + + if (v == null) { + // TODO: see if we can fake tabs even without the FrameLayout (same below when the framelayout is empty) + throw new PostInflateException( + "TabHost requires a FrameLayout with id \"android:id/tabcontent\"."); + } + + if ((v instanceof FrameLayout) == false) { + throw new PostInflateException(String.format( + "TabHost requires a FrameLayout with id \"android:id/tabcontent\".\n" + + "View found with id 'tabcontent' is '%s'", v.getClass().getCanonicalName())); + } + + FrameLayout content = (FrameLayout)v; + + // now process the content of the framelayout and dynamically create tabs for it. + final int count = content.getChildCount(); + + // this must be called before addTab() so that the TabHost searches its TabWidget + // and FrameLayout. + tabHost.setup(); + + if (count == 0) { + // Create a dummy child to get a single tab + TabSpec spec = tabHost.newTabSpec("tag").setIndicator("Tab Label", + tabHost.getResources().getDrawable(android.R.drawable.ic_menu_info_details)) + .setContent(new TabHost.TabContentFactory() { + @Override + public View createTabContent(String tag) { + return new LinearLayout(getContext()); + } + }); + tabHost.addTab(spec); + return; + } else { + // for each child of the framelayout, add a new TabSpec + for (int i = 0 ; i < count ; i++) { + View child = content.getChildAt(i); + String tabSpec = String.format("tab_spec%d", i+1); + int id = child.getId(); + Pair<ResourceType, String> resource = projectCallback.resolveResourceId(id); + String name; + if (resource != null) { + name = resource.getSecond(); + } else { + name = String.format("Tab %d", i+1); // default name if id is unresolved. + } + tabHost.addTab(tabHost.newTabSpec(tabSpec).setIndicator(name).setContent(id)); + } + } + } + + private List<ViewInfo> startVisitingViews(View view, int offset, boolean setExtendedInfo) { + if (view == null) { + return null; + } + + // adjust the offset to this view. + offset += view.getTop(); + + if (view == mContentRoot) { + return visitAllChildren(mContentRoot, offset, setExtendedInfo); + } + + // otherwise, look for mContentRoot in the children + if (view instanceof ViewGroup) { + ViewGroup group = ((ViewGroup) view); + + for (int i = 0; i < group.getChildCount(); i++) { + List<ViewInfo> list = startVisitingViews(group.getChildAt(i), offset, + setExtendedInfo); + if (list != null) { + return list; + } + } + } + + return null; + } + + /** + * Visits a View and its children and generate a {@link ViewInfo} containing the + * bounds of all the views. + * @param view the root View + * @param offset an offset for the view bounds. + * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object. + */ + private ViewInfo visit(View view, int offset, boolean setExtendedInfo) { + if (view == null) { + return null; + } + + ViewInfo result = new ViewInfo(view.getClass().getName(), + getContext().getViewKey(view), + view.getLeft(), view.getTop() + offset, view.getRight(), view.getBottom() + offset, + view, view.getLayoutParams()); + + if (setExtendedInfo) { + MarginLayoutParams marginParams = null; + LayoutParams params = view.getLayoutParams(); + if (params instanceof MarginLayoutParams) { + marginParams = (MarginLayoutParams) params; + } + result.setExtendedInfo(view.getBaseline(), + marginParams != null ? marginParams.leftMargin : 0, + marginParams != null ? marginParams.topMargin : 0, + marginParams != null ? marginParams.rightMargin : 0, + marginParams != null ? marginParams.bottomMargin : 0); + } + + if (view instanceof ViewGroup) { + ViewGroup group = ((ViewGroup) view); + result.setChildren(visitAllChildren(group, 0 /*offset*/, setExtendedInfo)); + } + + return result; + } + + /** + * Visits all the children of a given ViewGroup generate a list of {@link ViewInfo} + * containing the bounds of all the views. + * @param view the root View + * @param offset an offset for the view bounds. + * @param setExtendedInfo whether to set the extended view info in the {@link ViewInfo} object. + */ + private List<ViewInfo> visitAllChildren(ViewGroup viewGroup, int offset, + boolean setExtendedInfo) { + if (viewGroup == null) { + return null; + } + + List<ViewInfo> children = new ArrayList<ViewInfo>(); + for (int i = 0; i < viewGroup.getChildCount(); i++) { + children.add(visit(viewGroup.getChildAt(i), offset, setExtendedInfo)); + } + return children; + } + + + private void invalidateRenderingSize() { + mMeasuredScreenWidth = mMeasuredScreenHeight = -1; + } + + public BufferedImage getImage() { + return mImage; + } + + public boolean isAlphaChannelImage() { + return mIsAlphaChannelImage; + } + + public List<ViewInfo> getViewInfos() { + return mViewInfoList; + } + + public Map<String, String> getDefaultProperties(Object viewObject) { + return getContext().getDefaultPropMap(viewObject); + } + + public void setScene(RenderSession session) { + mScene = session; + } + + public RenderSession getSession() { + return mScene; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java new file mode 100644 index 000000000000..6dcb69393bd4 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java @@ -0,0 +1,493 @@ +/* + * 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.impl; + +import com.android.ide.common.rendering.api.DensityBasedResourceValue; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; +import com.android.ninepatch.NinePatch; +import com.android.ninepatch.NinePatchChunk; +import com.android.resources.Density; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.res.ColorStateList; +import android.graphics.Bitmap; +import android.graphics.Bitmap_Delegate; +import android.graphics.NinePatch_Delegate; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.NinePatchDrawable; +import android.util.TypedValue; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper class to provide various conversion 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. + */ + public static int getColor(String value) { + if (value != null) { + if (value.startsWith("#") == false) { + throw new NumberFormatException( + String.format("Color value '%s' must start with #", value)); + } + + value = value.substring(1); + + // make sure it's not longer than 32bit + if (value.length() > 8) { + throw new NumberFormatException(String.format( + "Color value '%s' is too long. Format is either" + + "#AARRGGBB, #RRGGBB, #RGB, or #ARGB", + value)); + } + + 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(); + } + + public static ColorStateList getColorStateList(ResourceValue resValue, BridgeContext context) { + String value = resValue.getValue(); + if (value != null && RenderResources.REFERENCE_NULL.equals(value) == false) { + // first check if the value is a file (xml most likely) + File f = new File(value); + if (f.isFile()) { + try { + // let the framework inflate the ColorStateList from the XML file, by + // providing an XmlPullParser + XmlPullParser parser = ParserFactory.create(f); + + BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( + parser, context, resValue.isFramework()); + try { + return ColorStateList.createFromXml(context.getResources(), blockParser); + } finally { + blockParser.ensurePopped(); + } + } catch (XmlPullParserException e) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + "Failed to configure parser for " + value, e, null /*data*/); + // we'll return null below. + } catch (Exception e) { + // this is an error and not warning since the file existence is + // checked before attempting to parse it. + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, + "Failed to parse file " + value, e, null /*data*/); + + return null; + } + } else { + // try to load the color state list from an int + try { + int color = ResourceHelper.getColor(value); + return ColorStateList.valueOf(color); + } catch (NumberFormatException e) { + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, + "Failed to convert " + value + " into a ColorStateList", e, + null /*data*/); + return null; + } + } + } + + return null; + } + + /** + * Returns a drawable from the given value. + * @param value The value that contains a path to a 9 patch, a bitmap or a xml based drawable, + * or an hexadecimal color + * @param context the current context + */ + public static Drawable getDrawable(ResourceValue value, BridgeContext context) { + String stringValue = value.getValue(); + if (RenderResources.REFERENCE_NULL.equals(stringValue)) { + return null; + } + + String lowerCaseValue = stringValue.toLowerCase(); + + Density density = Density.MEDIUM; + if (value instanceof DensityBasedResourceValue) { + density = + ((DensityBasedResourceValue)value).getResourceDensity(); + } + + + if (lowerCaseValue.endsWith(NinePatch.EXTENSION_9PATCH)) { + File file = new File(stringValue); + if (file.isFile()) { + try { + return getNinePatchDrawable( + new FileInputStream(file), density, value.isFramework(), + stringValue, context); + } catch (IOException e) { + // failed to read the file, we'll return null below. + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, + "Failed lot load " + file.getAbsolutePath(), e, null /*data*/); + } + } + + return null; + } else if (lowerCaseValue.endsWith(".xml")) { + // create a block parser for the file + File f = new File(stringValue); + if (f.isFile()) { + try { + // let the framework inflate the Drawable from the XML file. + XmlPullParser parser = ParserFactory.create(f); + + BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( + parser, context, value.isFramework()); + try { + return Drawable.createFromXml(context.getResources(), blockParser); + } finally { + blockParser.ensurePopped(); + } + } catch (Exception e) { + // this is an error and not warning since the file existence is checked before + // attempting to parse it. + Bridge.getLog().error(null, "Failed to parse file " + stringValue, + e, null /*data*/); + } + } else { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + String.format("File %s does not exist (or is not a file)", stringValue), + null /*data*/); + } + + return null; + } else { + File bmpFile = new File(stringValue); + if (bmpFile.isFile()) { + try { + Bitmap bitmap = Bridge.getCachedBitmap(stringValue, + value.isFramework() ? null : context.getProjectKey()); + + if (bitmap == null) { + bitmap = Bitmap_Delegate.createBitmap(bmpFile, false /*isMutable*/, + density); + Bridge.setCachedBitmap(stringValue, bitmap, + value.isFramework() ? null : context.getProjectKey()); + } + + return new BitmapDrawable(context.getResources(), bitmap); + } catch (IOException e) { + // we'll return null below + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, + "Failed lot load " + bmpFile.getAbsolutePath(), e, null /*data*/); + } + } else { + // attempt to get a color from the value + try { + int color = getColor(stringValue); + return new ColorDrawable(color); + } catch (NumberFormatException e) { + // we'll return null below. + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_FORMAT, + "Failed to convert " + stringValue + " into a drawable", e, + null /*data*/); + } + } + } + + return null; + } + + private static Drawable getNinePatchDrawable(InputStream inputStream, Density density, + boolean isFramework, String cacheKey, BridgeContext context) throws IOException { + // see if we still have both the chunk and the bitmap in the caches + NinePatchChunk chunk = Bridge.getCached9Patch(cacheKey, + isFramework ? null : context.getProjectKey()); + Bitmap bitmap = Bridge.getCachedBitmap(cacheKey, + isFramework ? null : context.getProjectKey()); + + // if either chunk or bitmap is null, then we reload the 9-patch file. + if (chunk == null || bitmap == null) { + try { + NinePatch ninePatch = NinePatch.load(inputStream, true /*is9Patch*/, + false /* convert */); + if (ninePatch != null) { + if (chunk == null) { + chunk = ninePatch.getChunk(); + + Bridge.setCached9Patch(cacheKey, chunk, + isFramework ? null : context.getProjectKey()); + } + + if (bitmap == null) { + bitmap = Bitmap_Delegate.createBitmap(ninePatch.getImage(), + false /*isMutable*/, + density); + + Bridge.setCachedBitmap(cacheKey, bitmap, + isFramework ? null : context.getProjectKey()); + } + } + } catch (MalformedURLException e) { + // URL is wrong, we'll return null below + } + } + + if (chunk != null && bitmap != null) { + int[] padding = chunk.getPadding(); + Rect paddingRect = new Rect(padding[0], padding[1], padding[2], padding[3]); + + return new NinePatchDrawable(context.getResources(), bitmap, + NinePatch_Delegate.serialize(chunk), + paddingRect, null); + } + + 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 attribute float-type value string. + * This object is only valid until the next call on to {@link ResourceHelper}. + */ + public static TypedValue getValue(String attribute, String value, boolean requireUnit) { + if (parseFloatAttribute(attribute, value, mValue, requireUnit)) { + return mValue; + } + + return null; + } + + /** + * Parse a float attribute and return the parsed value into a given TypedValue. + * @param attribute the name of the attribute. Can be null if <var>requireUnit</var> is false. + * @param value the string value of the attribute + * @param outValue the TypedValue to receive the parsed value + * @param requireUnit whether the value is expected to contain a unit. + * @return true if success. + */ + public static boolean parseFloatAttribute(String attribute, String value, + TypedValue outValue, boolean requireUnit) { + assert requireUnit == false || attribute != null; + + // remove the space before and after + value = value.trim(); + int len = value.length(); + + if (len <= 0) { + return false; + } + + // check that there's no non ascii characters. + char[] buf = value.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] != '.' && buf[0] != '-') { + return false; + } + + // now look for the string that is after the float... + Matcher m = sFloatPattern.matcher(value); + 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)) { + computeTypedValue(outValue, f, sFloatOut[0]); + return true; + } + return false; + } + + // make sure it's only spaces at the end. + end = end.trim(); + + if (end.length() == 0) { + if (outValue != null) { + if (requireUnit == false) { + outValue.type = TypedValue.TYPE_FLOAT; + outValue.data = Float.floatToIntBits(f); + } else { + // no unit when required? Use dp and out an error. + applyUnit(sUnitNames[1], outValue, sFloatOut); + computeTypedValue(outValue, f, sFloatOut[0]); + + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_RESOLVE, + String.format( + "Dimension \"%1$s\" in attribute \"%2$s\" is missing unit!", + value, attribute), + null); + } + return true; + } + } + } + + return false; + } + + private static void computeTypedValue(TypedValue outValue, float value, float scale) { + value *= scale; + boolean neg = value < 0; + if (neg) { + value = -value; + } + long bits = (long)(value*(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); + } + + private static boolean parseUnit(String str, TypedValue outValue, float[] outScale) { + str = str.trim(); + + for (UnitEntry unit : sUnitNames) { + if (unit.name.equals(str)) { + applyUnit(unit, outValue, outScale); + return true; + } + } + + return false; + } + + private static void applyUnit(UnitEntry unit, TypedValue outValue, float[] outScale) { + outValue.type = unit.type; + outValue.data = unit.unit << TypedValue.COMPLEX_UNIT_SHIFT; + outScale[0] = unit.scale; + } +} + diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/Stack.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/Stack.java new file mode 100644 index 000000000000..9bd0015db508 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/Stack.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl; + +import java.util.ArrayList; + +/** + * Custom Stack implementation on top of an {@link ArrayList} instead of + * using {@link java.util.Stack} which is on top of a vector. + * + * @param <T> + */ +public class Stack<T> extends ArrayList<T> { + + private static final long serialVersionUID = 1L; + + public Stack() { + super(); + } + + public Stack(int size) { + super(size); + } + + /** + * Pushes the given object to the stack + * @param object the object to push + */ + public void push(T object) { + add(object); + } + + /** + * Remove the object at the top of the stack and returns it. + * @return the removed object or null if the stack was empty. + */ + public T pop() { + if (size() > 0) { + return remove(size() - 1); + } + + return null; + } + + /** + * Returns the object at the top of the stack. + * @return the object at the top or null if the stack is empty. + */ + public T peek() { + if (size() > 0) { + return get(size() - 1); + } + + return null; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/BaseAdapter.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/BaseAdapter.java new file mode 100644 index 000000000000..e0414fe3ee4b --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/BaseAdapter.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl.binding; + +import com.android.ide.common.rendering.api.AdapterBinding; +import com.android.ide.common.rendering.api.DataBindingItem; +import com.android.ide.common.rendering.api.IProjectCallback; +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.ide.common.rendering.api.ResourceReference; +import com.android.ide.common.rendering.api.IProjectCallback.ViewAttribute; +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.impl.RenderAction; +import com.android.util.Pair; + +import android.database.DataSetObserver; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Checkable; +import android.widget.ImageView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Base adapter to do fake data binding in {@link AdapterView} objects. + */ +public class BaseAdapter { + + /** + * This is the items provided by the adapter. They are dynamically generated. + */ + protected final static class AdapterItem { + private final DataBindingItem mItem; + private final int mType; + private final int mFullPosition; + private final int mPositionPerType; + private List<AdapterItem> mChildren; + + protected AdapterItem(DataBindingItem item, int type, int fullPosition, + int positionPerType) { + mItem = item; + mType = type; + mFullPosition = fullPosition; + mPositionPerType = positionPerType; + } + + void addChild(AdapterItem child) { + if (mChildren == null) { + mChildren = new ArrayList<AdapterItem>(); + } + + mChildren.add(child); + } + + List<AdapterItem> getChildren() { + if (mChildren != null) { + return mChildren; + } + + return Collections.emptyList(); + } + + int getType() { + return mType; + } + + int getFullPosition() { + return mFullPosition; + } + + int getPositionPerType() { + return mPositionPerType; + } + + DataBindingItem getDataBindingItem() { + return mItem; + } + } + + private final AdapterBinding mBinding; + private final IProjectCallback mCallback; + private final ResourceReference mAdapterRef; + private boolean mSkipCallbackParser = false; + + protected final List<AdapterItem> mItems = new ArrayList<AdapterItem>(); + + protected BaseAdapter(ResourceReference adapterRef, AdapterBinding binding, + IProjectCallback callback) { + mAdapterRef = adapterRef; + mBinding = binding; + mCallback = callback; + } + + // ------- Some Adapter method used by all children classes. + + public boolean areAllItemsEnabled() { + return true; + } + + public boolean hasStableIds() { + return true; + } + + public boolean isEmpty() { + return mItems.size() == 0; + } + + public void registerDataSetObserver(DataSetObserver observer) { + // pass + } + + public void unregisterDataSetObserver(DataSetObserver observer) { + // pass + } + + // ------- + + + protected AdapterBinding getBinding() { + return mBinding; + } + + protected View getView(AdapterItem item, AdapterItem parentItem, View convertView, + ViewGroup parent) { + // we don't care about recycling here because we never scroll. + DataBindingItem dataBindingItem = item.getDataBindingItem(); + + BridgeContext context = RenderAction.getCurrentContext(); + + Pair<View, Boolean> pair = context.inflateView(dataBindingItem.getViewReference(), + parent, false /*attachToRoot*/, mSkipCallbackParser); + + View view = pair.getFirst(); + mSkipCallbackParser |= pair.getSecond(); + + if (view != null) { + fillView(context, view, item, parentItem); + } else { + // create a text view to display an error. + TextView tv = new TextView(context); + tv.setText("Unable to find layout: " + dataBindingItem.getViewReference().getName()); + view = tv; + } + + return view; + } + + private void fillView(BridgeContext context, View view, AdapterItem item, + AdapterItem parentItem) { + if (view instanceof ViewGroup) { + ViewGroup group = (ViewGroup) view; + final int count = group.getChildCount(); + for (int i = 0 ; i < count ; i++) { + fillView(context, group.getChildAt(i), item, parentItem); + } + } else { + int id = view.getId(); + if (id != 0) { + ResourceReference resolvedRef = context.resolveId(id); + if (resolvedRef != null) { + int fullPosition = item.getFullPosition(); + int positionPerType = item.getPositionPerType(); + int fullParentPosition = parentItem != null ? parentItem.getFullPosition() : 0; + int parentPositionPerType = parentItem != null ? + parentItem.getPositionPerType() : 0; + + if (view instanceof TextView) { + TextView tv = (TextView) view; + Object value = mCallback.getAdapterItemValue( + mAdapterRef, context.getViewKey(view), + item.getDataBindingItem().getViewReference(), + fullPosition, positionPerType, + fullParentPosition, parentPositionPerType, + resolvedRef, ViewAttribute.TEXT, tv.getText().toString()); + if (value != null) { + if (value.getClass() != ViewAttribute.TEXT.getAttributeClass()) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, String.format( + "Wrong Adapter Item value class for TEXT. Expected String, got %s", + value.getClass().getName()), null); + } else { + tv.setText((String) value); + } + } + } + + if (view instanceof Checkable) { + Checkable cb = (Checkable) view; + + Object value = mCallback.getAdapterItemValue( + mAdapterRef, context.getViewKey(view), + item.getDataBindingItem().getViewReference(), + fullPosition, positionPerType, + fullParentPosition, parentPositionPerType, + resolvedRef, ViewAttribute.IS_CHECKED, cb.isChecked()); + if (value != null) { + if (value.getClass() != ViewAttribute.IS_CHECKED.getAttributeClass()) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, String.format( + "Wrong Adapter Item value class for TEXT. Expected Boolean, got %s", + value.getClass().getName()), null); + } else { + cb.setChecked((Boolean) value); + } + } + } + + if (view instanceof ImageView) { + ImageView iv = (ImageView) view; + + Object value = mCallback.getAdapterItemValue( + mAdapterRef, context.getViewKey(view), + item.getDataBindingItem().getViewReference(), + fullPosition, positionPerType, + fullParentPosition, parentPositionPerType, + resolvedRef, ViewAttribute.SRC, iv.getDrawable()); + if (value != null) { + if (value.getClass() != ViewAttribute.SRC.getAttributeClass()) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, String.format( + "Wrong Adapter Item value class for TEXT. Expected Boolean, got %s", + value.getClass().getName()), null); + } else { + // FIXME + } + } + } + } + } + } + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeAdapter.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeAdapter.java new file mode 100644 index 000000000000..22570b9cf865 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeAdapter.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl.binding; + +import com.android.ide.common.rendering.api.AdapterBinding; +import com.android.ide.common.rendering.api.DataBindingItem; +import com.android.ide.common.rendering.api.IProjectCallback; +import com.android.ide.common.rendering.api.ResourceReference; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ListAdapter; +import android.widget.SpinnerAdapter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Fake adapter to do fake data binding in {@link AdapterView} objects for {@link ListAdapter} + * and {@link SpinnerAdapter}. + * + */ +public class FakeAdapter extends BaseAdapter implements ListAdapter, SpinnerAdapter { + + // don't use a set because the order is important. + private final List<ResourceReference> mTypes = new ArrayList<ResourceReference>(); + + public FakeAdapter(ResourceReference adapterRef, AdapterBinding binding, + IProjectCallback callback) { + super(adapterRef, binding, callback); + + final int repeatCount = getBinding().getRepeatCount(); + final int itemCount = getBinding().getItemCount(); + + // Need an array to count for each type. + // This is likely too big, but is the max it can be. + int[] typeCount = new int[itemCount]; + + // We put several repeating sets. + for (int r = 0 ; r < repeatCount ; r++) { + // loop on the type of list items, and add however many for each type. + for (DataBindingItem dataBindingItem : getBinding()) { + ResourceReference viewRef = dataBindingItem.getViewReference(); + int typeIndex = mTypes.indexOf(viewRef); + if (typeIndex == -1) { + typeIndex = mTypes.size(); + mTypes.add(viewRef); + } + + int count = dataBindingItem.getCount(); + + int index = typeCount[typeIndex]; + typeCount[typeIndex] += count; + + for (int k = 0 ; k < count ; k++) { + mItems.add(new AdapterItem(dataBindingItem, typeIndex, mItems.size(), index++)); + } + } + } + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + @Override + public int getCount() { + return mItems.size(); + } + + @Override + public Object getItem(int position) { + return mItems.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemViewType(int position) { + return mItems.get(position).getType(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // we don't care about recycling here because we never scroll. + AdapterItem item = mItems.get(position); + return getView(item, null /*parentGroup*/, convertView, parent); + } + + @Override + public int getViewTypeCount() { + return mTypes.size(); + } + + // ---- SpinnerAdapter + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + // pass + return null; + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeExpandableAdapter.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeExpandableAdapter.java new file mode 100644 index 000000000000..199e0404a16b --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeExpandableAdapter.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.impl.binding; + +import com.android.ide.common.rendering.api.AdapterBinding; +import com.android.ide.common.rendering.api.DataBindingItem; +import com.android.ide.common.rendering.api.IProjectCallback; +import com.android.ide.common.rendering.api.ResourceReference; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ExpandableListAdapter; +import android.widget.HeterogeneousExpandableList; + +import java.util.ArrayList; +import java.util.List; + +public class FakeExpandableAdapter extends BaseAdapter implements ExpandableListAdapter, + HeterogeneousExpandableList { + + // don't use a set because the order is important. + private final List<ResourceReference> mGroupTypes = new ArrayList<ResourceReference>(); + private final List<ResourceReference> mChildrenTypes = new ArrayList<ResourceReference>(); + + public FakeExpandableAdapter(ResourceReference adapterRef, AdapterBinding binding, + IProjectCallback callback) { + super(adapterRef, binding, callback); + + createItems(binding, binding.getItemCount(), binding.getRepeatCount(), mGroupTypes, 1); + } + + private void createItems(Iterable<DataBindingItem> iterable, final int itemCount, + final int repeatCount, List<ResourceReference> types, int depth) { + // Need an array to count for each type. + // This is likely too big, but is the max it can be. + int[] typeCount = new int[itemCount]; + + // we put several repeating sets. + for (int r = 0 ; r < repeatCount ; r++) { + // loop on the type of list items, and add however many for each type. + for (DataBindingItem dataBindingItem : iterable) { + ResourceReference viewRef = dataBindingItem.getViewReference(); + int typeIndex = types.indexOf(viewRef); + if (typeIndex == -1) { + typeIndex = types.size(); + types.add(viewRef); + } + + List<DataBindingItem> children = dataBindingItem.getChildren(); + int count = dataBindingItem.getCount(); + + // if there are children, we use the count as a repeat count for the children. + if (children.size() > 0) { + count = 1; + } + + int index = typeCount[typeIndex]; + typeCount[typeIndex] += count; + + for (int k = 0 ; k < count ; k++) { + AdapterItem item = new AdapterItem(dataBindingItem, typeIndex, mItems.size(), + index++); + mItems.add(item); + + if (children.size() > 0) { + createItems(dataBindingItem, depth + 1); + } + } + } + } + } + + private void createItems(DataBindingItem item, int depth) { + if (depth == 2) { + createItems(item, item.getChildren().size(), item.getCount(), mChildrenTypes, depth); + } + } + + private AdapterItem getChildItem(int groupPosition, int childPosition) { + AdapterItem item = mItems.get(groupPosition); + + List<AdapterItem> children = item.getChildren(); + return children.get(childPosition); + } + + // ---- ExpandableListAdapter + + @Override + public int getGroupCount() { + return mItems.size(); + } + + @Override + public int getChildrenCount(int groupPosition) { + AdapterItem item = mItems.get(groupPosition); + return item.getChildren().size(); + } + + @Override + public Object getGroup(int groupPosition) { + return mItems.get(groupPosition); + } + + @Override + public Object getChild(int groupPosition, int childPosition) { + return getChildItem(groupPosition, childPosition); + } + + @Override + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, + ViewGroup parent) { + // we don't care about recycling here because we never scroll. + AdapterItem item = mItems.get(groupPosition); + return getView(item, null /*parentItem*/, convertView, parent); + } + + @Override + public View getChildView(int groupPosition, int childPosition, boolean isLastChild, + View convertView, ViewGroup parent) { + // we don't care about recycling here because we never scroll. + AdapterItem parentItem = mItems.get(groupPosition); + AdapterItem item = getChildItem(groupPosition, childPosition); + return getView(item, parentItem, convertView, parent); + } + + @Override + public long getGroupId(int groupPosition) { + return groupPosition; + } + + @Override + public long getChildId(int groupPosition, int childPosition) { + return childPosition; + } + + @Override + public long getCombinedGroupId(long groupId) { + return groupId << 16 | 0x0000FFFF; + } + + @Override + public long getCombinedChildId(long groupId, long childId) { + return groupId << 16 | childId; + } + + @Override + public boolean isChildSelectable(int groupPosition, int childPosition) { + return true; + } + + @Override + public void onGroupCollapsed(int groupPosition) { + // pass + } + + @Override + public void onGroupExpanded(int groupPosition) { + // pass + } + + // ---- HeterogeneousExpandableList + + @Override + public int getChildType(int groupPosition, int childPosition) { + return getChildItem(groupPosition, childPosition).getType(); + } + + @Override + public int getChildTypeCount() { + return mChildrenTypes.size(); + } + + @Override + public int getGroupType(int groupPosition) { + return mItems.get(groupPosition).getType(); + } + + @Override + public int getGroupTypeCount() { + return mGroupTypes.size(); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/Debug.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/Debug.java new file mode 100644 index 000000000000..82eab8560acb --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/Debug.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.util; + +public class Debug { + + public final static boolean DEBUG = false; + +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/DynamicIdMap.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/DynamicIdMap.java new file mode 100644 index 000000000000..a1fae95fc1ae --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/DynamicIdMap.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2012 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.util; + +import com.android.resources.ResourceType; +import com.android.util.Pair; + +import android.util.SparseArray; + +import java.util.HashMap; +import java.util.Map; + +public class DynamicIdMap { + + private final Map<Pair<ResourceType, String>, Integer> mDynamicIds = new HashMap<Pair<ResourceType, String>, Integer>(); + private final SparseArray<Pair<ResourceType, String>> mRevDynamicIds = new SparseArray<Pair<ResourceType, String>>(); + private int mDynamicSeed; + + public DynamicIdMap(int seed) { + mDynamicSeed = seed; + } + + public void reset(int seed) { + mDynamicIds.clear(); + mRevDynamicIds.clear(); + mDynamicSeed = seed; + } + + /** + * Returns a dynamic integer for the given resource type/name, creating it if it doesn't + * already exist. + * + * @param type the type of the resource + * @param name the name of the resource + * @return an integer. + */ + public Integer getId(ResourceType type, String name) { + return getId(Pair.of(type, name)); + } + + /** + * Returns a dynamic integer for the given resource type/name, creating it if it doesn't + * already exist. + * + * @param resource the type/name of the resource + * @return an integer. + */ + public Integer getId(Pair<ResourceType, String> resource) { + Integer value = mDynamicIds.get(resource); + if (value == null) { + value = Integer.valueOf(++mDynamicSeed); + mDynamicIds.put(resource, value); + mRevDynamicIds.put(value, resource); + } + + return value; + } + + public Pair<ResourceType, String> resolveId(int id) { + return mRevDynamicIds.get(id); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/SparseWeakArray.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/SparseWeakArray.java new file mode 100644 index 000000000000..4d0c9ce3c71e --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/SparseWeakArray.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.util; + + +import com.android.internal.util.ArrayUtils; + +import android.util.SparseArray; + +import java.lang.ref.WeakReference; + +/** + * This is a custom {@link SparseArray} that uses {@link WeakReference} around the objects added + * to it. When the array is compacted, not only deleted indices but also empty references + * are removed, making the array efficient at removing references that were reclaimed. + * + * The code is taken from {@link SparseArray} directly and adapted to use weak references. + * + * Because our usage means that we never actually call {@link #remove(int)} or {@link #delete(int)}, + * we must manually check if there are reclaimed references to trigger an internal compact step + * (which is normally only triggered when an item is manually removed). + * + * SparseArrays map integers to Objects. Unlike a normal array of Objects, + * there can be gaps in the indices. It is intended to be more efficient + * than using a HashMap to map Integers to Objects. + */ +@SuppressWarnings("unchecked") +public class SparseWeakArray<E> { + + private static final Object DELETED_REF = new Object(); + private static final WeakReference<?> DELETED = new WeakReference(DELETED_REF); + private boolean mGarbage = false; + + /** + * Creates a new SparseArray containing no mappings. + */ + public SparseWeakArray() { + this(10); + } + + /** + * Creates a new SparseArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. + */ + public SparseWeakArray(int initialCapacity) { + initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity); + + mKeys = new int[initialCapacity]; + mValues = new WeakReference[initialCapacity]; + mSize = 0; + } + + /** + * Gets the Object mapped from the specified key, or <code>null</code> + * if no such mapping has been made. + */ + public E get(int key) { + return get(key, null); + } + + /** + * Gets the Object mapped from the specified key, or the specified Object + * if no such mapping has been made. + */ + public E get(int key, E valueIfKeyNotFound) { + int i = binarySearch(mKeys, 0, mSize, key); + + if (i < 0 || mValues[i] == DELETED || mValues[i].get() == null) { + return valueIfKeyNotFound; + } else { + return (E) mValues[i].get(); + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) { + int i = binarySearch(mKeys, 0, mSize, key); + + if (i >= 0) { + if (mValues[i] != DELETED) { + mValues[i] = DELETED; + mGarbage = true; + } + } + } + + /** + * Alias for {@link #delete(int)}. + */ + public void remove(int key) { + delete(key); + } + + /** + * Removes the mapping at the specified index. + */ + public void removeAt(int index) { + if (mValues[index] != DELETED) { + mValues[index] = DELETED; + mGarbage = true; + } + } + + private void gc() { + int n = mSize; + int o = 0; + int[] keys = mKeys; + WeakReference<?>[] values = mValues; + + for (int i = 0; i < n; i++) { + WeakReference<?> val = values[i]; + + // Don't keep any non DELETED values, but only the one that still have a valid + // reference. + if (val != DELETED && val.get() != null) { + if (i != o) { + keys[o] = keys[i]; + values[o] = val; + } + + o++; + } + } + + mGarbage = false; + mSize = o; + + int newSize = ArrayUtils.idealIntArraySize(mSize); + if (newSize < mKeys.length) { + int[] nkeys = new int[newSize]; + WeakReference<?>[] nvalues = new WeakReference[newSize]; + + System.arraycopy(mKeys, 0, nkeys, 0, newSize); + System.arraycopy(mValues, 0, nvalues, 0, newSize); + + mKeys = nkeys; + mValues = nvalues; + } + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(int key, E value) { + int i = binarySearch(mKeys, 0, mSize, key); + + if (i >= 0) { + mValues[i] = new WeakReference(value); + } else { + i = ~i; + + if (i < mSize && (mValues[i] == DELETED || mValues[i].get() == null)) { + mKeys[i] = key; + mValues[i] = new WeakReference(value); + return; + } + + if (mSize >= mKeys.length && (mGarbage || hasReclaimedRefs())) { + gc(); + + // Search again because indices may have changed. + i = ~binarySearch(mKeys, 0, mSize, key); + } + + if (mSize >= mKeys.length) { + int n = ArrayUtils.idealIntArraySize(mSize + 1); + + int[] nkeys = new int[n]; + WeakReference<?>[] nvalues = new WeakReference[n]; + + // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + if (mSize - i != 0) { + // Log.e("SparseArray", "move " + (mSize - i)); + System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); + System.arraycopy(mValues, i, mValues, i + 1, mSize - i); + } + + mKeys[i] = key; + mValues[i] = new WeakReference(value); + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseArray + * currently stores. + */ + public int size() { + if (mGarbage) { + gc(); + } + + return mSize; + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the key from the <code>index</code>th key-value mapping that this + * SparseArray stores. + */ + public int keyAt(int index) { + if (mGarbage) { + gc(); + } + + return mKeys[index]; + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the value from the <code>index</code>th key-value mapping that this + * SparseArray stores. + */ + public E valueAt(int index) { + if (mGarbage) { + gc(); + } + + return (E) mValues[index].get(); + } + + /** + * Given an index in the range <code>0...size()-1</code>, sets a new + * value for the <code>index</code>th key-value mapping that this + * SparseArray stores. + */ + public void setValueAt(int index, E value) { + if (mGarbage) { + gc(); + } + + mValues[index] = new WeakReference(value); + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(int key) { + if (mGarbage) { + gc(); + } + + return binarySearch(mKeys, 0, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(E value) { + if (mGarbage) { + gc(); + } + + for (int i = 0; i < mSize; i++) + if (mValues[i].get() == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this SparseArray. + */ + public void clear() { + int n = mSize; + WeakReference<?>[] values = mValues; + + for (int i = 0; i < n; i++) { + values[i] = null; + } + + mSize = 0; + mGarbage = false; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(int key, E value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + if (mSize >= mKeys.length && (mGarbage || hasReclaimedRefs())) { + gc(); + } + + int pos = mSize; + if (pos >= mKeys.length) { + int n = ArrayUtils.idealIntArraySize(pos + 1); + + int[] nkeys = new int[n]; + WeakReference<?>[] nvalues = new WeakReference[n]; + + // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + mKeys[pos] = key; + mValues[pos] = new WeakReference(value); + mSize = pos + 1; + } + + private boolean hasReclaimedRefs() { + for (int i = 0 ; i < mSize ; i++) { + if (mValues[i].get() == null) { // DELETED.get() never returns null. + return true; + } + } + + return false; + } + + private static int binarySearch(int[] a, int start, int len, int key) { + int high = start + len, low = start - 1, guess; + + while (high - low > 1) { + guess = (high + low) / 2; + + if (a[guess] < key) + low = guess; + else + high = guess; + } + + if (high == start + len) + return ~(start + len); + else if (a[high] == key) + return high; + else + return ~high; + } + + private int[] mKeys; + private WeakReference<?>[] mValues; + private int mSize; +} 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 000000000000..6d013bbe6b05 --- /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/src/libcore/icu/ICU_Delegate.java b/tools/layoutlib/bridge/src/libcore/icu/ICU_Delegate.java new file mode 100644 index 000000000000..cd4f82bee925 --- /dev/null +++ b/tools/layoutlib/bridge/src/libcore/icu/ICU_Delegate.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package libcore.icu; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.util.Locale; + +/** + * Delegate implementing the native methods of libcore.icu.ICU + * + * Through the layoutlib_create tool, the original native methods of ICU have been replaced + * by calls to methods of the same name in this delegate class. + * + */ +public class ICU_Delegate { + + // --- Java delegates + + @LayoutlibDelegate + /*package*/ static String toLowerCase(String s, String localeName) { + return s.toLowerCase(); + } + + @LayoutlibDelegate + /*package*/ static String toUpperCase(String s, String localeName) { + return s.toUpperCase(); + } + + // --- Native methods accessing ICU's database. + + @LayoutlibDelegate + /*package*/ static String getBestDateTimePattern(String skeleton, String localeName) { + return ""; // TODO: check what the right value should be. + } + + @LayoutlibDelegate + /*package*/ static String getCldrVersion() { + return "22.1.1"; // TODO: check what the right value should be. + } + + @LayoutlibDelegate + /*package*/ static String getIcuVersion() { + return "unknown_layoutlib"; + } + + @LayoutlibDelegate + /*package*/ static String getUnicodeVersion() { + return "5.2"; + } + + @LayoutlibDelegate + /*package*/ static String[] getAvailableBreakIteratorLocalesNative() { + return new String[0]; + } + + @LayoutlibDelegate + /*package*/ static String[] getAvailableCalendarLocalesNative() { + return new String[0]; + } + + @LayoutlibDelegate + /*package*/ static String[] getAvailableCollatorLocalesNative() { + return new String[0]; + } + + @LayoutlibDelegate + /*package*/ static String[] getAvailableDateFormatLocalesNative() { + return new String[0]; + } + + @LayoutlibDelegate + /*package*/ static String[] getAvailableLocalesNative() { + return new String[0]; + } + + @LayoutlibDelegate + /*package*/ static String[] getAvailableNumberFormatLocalesNative() { + return new String[0]; + } + + @LayoutlibDelegate + /*package*/ static String[] getAvailableCurrencyCodes() { + return new String[0]; + } + + @LayoutlibDelegate + /*package*/ static String getCurrencyCode(String locale) { + return ""; + } + + @LayoutlibDelegate + /*package*/ static String getCurrencyDisplayName(String locale, String currencyCode) { + return ""; + } + + @LayoutlibDelegate + /*package*/ static int getCurrencyFractionDigits(String currencyCode) { + return 0; + } + + @LayoutlibDelegate + /*package*/ static String getCurrencySymbol(String locale, String currencyCode) { + return ""; + } + + @LayoutlibDelegate + /*package*/ static String getDisplayCountryNative(String countryCode, String locale) { + return ""; + } + + @LayoutlibDelegate + /*package*/ static String getDisplayLanguageNative(String languageCode, String locale) { + return ""; + } + + @LayoutlibDelegate + /*package*/ static String getDisplayVariantNative(String variantCode, String locale) { + return ""; + } + + @LayoutlibDelegate + /*package*/ static String getISO3CountryNative(String locale) { + return ""; + } + + @LayoutlibDelegate + /*package*/ static String getISO3LanguageNative(String locale) { + return ""; + } + + @LayoutlibDelegate + /*package*/ static String addLikelySubtags(String locale) { + return ""; + } + + @LayoutlibDelegate + /*package*/ static String getScript(String locale) { + return ""; + } + + @LayoutlibDelegate + /*package*/ static String[] getISOLanguagesNative() { + return Locale.getISOLanguages(); + } + + @LayoutlibDelegate + /*package*/ static String[] getISOCountriesNative() { + return Locale.getISOCountries(); + } + + @LayoutlibDelegate + /*package*/ static boolean initLocaleDataImpl(String locale, LocaleData result) { + + // Used by Calendar. + result.firstDayOfWeek = Integer.valueOf(1); + result.minimalDaysInFirstWeek = Integer.valueOf(1); + + // Used by DateFormatSymbols. + result.amPm = new String[] { "AM", "PM" }; + result.eras = new String[] { "BC", "AD" }; + + result.longMonthNames = new String[] { "January", "February", "March", "April", "May", + "June", "July", "August", "September", "October", "November", "December" }; + result.shortMonthNames = new String[] { "Jan", "Feb", "Mar", "Apr", "May", + "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; + result.longStandAloneMonthNames = result.longMonthNames; + result.shortStandAloneMonthNames = result.shortMonthNames; + + result.longWeekdayNames = new String[] { + "Monday" ,"Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }; + result.shortWeekdayNames = new String[] { + "Mon" ,"Tue", "Wed", "Thu", "Fri", "Sat", "Sun" }; + result.longStandAloneWeekdayNames = result.longWeekdayNames; + result.shortStandAloneWeekdayNames = result.shortWeekdayNames; + + result.fullTimeFormat = ""; + result.longTimeFormat = ""; + result.mediumTimeFormat = ""; + result.shortTimeFormat = ""; + + result.fullDateFormat = ""; + result.longDateFormat = ""; + result.mediumDateFormat = ""; + result.shortDateFormat = ""; + + // Used by DecimalFormatSymbols. + result.zeroDigit = '0'; + result.decimalSeparator = '.'; + result.groupingSeparator = ','; + result.patternSeparator = ' '; + result.percent = '%'; + result.perMill = '\u2030'; + result.monetarySeparator = ' '; + result.minusSign = '-'; + result.exponentSeparator = "e"; + result.infinity = "\u221E"; + result.NaN = "NaN"; + // Also used by Currency. + result.currencySymbol = "$"; + result.internationalCurrencySymbol = "USD"; + + // Used by DecimalFormat and NumberFormat. + result.numberPattern = "%f"; + result.integerPattern = "%d"; + result.currencyPattern = "%s"; + result.percentPattern = "%f"; + + return true; + } +} diff --git a/tools/layoutlib/bridge/tests/.classpath b/tools/layoutlib/bridge/tests/.classpath new file mode 100644 index 000000000000..2b32e097de90 --- /dev/null +++ b/tools/layoutlib/bridge/tests/.classpath @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="src" path="res"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> + <classpathentry combineaccessrules="false" kind="src" path="/layoutlib_bridge"/> + <classpathentry kind="var" path="ANDROID_PLAT_SRC/prebuilts/misc/common/kxml2/kxml2-2.3.0.jar" sourcepath="/ANDROID_PLAT_SRC/dalvik/libcore/xml/src/main/java"/> + <classpathentry kind="var" path="ANDROID_PLAT_SRC/out/host/common/obj/JAVA_LIBRARIES/temp_layoutlib_intermediates/javalib.jar" sourcepath="/ANDROID_PLAT_SRC/frameworks/base"/> + <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/3"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/tools/layoutlib/bridge/tests/.project b/tools/layoutlib/bridge/tests/.project new file mode 100644 index 000000000000..2325eed16627 --- /dev/null +++ b/tools/layoutlib/bridge/tests/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>layoutlib_bridge-tests</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/tests/Android.mk b/tools/layoutlib/bridge/tests/Android.mk new file mode 100644 index 000000000000..98cade9f681d --- /dev/null +++ b/tools/layoutlib/bridge/tests/Android.mk @@ -0,0 +1,32 @@ +# Copyright (C) 2011 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +# Only compile source java files in this lib. +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +LOCAL_JAVA_RESOURCE_DIRS := res + +LOCAL_MODULE := layoutlib-tests +LOCAL_MODULE_TAGS := optional + +LOCAL_JAVA_LIBRARIES := layoutlib kxml2-2.3.0 junit + +include $(BUILD_HOST_JAVA_LIBRARY) + +# Build all sub-directories +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tools/layoutlib/bridge/tests/res/com/android/layoutlib/testdata/layout1.xml b/tools/layoutlib/bridge/tests/res/com/android/layoutlib/testdata/layout1.xml new file mode 100644 index 000000000000..b8fc9472ed83 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/com/android/layoutlib/testdata/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="match_parent" + android:layout_height="match_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="match_parent" + android:layout_height="match_parent" + android:layout_weight="2" + /> + <TextView + android:id="@+id/status" + android:paddingLeft="2dip" + android:layout_weight="0" + android:background="@drawable/black" + android:layout_width="match_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/bridge/tests/src/android/graphics/Matrix_DelegateTest.java b/tools/layoutlib/bridge/tests/src/android/graphics/Matrix_DelegateTest.java new file mode 100644 index 000000000000..ec4edacca5eb --- /dev/null +++ b/tools/layoutlib/bridge/tests/src/android/graphics/Matrix_DelegateTest.java @@ -0,0 +1,58 @@ +/* + * 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 junit.framework.TestCase; + +/** + * + */ +public class Matrix_DelegateTest extends TestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testIdentity() { + Matrix m1 = new Matrix(); + + assertTrue(m1.isIdentity()); + + m1.setValues(new float[] { 1,0,0, 0,1,0, 0,0,1 }); + assertTrue(m1.isIdentity()); + } + + public void testCopyConstructor() { + Matrix m1 = new Matrix(); + 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]); + } + } +} diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/TestDelegates.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/TestDelegates.java new file mode 100644 index 000000000000..d3218dbd5bd3 --- /dev/null +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/TestDelegates.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import com.android.tools.layoutlib.create.CreateInfo; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +/** + * Tests that native delegate classes implement all the required methods. + * + * This looks at {@link CreateInfo#DELEGATE_CLASS_NATIVES} to get the list of classes that + * have their native methods reimplemented through a delegate. + * + * Since the reimplemented methods are not native anymore, we look for the annotation + * {@link LayoutlibDelegate}, and look for a matching method in the delegate (named the same + * as the modified class with _Delegate added as a suffix). + * If the original native method is not static, then we make sure the delegate method also + * include the original class as first parameter (to access "this"). + * + */ +public class TestDelegates extends TestCase { + + public void testNativeDelegates() { + + final String[] classes = CreateInfo.DELEGATE_CLASS_NATIVES; + final int count = classes.length; + for (int i = 0 ; i < count ; i++) { + loadAndCompareClasses(classes[i], classes[i] + "_Delegate"); + } + } + + public void testMethodDelegates() { + final String[] methods = CreateInfo.DELEGATE_METHODS; + final int count = methods.length; + for (int i = 0 ; i < count ; i++) { + String methodName = methods[i]; + + // extract the class name + String className = methodName.substring(0, methodName.indexOf('#')); + String targetClassName = className.replace('$', '_') + "_Delegate"; + + loadAndCompareClasses(className, targetClassName); + } + } + + private void loadAndCompareClasses(String originalClassName, String delegateClassName) { + // load the classes + try { + ClassLoader classLoader = TestDelegates.class.getClassLoader(); + Class<?> originalClass = classLoader.loadClass(originalClassName); + Class<?> delegateClass = classLoader.loadClass(delegateClassName); + + compare(originalClass, delegateClass); + } catch (ClassNotFoundException e) { + fail("Failed to load class: " + e.getMessage()); + } catch (SecurityException e) { + fail("Failed to load class: " + e.getMessage()); + } + } + + private void compare(Class<?> originalClass, Class<?> delegateClass) throws SecurityException { + List<Method> checkedDelegateMethods = new ArrayList<Method>(); + + // loop on the methods of the original class, and for the ones that are annotated + // with @LayoutlibDelegate, look for a matching method in the delegate class. + // The annotation is automatically added by layoutlib_create when it replace a method + // by a call to a delegate + Method[] originalMethods = originalClass.getDeclaredMethods(); + for (Method originalMethod : originalMethods) { + // look for methods that are delegated: they have the LayoutlibDelegate annotation + if (originalMethod.getAnnotation(LayoutlibDelegate.class) == null) { + continue; + } + + // get the signature. + Class<?>[] parameters = originalMethod.getParameterTypes(); + + // if the method is not static, then the class is added as the first parameter + // (for "this") + if ((originalMethod.getModifiers() & Modifier.STATIC) == 0) { + + Class<?>[] newParameters = new Class<?>[parameters.length + 1]; + newParameters[0] = originalClass; + System.arraycopy(parameters, 0, newParameters, 1, parameters.length); + parameters = newParameters; + } + + // if the original class is an inner class that's not static, then + // we add this on the enclosing class at the beginning + if (originalClass.getEnclosingClass() != null && + (originalClass.getModifiers() & Modifier.STATIC) == 0) { + Class<?>[] newParameters = new Class<?>[parameters.length + 1]; + newParameters[0] = originalClass.getEnclosingClass(); + System.arraycopy(parameters, 0, newParameters, 1, parameters.length); + parameters = newParameters; + } + + try { + // try to load the method with the given parameter types. + Method delegateMethod = delegateClass.getDeclaredMethod(originalMethod.getName(), + parameters); + + // check that the method has the annotation + assertNotNull( + String.format( + "Delegate method %1$s for class %2$s does not have the @LayoutlibDelegate annotation", + delegateMethod.getName(), + originalClass.getName()), + delegateMethod.getAnnotation(LayoutlibDelegate.class)); + + // check that the method is static + assertTrue( + String.format( + "Delegate method %1$s for class %2$s is not static", + delegateMethod.getName(), + originalClass.getName()), + (delegateMethod.getModifiers() & Modifier.STATIC) == Modifier.STATIC); + + // add the method as checked. + checkedDelegateMethods.add(delegateMethod); + } catch (NoSuchMethodException e) { + String name = getMethodName(originalMethod, parameters); + fail(String.format("Missing %1$s.%2$s", delegateClass.getName(), name)); + } + } + + // look for dead (delegate) code. + // This looks for all methods in the delegate class, and if they have the + // @LayoutlibDelegate annotation, make sure they have been previously found as a + // match for a method in the original class. + // If not, this means the method is a delegate for a method that either doesn't exist + // anymore or is not delegated anymore. + Method[] delegateMethods = delegateClass.getDeclaredMethods(); + for (Method delegateMethod : delegateMethods) { + // look for methods that are delegates: they have the LayoutlibDelegate annotation + if (delegateMethod.getAnnotation(LayoutlibDelegate.class) == null) { + continue; + } + + assertTrue( + String.format( + "Delegate method %1$s.%2$s is not used anymore and must be removed", + delegateClass.getName(), + getMethodName(delegateMethod)), + checkedDelegateMethods.contains(delegateMethod)); + } + + } + + private String getMethodName(Method method) { + return getMethodName(method, method.getParameterTypes()); + } + + private String getMethodName(Method method, Class<?>[] parameters) { + // compute a full class name that's long but not too long. + StringBuilder sb = new StringBuilder(method.getName() + "("); + for (int j = 0; j < parameters.length; j++) { + Class<?> theClass = parameters[j]; + sb.append(theClass.getName()); + int dimensions = 0; + while (theClass.isArray()) { + dimensions++; + theClass = theClass.getComponentType(); + } + for (int i = 0; i < dimensions; i++) { + sb.append("[]"); + } + if (j < (parameters.length - 1)) { + sb.append(","); + } + } + sb.append(")"); + + return sb.toString(); + } +} diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/android/BridgeXmlBlockParserTest.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/android/BridgeXmlBlockParserTest.java new file mode 100644 index 000000000000..865a008a7457 --- /dev/null +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/android/BridgeXmlBlockParserTest.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.android.layoutlib.bridge.android; + +import com.android.layoutlib.bridge.impl.ParserFactory; + +import org.w3c.dom.Node; +import org.xmlpull.v1.XmlPullParser; + +import junit.framework.TestCase; + +public class BridgeXmlBlockParserTest extends TestCase { + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testXmlBlockParser() throws Exception { + + XmlPullParser parser = ParserFactory.create( + getClass().getResourceAsStream("/com/android/layoutlib/testdata/layout1.xml"), + "layout1.xml"); + + parser = new BridgeXmlBlockParser(parser, null, false /* platformResourceFlag */); + + 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()); + } + + //------------ + + /** + * 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/create/.classpath b/tools/layoutlib/create/.classpath new file mode 100644 index 000000000000..dbc4cfd392a7 --- /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/prebuilts/tools/common/asm-tools/asm-4.0.jar"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/tools/layoutlib/create/.project b/tools/layoutlib/create/.project new file mode 100644 index 000000000000..e100d175ee98 --- /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/.settings/README.txt b/tools/layoutlib/create/.settings/README.txt new file mode 100644 index 000000000000..9120b20710a3 --- /dev/null +++ b/tools/layoutlib/create/.settings/README.txt @@ -0,0 +1,2 @@ +Copy this in eclipse project as a .settings folder at the root. +This ensure proper compilation compliance and warning/error levels.
\ No newline at end of file diff --git a/tools/layoutlib/create/.settings/org.eclipse.jdt.core.prefs b/tools/layoutlib/create/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000000..5381a0e16c7d --- /dev/null +++ b/tools/layoutlib/create/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,93 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled +org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullReference=error +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/tools/layoutlib/create/Android.mk b/tools/layoutlib/create/Android.mk new file mode 100644 index 000000000000..9bd48ab7fddd --- /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-4.0 + +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 000000000000..894611b3e728 --- /dev/null +++ b/tools/layoutlib/create/README.txt @@ -0,0 +1,240 @@ +# Copyright (C) 2008 The Android Open Source Project + + +- Description - +--------------- + +Layoutlib_create generates a JAR library used by the Eclipse graphical layout editor +to perform layout. + + +- Usage - +--------- + + ./layoutlib_create path/to/android.jar destination.jar + + +- Design Overview - +------------------- + +Layoutlib_create uses the "android.jar" containing all the Java code used by Android +as generated by the Android build, right before the classes are converted to a DEX format. + +The Android JAR can't be used directly in Eclipse: +- it contains references to native code (which we want to avoid in Eclipse), +- some classes need to be overridden, for example all the drawing code that is + replaced by Java 2D calls in Eclipse. +- some of the classes that need to be changed are final and/or we need access + to their private internal state. + +Consequently this tool: +- parses the input JAR, +- modifies some of the classes directly using some bytecode manipulation, +- filters some packages and removes those we don't want in the output JAR, +- injects some new classes, +- generates a modified JAR file that is suitable for the Android plugin + for Eclipse to perform rendering. + +The ASM library is used to do the bytecode modification using its visitor pattern API. + +The layoutlib_create is *NOT* generic. There is no configuration file. Instead all the +configuration is done in the main() method and the CreateInfo structure is expected to +change with the Android platform as new classes are added, changed or removed. + +The resulting JAR is used by layoutlib_bridge (a.k.a. "the bridge"), also part of the +platform, that provides all the necessary missing implementation for rendering graphics +in Eclipse. + + + +- Implementation Notes - +------------------------ + +The tool works in two phases: +- first analyze the input jar (AsmAnalyzer class) +- then generate the output jar (AsmGenerator class), + + +- Analyzer +---------- + +The goal of the analyzer is to create a graph of all the classes from the input JAR +with their dependencies and then only keep the ones we want. + +To do that, the analyzer is created with a list of base classes to keep -- everything +that derives from these is kept. Currently the one such class is android.view.View: +since we want to render layouts, anything that is sort of a view needs to be kept. + +The analyzer is also given a list of class names to keep in the output. +This is done using shell-like glob patterns that filter on the fully-qualified +class names, for example "android.*.R**" ("*" does not matches dots whilst "**" does, +and "." and "$" are interpreted as-is). +In practice we almost but not quite request the inclusion of full packages. + +With this information, the analyzer parses the input zip to find all the classes. +All classes deriving from the requested bases classes are kept. +All classes which name matched the glob pattern are kept. +The analysis then finds all the dependencies of the classes that are to be kept +using an ASM visitor on the class, the field types, the method types and annotations types. +Classes that belong to the current JRE are excluded. + +The output of the analyzer is a set of ASM ClassReader instances which are then +fed to the generator. + + +- Generator +----------- + +The generator is constructed from a CreateInfo struct that acts as a config file +and lists: +- the classes to inject in the output JAR -- these classes are directly implemented + in layoutlib_create and will be used to interface with the renderer in Eclipse. +- specific methods to override (see method stubs details below). +- specific methods for which to delegate calls. +- specific methods to remove based on their return type. +- specific classes to rename. + +Each of these are specific strategies we use to be able to modify the Android code +to fit within the Eclipse renderer. These strategies are explained beow. + +The core method of the generator is transform(): it takes an input ASM ClassReader +and modifies it to produce a byte array suitable for the final JAR file. + +The first step of the transformation is changing the name of the class in case +we requested the class to be renamed. This uses the RenameClassAdapter to also rename +all inner classes and references in methods and types. Note that other classes are +not transformed and keep referencing the original name. + +The TransformClassAdapter is then used to process the potentially renamed class. +All protected or private classes are market as public. +All classes are made non-final. +Interfaces are left as-is. + +If a method has a return type that must be erased, the whole method is skipped. +Methods are also changed from protected/private to public. +The code of the methods is then kept as-is, except for native methods which are +replaced by a stub. Methods that are to be overridden are also replaced by a stub. + +The transformed class is then fed through the DelegateClassAdapter to implement +method delegates. + +Finally fields are also visited and changed from protected/private to public. + + +- Method stubs +-------------- + +As indicated above, all native and overridden methods are replaced by a stub. +We don't have the code to replace with in layoutlib_create. +Instead the StubMethodAdapter replaces the code of the method by a call to +OverrideMethod.invokeX(). When using the final JAR, the bridge can register +listeners from these overridden method calls based on the method signatures. + +The listeners are currently pretty basic: we only pass the signature of the +method being called, its caller object and a flag indicating whether the +method was native. We do not currently provide the parameters. The listener +can however specify the return value of the overridden method. + +This strategy is now obsolete and replaced by the method delegates. + + +- Strategies +------------ + +We currently have 4 strategies to deal with overriding the rendering code +and make it run in Eclipse. Most of these strategies are implemented hand-in-hand +by the bridge (which runs in Eclipse) and the generator. + + +1- Class Injection + +This is the easiest: we currently inject 4 classes, namely: +- OverrideMethod and its associated MethodListener and MethodAdapter are used + to intercept calls to some specific methods that are stubbed out and change + their return value. +- CreateInfo class, which configured the generator. Not used yet, but could + in theory help us track what the generator changed. + + +2- Overriding methods + +As explained earlier, the creator doesn't have any replacement code for +methods to override. Instead it removes the original code and replaces it +by a call to a specific OveriddeMethod.invokeX(). The bridge then registers +a listener on the method signature and can provide an implementation. + +This strategy is now obsolete and replaced by the method delegates. +See strategy 5 below. + + +3- Renaming classes + +This simply changes the name of a class in its definition, as well as all its +references in internal inner classes and methods. +Calls from other classes are not modified -- they keep referencing the original +class name. This allows the bridge to literally replace an implementation. + +An example will make this easier: android.graphics.Paint is the main drawing +class that we need to replace. To do so, the generator renames Paint to _original_Paint. +Later the bridge provides its own replacement version of Paint which will be used +by the rest of the Android stack. The replacement version of Paint can still use +(either by inheritance or delegation) all the original non-native code of _original_Paint +if it so desires. + +Some of the Android classes are basically wrappers over native objects and since +we don't have the native code in Eclipse, we need to provide a full alternate +implementation. Sub-classing doesn't work as some native methods are static and +we don't control object creation. + +This won't rename/replace the inner static methods of a given class. + + +4- Method erasure based on return type + +This is mostly an implementation detail of the bridge: in the Paint class +mentioned above, some inner static classes are used to pass around +attributes (e.g. FontMetrics, or the Style enum) and all the original implementation +is native. + +In this case we have a strategy that tells the generator that anything returning, for +example, the inner class Paint$Style in the Paint class should be discarded and the +bridge will provide its own implementation. + + +5- Method Delegates + +This strategy is used to override method implementations. +Given a method SomeClass.MethodName(), 1 or 2 methods are generated: +a- A copy of the original method named SomeClass.MethodName_Original(). + The content is the original method as-is from the reader. + This step is omitted if the method is native, since it has no Java implementation. +b- A brand new implementation of SomeClass.MethodName() which calls to a + non-existing static method named SomeClass_Delegate.MethodName(). + The implementation of this 'delegate' method is done in layoutlib_brigde. + +The delegate method is a static method. +If the original method is non-static, the delegate method receives the original 'this' +as its first argument. If the original method is an inner non-static method, it also +receives the inner 'this' as the second argument. + + + +- References - +-------------- + + +The JVM Specification 2nd edition: + http://java.sun.com/docs/books/jvms/second_edition/html/VMSpecTOC.doc.html + +Understanding bytecode: + http://www.ibm.com/developerworks/ibm/library/it-haggar_bytecode/ + +Bytecode opcode list: + http://en.wikipedia.org/wiki/Java_bytecode_instruction_listings + +ASM user guide: + http://download.forge.objectweb.org/asm/asm-guide.pdf + + +-- +end diff --git a/tools/layoutlib/create/manifest.txt b/tools/layoutlib/create/manifest.txt new file mode 100644 index 000000000000..238e7f90639a --- /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/annotations/LayoutlibDelegate.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/LayoutlibDelegate.java new file mode 100644 index 000000000000..9a48ea6d0841 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/LayoutlibDelegate.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Denotes a method that has been converted to a delegate by layoutlib_create. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface LayoutlibDelegate { +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/Nullable.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/Nullable.java new file mode 100644 index 000000000000..0689c92c6cbc --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/Nullable.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Denotes a parameter or field can be null. + * <p/> + * When decorating a method call parameter, this denotes the parameter can + * legitimately be null and the method will gracefully deal with it. Typically used + * on optional parameters. + * <p/> + * When decorating a method, this denotes the method might legitimately return null. + * <p/> + * This is a marker annotation and it has no specific attributes. + */ +@Retention(RetentionPolicy.SOURCE) +public @interface Nullable { +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/VisibleForTesting.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/VisibleForTesting.java new file mode 100644 index 000000000000..e4e016b03cbb --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/annotations/VisibleForTesting.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Denotes that the class, method or field has its visibility relaxed so + * that unit tests can access it. + * <p/> + * The <code>visibility</code> argument can be used to specific what the original + * visibility should have been if it had not been made public or package-private for testing. + * The default is to consider the element private. + */ +@Retention(RetentionPolicy.SOURCE) +public @interface VisibleForTesting { + /** + * Intended visibility if the element had not been made public or package-private for + * testing. + */ + enum Visibility { + /** The element should be considered protected. */ + PROTECTED, + /** The element should be considered package-private. */ + PACKAGE, + /** The element should be considered private. */ + PRIVATE + } + + /** + * Intended visibility if the element had not been made public or package-private for testing. + * If not specified, one should assume the element originally intended to be private. + */ + Visibility visibility() default Visibility.PRIVATE; +} 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 000000000000..412695f2661a --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java @@ -0,0 +1,845 @@ +/* + * 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.Opcodes; +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.Map.Entry; +import java.util.TreeMap; +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 extends ClassVisitor { + + /** 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) { + super(Opcodes.ASM4); + 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(new MySignatureVisitor()); + } + } + + /** + * 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 + @Override + 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); + } + + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public void visitAttribute(Attribute attr) { + // pass + } + + // Visits the end of a class + @Override + public void visitEnd() { + // pass + } + + private class MyFieldVisitor extends FieldVisitor { + + public MyFieldVisitor() { + super(Opcodes.ASM4); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public void visitAttribute(Attribute attr) { + // pass + } + + // Visits the end of a class + @Override + public void visitEnd() { + // pass + } + } + + @Override + 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 new MyFieldVisitor(); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // name is the internal name of an inner class (see getInternalName). + considerName(name); + } + + @Override + 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 new MyMethodVisitor(); + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + // pass + } + + @Override + public void visitSource(String source, String debug) { + // pass + } + + + // --------------------------------------------------- + // --- MethodVisitor + // --------------------------------------------------- + + private class MyMethodVisitor extends MethodVisitor { + + public MyMethodVisitor() { + super(Opcodes.ASM4); + } + + + @Override + public AnnotationVisitor visitAnnotationDefault() { + return new MyAnnotationVisitor(); + } + + @Override + public void visitCode() { + // pass + } + + // field instruction + @Override + 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); + } + + @Override + public void visitFrame(int type, int local, Object[] local2, int stack, Object[] stack2) { + // pass + } + + @Override + public void visitIincInsn(int var, int increment) { + // pass -- an IINC instruction + } + + @Override + public void visitInsn(int opcode) { + // pass -- a zero operand instruction + } + + @Override + public void visitIntInsn(int opcode, int operand) { + // pass -- a single int operand instruction + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + // pass -- a jump instruction + } + + @Override + public void visitLabel(Label label) { + // pass -- a label target + } + + // instruction to load a constant from the stack + @Override + public void visitLdcInsn(Object cst) { + if (cst instanceof Type) { + considerType((Type) cst); + } + } + + @Override + public void visitLineNumber(int line, Label start) { + // pass + } + + @Override + 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); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + // pass -- a lookup switch instruction + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + // pass + } + + // instruction that invokes a method + @Override + 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 + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + + // desc an array type descriptor. + considerDesc(desc); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, + boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + // pass -- table switch instruction + + } + + @Override + 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 + @Override + 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); + } + + @Override + public void visitVarInsn(int opcode, int var) { + // pass -- local variable instruction + } + } + + private class MySignatureVisitor extends SignatureVisitor { + + public MySignatureVisitor() { + super(Opcodes.ASM4); + } + + // --------------------------------------------------- + // --- SignatureVisitor + // --------------------------------------------------- + + private String mCurrentSignatureClass = null; + + // Starts the visit of a signature corresponding to a class or interface type + @Override + public void visitClassType(String name) { + mCurrentSignatureClass = name; + considerName(name); + } + + // Visits an inner class + @Override + public void visitInnerClassType(String name) { + if (mCurrentSignatureClass != null) { + mCurrentSignatureClass += "$" + name; + considerName(mCurrentSignatureClass); + } + } + + @Override + public SignatureVisitor visitArrayType() { + return new MySignatureVisitor(); + } + + @Override + public void visitBaseType(char descriptor) { + // pass -- a primitive type, ignored + } + + @Override + public SignatureVisitor visitClassBound() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitExceptionType() { + return new MySignatureVisitor(); + } + + @Override + public void visitFormalTypeParameter(String name) { + // pass + } + + @Override + public SignatureVisitor visitInterface() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitInterfaceBound() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitParameterType() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitReturnType() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitSuperclass() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitTypeArgument(char wildcard) { + return new MySignatureVisitor(); + } + + @Override + public void visitTypeVariable(String name) { + // pass + } + + @Override + public void visitTypeArgument() { + // pass + } + } + + + // --------------------------------------------------- + // --- AnnotationVisitor + // --------------------------------------------------- + + private class MyAnnotationVisitor extends AnnotationVisitor { + + public MyAnnotationVisitor() { + super(Opcodes.ASM4); + } + + // Visits a primitive value of an annotation + @Override + 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); + } + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String desc) { + // desc is the class descriptor of the nested annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public AnnotationVisitor visitArray(String name) { + return new MyAnnotationVisitor(); + } + + @Override + 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 000000000000..a9ede265b5d8 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java @@ -0,0 +1,370 @@ +/* + * 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.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +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 => set { list of return types to delete from the FQCN } }. */ + private HashMap<String, Set<String>> mDeleteReturns; + /** A map { FQCN => set { method names } } of methods to rewrite as delegates. + * The special name {@link DelegateClassAdapter#ALL_NATIVES} can be used as in internal set. */ + private final HashMap<String, Set<String>> mDelegateMethods; + + /** + * 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 createInfo Creation parameters. Must not be null. + */ + public AsmGenerator(Log log, String osDestJar, ICreateInfo createInfo) { + mLog = log; + mOsDestJar = osDestJar; + mInjectClasses = createInfo.getInjectedClasses(); + mStubMethods = new HashSet<String>(Arrays.asList(createInfo.getOverriddenMethods())); + + // Create the map/set of methods to change to delegates + mDelegateMethods = new HashMap<String, Set<String>>(); + for (String signature : createInfo.getDelegateMethods()) { + int pos = signature.indexOf('#'); + if (pos <= 0 || pos >= signature.length() - 1) { + continue; + } + String className = binaryToInternalClassName(signature.substring(0, pos)); + String methodName = signature.substring(pos + 1); + Set<String> methods = mDelegateMethods.get(className); + if (methods == null) { + methods = new HashSet<String>(); + mDelegateMethods.put(className, methods); + } + methods.add(methodName); + } + for (String className : createInfo.getDelegateClassNatives()) { + className = binaryToInternalClassName(className); + Set<String> methods = mDelegateMethods.get(className); + if (methods == null) { + methods = new HashSet<String>(); + mDelegateMethods.put(className, methods); + } + methods.add(DelegateClassAdapter.ALL_NATIVES); + } + + // Create the map of classes to rename. + mRenameClasses = new HashMap<String, String>(); + mClassesNotRenamed = new HashSet<String>(); + String[] renameClasses = createInfo.getRenamedClasses(); + int n = 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>>(); + String[] deleteReturns = createInfo.getDeleteReturns(); + 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); + + // Get the class name, as an internal name (e.g. com/android/SomeClass$InnerClass) + 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); + } + + ClassVisitor cv = new TransformClassAdapter(mLog, mStubMethods, + mDeleteReturns.get(className), + newName, rv, + stubNativesOnly, stubNativesOnly || hasNativeMethods); + + Set<String> delegateMethods = mDelegateMethods.get(className); + if (delegateMethods != null && !delegateMethods.isEmpty()) { + // If delegateMethods only contains one entry ALL_NATIVES and the class is + // known to have no native methods, just skip this step. + if (hasNativeMethods || + !(delegateMethods.size() == 1 && + delegateMethods.contains(DelegateClassAdapter.ALL_NATIVES))) { + cv = new DelegateClassAdapter(mLog, cv, className, delegateMethods); + } + } + + 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 000000000000..2c955fd9d9bb --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.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. + */ + +package com.android.tools.layoutlib.create; + +import com.android.tools.layoutlib.annotations.VisibleForTesting; +import com.android.tools.layoutlib.annotations.VisibleForTesting.Visibility; + +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 extends ClassVisitor { + public ClassHasNativeVisitor() { + super(Opcodes.ASM4); + } + + private boolean mHasNativeMethods = false; + + public boolean hasNativeMethods() { + return mHasNativeMethods; + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected void setHasNativeMethods(boolean hasNativeMethods, String methodName) { + mHasNativeMethods = hasNativeMethods; + } + + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + // pass + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // pass + return null; + } + + @Override + public void visitAttribute(Attribute attr) { + // pass + } + + @Override + public void visitEnd() { + // pass + } + + @Override + public FieldVisitor visitField(int access, String name, String desc, + String signature, Object value) { + // pass + return null; + } + + @Override + public void visitInnerClass(String name, String outerName, + String innerName, int access) { + // pass + } + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + if ((access & Opcodes.ACC_NATIVE) != 0) { + setHasNativeMethods(true, name); + } + return null; + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + // pass + } + + @Override + public void visitSource(String source, String debug) { + // pass + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java new file mode 100644 index 000000000000..d9550404d48a --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java @@ -0,0 +1,207 @@ +/* + * 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 com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Describes the work to be done by {@link AsmGenerator}. + */ +public final class CreateInfo implements ICreateInfo { + + /** + * Returns the list of class from layoutlib_create to inject in layoutlib. + * The list can be empty but must not be null. + */ + @Override + public Class<?>[] getInjectedClasses() { + return INJECTED_CLASSES; + } + + /** + * Returns the list of methods to rewrite as delegates. + * The list can be empty but must not be null. + */ + @Override + public String[] getDelegateMethods() { + return DELEGATE_METHODS; + } + + /** + * Returns the list of classes on which to delegate all native methods. + * The list can be empty but must not be null. + */ + @Override + public String[] getDelegateClassNatives() { + return DELEGATE_CLASS_NATIVES; + } + + /** + * Returns The list of methods to stub out. Each entry must be in the form + * "package.package.OuterClass$InnerClass#MethodName". + * The list can be empty but must not be null. + * <p/> + * This usage is deprecated. Please use method 'delegates' instead. + */ + @Override + public String[] getOverriddenMethods() { + return OVERRIDDEN_METHODS; + } + + /** + * Returns the list of classes to rename, must be an even list: the binary FQCN + * of class to replace followed by the new FQCN. + * The list can be empty but must not be null. + */ + @Override + public String[] getRenamedClasses() { + return RENAMED_CLASSES; + } + + /** + * Returns the 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. + * The list can be empty but must not be null. + */ + @Override + public String[] getDeleteReturns() { + return DELETE_RETURNS; + } + + //----- + + /** + * The list of class from layoutlib_create to inject in layoutlib. + */ + private final static Class<?>[] INJECTED_CLASSES = new Class<?>[] { + OverrideMethod.class, + MethodListener.class, + MethodAdapter.class, + ICreateInfo.class, + CreateInfo.class, + LayoutlibDelegate.class + }; + + /** + * The list of methods to rewrite as delegates. + */ + public final static String[] DELEGATE_METHODS = new String[] { + "android.app.Fragment#instantiate", //(Landroid/content/Context;Ljava/lang/String;Landroid/os/Bundle;)Landroid/app/Fragment;", + "android.content.res.Resources$Theme#obtainStyledAttributes", + "android.content.res.Resources$Theme#resolveAttribute", + "android.content.res.TypedArray#getValueAt", + "android.graphics.BitmapFactory#finishDecode", + "android.os.Handler#sendMessageAtTime", + "android.os.HandlerThread#run", + "android.os.Build#getString", + "android.text.format.DateFormat#is24HourFormat", + "android.view.Choreographer#getRefreshRate", + "android.view.Display#updateDisplayInfoLocked", + "android.view.LayoutInflater#rInflate", + "android.view.LayoutInflater#parseInclude", + "android.view.View#isInEditMode", + "android.view.ViewRootImpl#isInTouchMode", + "android.view.WindowManagerGlobal#getWindowManagerService", + "android.view.inputmethod.InputMethodManager#getInstance", + "com.android.internal.util.XmlUtils#convertValueToInt", + "com.android.internal.textservice.ITextServicesManager$Stub#asInterface", + }; + + /** + * The list of classes on which to delegate all native methods. + */ + public final static String[] DELEGATE_CLASS_NATIVES = new String[] { + "android.animation.PropertyValuesHolder", + "android.graphics.AvoidXfermode", + "android.graphics.Bitmap", + "android.graphics.BitmapFactory", + "android.graphics.BitmapShader", + "android.graphics.BlurMaskFilter", + "android.graphics.Canvas", + "android.graphics.ColorFilter", + "android.graphics.ColorMatrixColorFilter", + "android.graphics.ComposePathEffect", + "android.graphics.ComposeShader", + "android.graphics.CornerPathEffect", + "android.graphics.DashPathEffect", + "android.graphics.DiscretePathEffect", + "android.graphics.DrawFilter", + "android.graphics.EmbossMaskFilter", + "android.graphics.LayerRasterizer", + "android.graphics.LightingColorFilter", + "android.graphics.LinearGradient", + "android.graphics.MaskFilter", + "android.graphics.Matrix", + "android.graphics.NinePatch", + "android.graphics.Paint", + "android.graphics.PaintFlagsDrawFilter", + "android.graphics.Path", + "android.graphics.PathDashPathEffect", + "android.graphics.PathEffect", + "android.graphics.PixelXorXfermode", + "android.graphics.PorterDuffColorFilter", + "android.graphics.PorterDuffXfermode", + "android.graphics.RadialGradient", + "android.graphics.Rasterizer", + "android.graphics.Region", + "android.graphics.Shader", + "android.graphics.SumPathEffect", + "android.graphics.SweepGradient", + "android.graphics.Typeface", + "android.graphics.Xfermode", + "android.os.SystemClock", + "android.text.AndroidBidi", + "android.util.FloatMath", + "android.view.Display", + "libcore.icu.ICU", + }; + + /** + * The list of methods to stub out. Each entry must be in the form + * "package.package.OuterClass$InnerClass#MethodName". + * This usage is deprecated. Please use method 'delegates' instead. + */ + private final static String[] OVERRIDDEN_METHODS = new String[] { + }; + + /** + * The list of classes to rename, must be an even list: the binary FQCN + * of class to replace followed by the new FQCN. + */ + private final static String[] RENAMED_CLASSES = + new String[] { + "android.os.ServiceManager", "android.os._Original_ServiceManager", + "android.util.LruCache", "android.util._Original_LruCache", + "android.view.SurfaceView", "android.view._Original_SurfaceView", + "android.view.accessibility.AccessibilityManager", "android.view.accessibility._Original_AccessibilityManager", + "android.webkit.WebView", "android.webkit._Original_WebView", + "com.android.internal.policy.PolicyManager", "com.android.internal.policy._Original_PolicyManager", + }; + + /** + * 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. + */ + private final static String[] DELETE_RETURNS = + new String[] { + null }; // separator, for next class/methods list. +} + diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java new file mode 100644 index 000000000000..927be972fadf --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.util.Set; + +/** + * A {@link DelegateClassAdapter} can transform some methods from a class into + * delegates that defer the call to an associated delegate class. + * <p/> + * This is used to override specific methods and or all native methods in classes. + */ +public class DelegateClassAdapter extends ClassVisitor { + + /** Suffix added to original methods. */ + private static final String ORIGINAL_SUFFIX = "_Original"; + private static String CONSTRUCTOR = "<init>"; + private static String CLASS_INIT = "<clinit>"; + + public final static String ALL_NATIVES = "<<all_natives>>"; + + private final String mClassName; + private final Set<String> mDelegateMethods; + private final Log mLog; + + /** + * Creates a new {@link DelegateClassAdapter} that can transform some methods + * from a class into delegates that defer the call to an associated delegate class. + * <p/> + * This is used to override specific methods and or all native methods in classes. + * + * @param log The logger object. Must not be null. + * @param cv the class visitor to which this adapter must delegate calls. + * @param className The internal class name of the class to visit, + * e.g. <code>com/android/SomeClass$InnerClass</code>. + * @param delegateMethods The set of method names to modify and/or the + * special constant {@link #ALL_NATIVES} to convert all native methods. + */ + public DelegateClassAdapter(Log log, + ClassVisitor cv, + String className, + Set<String> delegateMethods) { + super(Opcodes.ASM4, cv); + mLog = log; + mClassName = className; + mDelegateMethods = delegateMethods; + } + + //---------------------------------- + // Methods from the ClassAdapter + + @Override + public MethodVisitor visitMethod(int access, String name, String desc, + String signature, String[] exceptions) { + + boolean isStatic = (access & Opcodes.ACC_STATIC) != 0; + boolean isNative = (access & Opcodes.ACC_NATIVE) != 0; + + boolean useDelegate = (isNative && mDelegateMethods.contains(ALL_NATIVES)) || + mDelegateMethods.contains(name); + + if (!useDelegate) { + // Not creating a delegate for this method, pass it as-is from the reader + // to the writer. + return super.visitMethod(access, name, desc, signature, exceptions); + } + + if (useDelegate) { + if (CONSTRUCTOR.equals(name) || CLASS_INIT.equals(name)) { + // We don't currently support generating delegates for constructors. + throw new UnsupportedOperationException( + String.format( + "Delegate doesn't support overriding constructor %1$s:%2$s(%3$s)", //$NON-NLS-1$ + mClassName, name, desc)); + } + } + + if (isNative) { + // Remove native flag + access = access & ~Opcodes.ACC_NATIVE; + MethodVisitor mwDelegate = super.visitMethod(access, name, desc, signature, exceptions); + + DelegateMethodAdapter2 a = new DelegateMethodAdapter2( + mLog, null /*mwOriginal*/, mwDelegate, mClassName, name, desc, isStatic); + + // A native has no code to visit, so we need to generate it directly. + a.generateDelegateCode(); + + return mwDelegate; + } + + // Given a non-native SomeClass.MethodName(), we want to generate 2 methods: + // - A copy of the original method named SomeClass.MethodName_Original(). + // The content is the original method as-is from the reader. + // - A brand new implementation of SomeClass.MethodName() which calls to a + // non-existing method named SomeClass_Delegate.MethodName(). + // The implementation of this 'delegate' method is done in layoutlib_brigde. + + int accessDelegate = access; + // change access to public for the original one + if (Main.sOptions.generatePublicAccess) { + access &= ~(Opcodes.ACC_PROTECTED | Opcodes.ACC_PRIVATE); + access |= Opcodes.ACC_PUBLIC; + } + + MethodVisitor mwOriginal = super.visitMethod(access, name + ORIGINAL_SUFFIX, + desc, signature, exceptions); + MethodVisitor mwDelegate = super.visitMethod(accessDelegate, name, + desc, signature, exceptions); + + DelegateMethodAdapter2 a = new DelegateMethodAdapter2( + mLog, mwOriginal, mwDelegate, mClassName, name, desc, isStatic); + return a; + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter2.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter2.java new file mode 100644 index 000000000000..0000b22ab57a --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter2.java @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Attribute; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import java.util.ArrayList; + +/** + * This method adapter generates delegate methods. + * <p/> + * Given a method {@code SomeClass.MethodName()}, this generates 1 or 2 methods: + * <ul> + * <li> A copy of the original method named {@code SomeClass.MethodName_Original()}. + * The content is the original method as-is from the reader. + * This step is omitted if the method is native, since it has no Java implementation. + * <li> A brand new implementation of {@code SomeClass.MethodName()} which calls to a + * non-existing method named {@code SomeClass_Delegate.MethodName()}. + * The implementation of this 'delegate' method is done in layoutlib_brigde. + * </ul> + * A method visitor is generally constructed to generate a single method; however + * here we might want to generate one or two depending on the context. To achieve + * that, the visitor here generates the 'original' method and acts as a no-op if + * no such method exists (e.g. when the original is a native method). + * The delegate method is generated after the {@code visitEnd} of the original method + * or by having the class adapter <em>directly</em> call {@link #generateDelegateCode()} + * for native methods. + * <p/> + * When generating the 'delegate', the implementation generates a call to a class + * class named <code><className>_Delegate</code> with static methods matching + * the methods to be overridden here. The methods have the same return type. + * The argument type list is the same except the "this" reference is passed first + * for non-static methods. + * <p/> + * A new annotation is added to these 'delegate' methods so that we can easily find them + * for automated testing. + * <p/> + * This class isn't intended to be generic or reusable. + * It is called by {@link DelegateClassAdapter}, which takes care of properly initializing + * the two method writers for the original and the delegate class, as needed, with their + * expected names. + * <p/> + * The class adapter also takes care of calling {@link #generateDelegateCode()} directly for + * a native and use the visitor pattern for non-natives. + * Note that native methods have, by definition, no code so there's nothing a visitor + * can visit. + * <p/> + * Instances of this class are not re-usable. + * The class adapter creates a new instance for each method. + */ +class DelegateMethodAdapter2 extends MethodVisitor { + + /** Suffix added to delegate classes. */ + public static final String DELEGATE_SUFFIX = "_Delegate"; + + /** The parent method writer to copy of the original method. + * Null when dealing with a native original method. */ + private MethodVisitor mOrgWriter; + /** The parent method writer to generate the delegating method. Never null. */ + private MethodVisitor mDelWriter; + /** The original method descriptor (return type + argument types.) */ + private String mDesc; + /** True if the original method is static. */ + private final boolean mIsStatic; + /** The internal class name (e.g. <code>com/android/SomeClass$InnerClass</code>.) */ + private final String mClassName; + /** The method name. */ + private final String mMethodName; + /** Logger object. */ + private final Log mLog; + + /** Array used to capture the first line number information from the original method + * and duplicate it in the delegate. */ + private Object[] mDelegateLineNumber; + + /** + * Creates a new {@link DelegateMethodAdapter2} that will transform this method + * into a delegate call. + * <p/> + * See {@link DelegateMethodAdapter2} for more details. + * + * @param log The logger object. Must not be null. + * @param mvOriginal The parent method writer to copy of the original method. + * Must be {@code null} when dealing with a native original method. + * @param mvDelegate The parent method writer to generate the delegating method. + * Must never be null. + * @param className The internal class name of the class to visit, + * e.g. <code>com/android/SomeClass$InnerClass</code>. + * @param methodName The simple name of the method. + * @param desc A method descriptor (c.f. {@link Type#getReturnType(String)} + + * {@link Type#getArgumentTypes(String)}) + * @param isStatic True if the method is declared static. + */ + public DelegateMethodAdapter2(Log log, + MethodVisitor mvOriginal, + MethodVisitor mvDelegate, + String className, + String methodName, + String desc, + boolean isStatic) { + super(Opcodes.ASM4); + mLog = log; + mOrgWriter = mvOriginal; + mDelWriter = mvDelegate; + mClassName = className; + mMethodName = methodName; + mDesc = desc; + mIsStatic = isStatic; + } + + /** + * Generates the new code for the method. + * <p/> + * For native methods, this must be invoked directly by {@link DelegateClassAdapter} + * (since they have no code to visit). + * <p/> + * Otherwise for non-native methods the {@link DelegateClassAdapter} simply needs to + * return this instance of {@link DelegateMethodAdapter2} and let the normal visitor pattern + * invoke it as part of the {@link ClassReader#accept(ClassVisitor, int)} workflow and then + * this method will be invoked from {@link MethodVisitor#visitEnd()}. + */ + public void generateDelegateCode() { + /* + * The goal is to generate a call to a static delegate method. + * If this method is non-static, the first parameter will be 'this'. + * All the parameters must be passed and then the eventual return type returned. + * + * Example, let's say we have a method such as + * public void myMethod(int a, Object b, ArrayList<String> c) { ... } + * + * We'll want to create a body that calls a delegate method like this: + * TheClass_Delegate.myMethod(this, a, b, c); + * + * If the method is non-static and the class name is an inner class (e.g. has $ in its + * last segment), we want to push the 'this' of the outer class first: + * OuterClass_InnerClass_Delegate.myMethod( + * OuterClass.this, + * OuterClass$InnerClass.this, + * a, b, c); + * + * Only one level of inner class is supported right now, for simplicity and because + * we don't need more. + * + * The generated class name is the current class name with "_Delegate" appended to it. + * One thing to realize is that we don't care about generics -- since generic types + * are erased at build time, they have no influence on the method name being called. + */ + + // Add our annotation + AnnotationVisitor aw = mDelWriter.visitAnnotation( + Type.getObjectType(Type.getInternalName(LayoutlibDelegate.class)).toString(), + true); // visible at runtime + if (aw != null) { + aw.visitEnd(); + } + + mDelWriter.visitCode(); + + if (mDelegateLineNumber != null) { + Object[] p = mDelegateLineNumber; + mDelWriter.visitLineNumber((Integer) p[0], (Label) p[1]); + } + + ArrayList<Type> paramTypes = new ArrayList<Type>(); + String delegateClassName = mClassName + DELEGATE_SUFFIX; + boolean pushedArg0 = false; + int maxStack = 0; + + // Check if the last segment of the class name has inner an class. + // Right now we only support one level of inner classes. + Type outerType = null; + int slash = mClassName.lastIndexOf('/'); + int dol = mClassName.lastIndexOf('$'); + if (dol != -1 && dol > slash && dol == mClassName.indexOf('$')) { + String outerClass = mClassName.substring(0, dol); + outerType = Type.getObjectType(outerClass); + + // Change a delegate class name to "com/foo/Outer_Inner_Delegate" + delegateClassName = delegateClassName.replace('$', '_'); + } + + // For an instance method (e.g. non-static), push the 'this' preceded + // by the 'this' of any outer class, if any. + if (!mIsStatic) { + + if (outerType != null) { + // The first-level inner class has a package-protected member called 'this$0' + // that points to the outer class. + + // Push this.getField("this$0") on the call stack. + mDelWriter.visitVarInsn(Opcodes.ALOAD, 0); // var 0 = this + mDelWriter.visitFieldInsn(Opcodes.GETFIELD, + mClassName, // class where the field is defined + "this$0", // field name + outerType.getDescriptor()); // type of the field + maxStack++; + paramTypes.add(outerType); + + } + + // Push "this" for the instance method, which is always ALOAD 0 + mDelWriter.visitVarInsn(Opcodes.ALOAD, 0); + maxStack++; + pushedArg0 = true; + paramTypes.add(Type.getObjectType(mClassName)); + } + + // Push all other arguments. Start at arg 1 if we already pushed 'this' above. + Type[] argTypes = Type.getArgumentTypes(mDesc); + int maxLocals = pushedArg0 ? 1 : 0; + for (Type t : argTypes) { + int size = t.getSize(); + mDelWriter.visitVarInsn(t.getOpcode(Opcodes.ILOAD), maxLocals); + maxLocals += size; + maxStack += size; + paramTypes.add(t); + } + + // Construct the descriptor of the delegate based on the parameters + // we pushed on the call stack. The return type remains unchanged. + String desc = Type.getMethodDescriptor( + Type.getReturnType(mDesc), + paramTypes.toArray(new Type[paramTypes.size()])); + + // Invoke the static delegate + mDelWriter.visitMethodInsn(Opcodes.INVOKESTATIC, + delegateClassName, + mMethodName, + desc); + + Type returnType = Type.getReturnType(mDesc); + mDelWriter.visitInsn(returnType.getOpcode(Opcodes.IRETURN)); + + mDelWriter.visitMaxs(maxStack, maxLocals); + mDelWriter.visitEnd(); + + // For debugging now. Maybe we should collect these and store them in + // a text file for helping create the delegates. We could also compare + // the text file to a golden and break the build on unsupported changes + // or regressions. Even better we could fancy-print something that looks + // like the expected Java method declaration. + mLog.debug("Delegate: %1$s # %2$s %3$s", delegateClassName, mMethodName, desc); + } + + /* Pass down to visitor writer. In this implementation, either do nothing. */ + @Override + public void visitCode() { + if (mOrgWriter != null) { + mOrgWriter.visitCode(); + } + } + + /* + * visitMaxs is called just before visitEnd if there was any code to rewrite. + */ + @Override + public void visitMaxs(int maxStack, int maxLocals) { + if (mOrgWriter != null) { + mOrgWriter.visitMaxs(maxStack, maxLocals); + } + } + + /** End of visiting. Generate the delegating code. */ + @Override + public void visitEnd() { + if (mOrgWriter != null) { + mOrgWriter.visitEnd(); + } + generateDelegateCode(); + } + + /* Writes all annotation from the original method. */ + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + if (mOrgWriter != null) { + return mOrgWriter.visitAnnotation(desc, visible); + } else { + return null; + } + } + + /* Writes all annotation default values from the original method. */ + @Override + public AnnotationVisitor visitAnnotationDefault() { + if (mOrgWriter != null) { + return mOrgWriter.visitAnnotationDefault(); + } else { + return null; + } + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, + boolean visible) { + if (mOrgWriter != null) { + return mOrgWriter.visitParameterAnnotation(parameter, desc, visible); + } else { + return null; + } + } + + /* Writes all attributes from the original method. */ + @Override + public void visitAttribute(Attribute attr) { + if (mOrgWriter != null) { + mOrgWriter.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. + */ + @Override + public void visitLineNumber(int line, Label start) { + // Capture the first line values for the new delegate method + if (mDelegateLineNumber == null) { + mDelegateLineNumber = new Object[] { line, start }; + } + if (mOrgWriter != null) { + mOrgWriter.visitLineNumber(line, start); + } + } + + @Override + public void visitInsn(int opcode) { + if (mOrgWriter != null) { + mOrgWriter.visitInsn(opcode); + } + } + + @Override + public void visitLabel(Label label) { + if (mOrgWriter != null) { + mOrgWriter.visitLabel(label); + } + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (mOrgWriter != null) { + mOrgWriter.visitTryCatchBlock(start, end, handler, type); + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + if (mOrgWriter != null) { + mOrgWriter.visitMethodInsn(opcode, owner, name, desc); + } + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + if (mOrgWriter != null) { + mOrgWriter.visitFieldInsn(opcode, owner, name, desc); + } + } + + @Override + public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { + if (mOrgWriter != null) { + mOrgWriter.visitFrame(type, nLocal, local, nStack, stack); + } + } + + @Override + public void visitIincInsn(int var, int increment) { + if (mOrgWriter != null) { + mOrgWriter.visitIincInsn(var, increment); + } + } + + @Override + public void visitIntInsn(int opcode, int operand) { + if (mOrgWriter != null) { + mOrgWriter.visitIntInsn(opcode, operand); + } + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + if (mOrgWriter != null) { + mOrgWriter.visitJumpInsn(opcode, label); + } + } + + @Override + public void visitLdcInsn(Object cst) { + if (mOrgWriter != null) { + mOrgWriter.visitLdcInsn(cst); + } + } + + @Override + public void visitLocalVariable(String name, String desc, String signature, + Label start, Label end, int index) { + if (mOrgWriter != null) { + mOrgWriter.visitLocalVariable(name, desc, signature, start, end, index); + } + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + if (mOrgWriter != null) { + mOrgWriter.visitLookupSwitchInsn(dflt, keys, labels); + } + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + if (mOrgWriter != null) { + mOrgWriter.visitMultiANewArrayInsn(desc, dims); + } + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + if (mOrgWriter != null) { + mOrgWriter.visitTableSwitchInsn(min, max, dflt, labels); + } + } + + @Override + public void visitTypeInsn(int opcode, String type) { + if (mOrgWriter != null) { + mOrgWriter.visitTypeInsn(opcode, type); + } + } + + @Override + public void visitVarInsn(int opcode, int var) { + if (mOrgWriter != null) { + mOrgWriter.visitVarInsn(opcode, var); + } + } + +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java new file mode 100644 index 000000000000..c988c7099cb8 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java @@ -0,0 +1,787 @@ +/* + * Copyright (C) 2012 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 com.android.tools.layoutlib.annotations.VisibleForTesting; +import com.android.tools.layoutlib.annotations.VisibleForTesting.Visibility; + +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.Opcodes; +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.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Analyzes the input JAR using the ASM java bytecode manipulation library + * to list the classes and their dependencies. A "dependency" is a class + * used by another class. + */ +public class DependencyFinder { + + // Note: a bunch of stuff has package-level access for unit tests. Consider it private. + + /** Output logger. */ + private final Log mLog; + + /** + * Creates a new analyzer. + * + * @param log The log output. + */ + public DependencyFinder(Log log) { + mLog = log; + } + + /** + * Starts the analysis using parameters from the constructor. + * + * @param osJarPath The input source JARs to parse. + * @return A pair: [0]: map { class FQCN => set of FQCN class dependencies }. + * [1]: map { missing class FQCN => set of FQCN class that uses it. } + */ + public List<Map<String, Set<String>>> findDeps(List<String> osJarPath) throws IOException { + + Map<String, ClassReader> zipClasses = parseZip(osJarPath); + mLog.info("Found %d classes in input JAR%s.", + zipClasses.size(), + osJarPath.size() > 1 ? "s" : ""); + + Map<String, Set<String>> deps = findClassesDeps(zipClasses); + + Map<String, Set<String>> missing = findMissingClasses(deps, zipClasses.keySet()); + + List<Map<String, Set<String>>> result = new ArrayList<Map<String,Set<String>>>(2); + result.add(deps); + result.add(missing); + return result; + } + + /** + * Prints dependencies to the current logger, found stuff and missing stuff. + */ + public void printAllDeps(List<Map<String, Set<String>>> result) { + assert result.size() == 2; + Map<String, Set<String>> deps = result.get(0); + Map<String, Set<String>> missing = result.get(1); + + // Print all dependences found in the format: + // +Found: <FQCN from zip> + // uses: FQCN + + mLog.info("++++++ %d Entries found in source JARs", deps.size()); + mLog.info(""); + + for (Entry<String, Set<String>> entry : deps.entrySet()) { + mLog.info( "+Found : %s", entry.getKey()); + for (String dep : entry.getValue()) { + mLog.info(" uses: %s", dep); + } + + mLog.info(""); + } + + + // Now print all missing dependences in the format: + // -Missing <FQCN>: + // used by: <FQCN> + + mLog.info(""); + mLog.info("------ %d Entries missing from source JARs", missing.size()); + mLog.info(""); + + for (Entry<String, Set<String>> entry : missing.entrySet()) { + mLog.info( "-Missing : %s", entry.getKey()); + for (String dep : entry.getValue()) { + mLog.info(" used by: %s", dep); + } + + mLog.info(""); + } + } + + /** + * Prints only a summary of the missing dependencies to the current logger. + */ + public void printMissingDeps(List<Map<String, Set<String>>> result) { + assert result.size() == 2; + @SuppressWarnings("unused") Map<String, Set<String>> deps = result.get(0); + Map<String, Set<String>> missing = result.get(1); + + for (String fqcn : missing.keySet()) { + mLog.info("%s", fqcn); + } + } + + // ---------------- + + /** + * 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('/', '.'); + } + } + + /** + * Finds all dependencies for all classes in keepClasses which are also + * listed in zipClasses. Returns a map of all the dependencies found. + */ + Map<String, Set<String>> findClassesDeps(Map<String, ClassReader> zipClasses) { + + // The dependencies that we'll collect. + // It's a map Class name => uses class names. + Map<String, Set<String>> dependencyMap = new TreeMap<String, Set<String>>(); + + DependencyVisitor visitor = getVisitor(); + + int count = 0; + try { + for (Entry<String, ClassReader> entry : zipClasses.entrySet()) { + String name = entry.getKey(); + + TreeSet<String> set = new TreeSet<String>(); + dependencyMap.put(name, set); + visitor.setDependencySet(set); + + ClassReader cr = entry.getValue(); + cr.accept(visitor, 0 /* flags */); + + visitor.setDependencySet(null); + + mLog.debugNoln("Visited %d classes\r", ++count); + } + } finally { + mLog.debugNoln("\n"); + } + + return dependencyMap; + } + + /** + * Computes which classes FQCN were found as dependencies that are NOT listed + * in the original JAR classes. + * + * @param deps The map { FQCN => dependencies[] } returned by {@link #findClassesDeps(Map)}. + * @param zipClasses The set of all classes FQCN found in the JAR files. + * @return A map { FQCN not found in the zipClasses => classes using it } + */ + private Map<String, Set<String>> findMissingClasses( + Map<String, Set<String>> deps, + Set<String> zipClasses) { + Map<String, Set<String>> missing = new TreeMap<String, Set<String>>(); + + for (Entry<String, Set<String>> entry : deps.entrySet()) { + String name = entry.getKey(); + + for (String dep : entry.getValue()) { + if (!zipClasses.contains(dep)) { + // This dependency doesn't exist in the zip classes. + Set<String> set = missing.get(dep); + if (set == null) { + set = new TreeSet<String>(); + missing.put(dep, set); + } + set.add(name); + } + } + + } + + return missing; + } + + + // ---------------------------------- + + /** + * Instantiates a new DependencyVisitor. Useful for unit tests. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + DependencyVisitor getVisitor() { + return new DependencyVisitor(); + } + + /** + * Visitor to collect all the type dependencies from a class. + */ + public class DependencyVisitor extends ClassVisitor { + + private Set<String> mCurrentDepSet; + + /** + * Creates a new visitor that will find all the dependencies for the visited class. + */ + public DependencyVisitor() { + super(Opcodes.ASM4); + } + + /** + * Sets the {@link Set} where to record direct dependencies for this class. + * This will change before each {@link ClassReader#accept(ClassVisitor, int)} call. + */ + public void setDependencySet(Set<String> set) { + mCurrentDepSet = set; + } + + /** + * Considers the given class name as a dependency. + */ + public void considerName(String className) { + if (className == null) { + return; + } + + className = internalToBinaryClassName(className); + + 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 + } + + // Add it to the dependency set for the currently visited class, as needed. + assert mCurrentDepSet != null; + if (mCurrentDepSet != null) { + mCurrentDepSet.add(className); + } + } + + /** + * 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(new MySignatureVisitor()); + } + } + + /** + * 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 boolean considerDesc(String desc) { + if (desc != null) { + try { + if (desc.length() > 0 && desc.charAt(0) == '(') { + // This is a method descriptor with arguments and a return type. + Type t = Type.getReturnType(desc); + considerType(t); + + for (Type arg : Type.getArgumentTypes(desc)) { + considerType(arg); + } + + } else { + Type t = Type.getType(desc); + considerType(t); + } + return true; + } catch (ArrayIndexOutOfBoundsException e) { + // ignore, not a valid type. + } + } + return false; + } + + + // --------------------------------------------------- + // --- ClassVisitor, FieldVisitor + // --------------------------------------------------- + + // Visits a class header + @Override + 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); + } + + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public void visitAttribute(Attribute attr) { + // pass + } + + // Visits the end of a class + @Override + public void visitEnd() { + // pass + } + + private class MyFieldVisitor extends FieldVisitor { + + public MyFieldVisitor() { + super(Opcodes.ASM4); + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public void visitAttribute(Attribute attr) { + // pass + } + + // Visits the end of a class + @Override + public void visitEnd() { + // pass + } + } + + @Override + 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 new MyFieldVisitor(); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // name is the internal name of an inner class (see getInternalName). + // Note: outerName/innerName seems to be null when we're reading the + // _Original_ClassName classes generated by layoutlib_create. + if (outerName != null) { + considerName(name); + } + } + + @Override + 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 new MyMethodVisitor(); + } + + @Override + public void visitOuterClass(String owner, String name, String desc) { + // pass + } + + @Override + public void visitSource(String source, String debug) { + // pass + } + + + // --------------------------------------------------- + // --- MethodVisitor + // --------------------------------------------------- + + private class MyMethodVisitor extends MethodVisitor { + + public MyMethodVisitor() { + super(Opcodes.ASM4); + } + + + @Override + public AnnotationVisitor visitAnnotationDefault() { + return new MyAnnotationVisitor(); + } + + @Override + public void visitCode() { + // pass + } + + // field instruction + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + // name is the field's name. + // desc is the field's descriptor (see Type). + considerDesc(desc); + } + + @Override + public void visitFrame(int type, int local, Object[] local2, int stack, Object[] stack2) { + // pass + } + + @Override + public void visitIincInsn(int var, int increment) { + // pass -- an IINC instruction + } + + @Override + public void visitInsn(int opcode) { + // pass -- a zero operand instruction + } + + @Override + public void visitIntInsn(int opcode, int operand) { + // pass -- a single int operand instruction + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + // pass -- a jump instruction + } + + @Override + public void visitLabel(Label label) { + // pass -- a label target + } + + // instruction to load a constant from the stack + @Override + public void visitLdcInsn(Object cst) { + if (cst instanceof Type) { + considerType((Type) cst); + } + } + + @Override + public void visitLineNumber(int line, Label start) { + // pass + } + + @Override + 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); + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + // pass -- a lookup switch instruction + } + + @Override + public void visitMaxs(int maxStack, int maxLocals) { + // pass + } + + // instruction that invokes a method + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + + // owner is the internal name of the method's owner class + if (!considerDesc(owner) && owner.indexOf('/') != -1) { + considerName(owner); + } + // desc is the method's descriptor (see Type). + considerDesc(desc); + } + + // instruction multianewarray, whatever that is + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + + // desc an array type descriptor. + considerDesc(desc); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, + boolean visible) { + // desc is the class descriptor of the annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + // pass -- table switch instruction + + } + + @Override + 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 + @Override + 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); + } + + @Override + public void visitVarInsn(int opcode, int var) { + // pass -- local variable instruction + } + } + + private class MySignatureVisitor extends SignatureVisitor { + + public MySignatureVisitor() { + super(Opcodes.ASM4); + } + + // --------------------------------------------------- + // --- SignatureVisitor + // --------------------------------------------------- + + private String mCurrentSignatureClass = null; + + // Starts the visit of a signature corresponding to a class or interface type + @Override + public void visitClassType(String name) { + mCurrentSignatureClass = name; + considerName(name); + } + + // Visits an inner class + @Override + public void visitInnerClassType(String name) { + if (mCurrentSignatureClass != null) { + mCurrentSignatureClass += "$" + name; + considerName(mCurrentSignatureClass); + } + } + + @Override + public SignatureVisitor visitArrayType() { + return new MySignatureVisitor(); + } + + @Override + public void visitBaseType(char descriptor) { + // pass -- a primitive type, ignored + } + + @Override + public SignatureVisitor visitClassBound() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitExceptionType() { + return new MySignatureVisitor(); + } + + @Override + public void visitFormalTypeParameter(String name) { + // pass + } + + @Override + public SignatureVisitor visitInterface() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitInterfaceBound() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitParameterType() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitReturnType() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitSuperclass() { + return new MySignatureVisitor(); + } + + @Override + public SignatureVisitor visitTypeArgument(char wildcard) { + return new MySignatureVisitor(); + } + + @Override + public void visitTypeVariable(String name) { + // pass + } + + @Override + public void visitTypeArgument() { + // pass + } + } + + + // --------------------------------------------------- + // --- AnnotationVisitor + // --------------------------------------------------- + + private class MyAnnotationVisitor extends AnnotationVisitor { + + public MyAnnotationVisitor() { + super(Opcodes.ASM4); + } + + // Visits a primitive value of an annotation + @Override + 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); + } + } + + @Override + public AnnotationVisitor visitAnnotation(String name, String desc) { + // desc is the class descriptor of the nested annotation class. + considerDesc(desc); + return new MyAnnotationVisitor(); + } + + @Override + public AnnotationVisitor visitArray(String name) { + return new MyAnnotationVisitor(); + } + + @Override + 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/ICreateInfo.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java new file mode 100644 index 000000000000..40c1706d9ed0 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ICreateInfo.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +/** + * Interface describing the work to be done by {@link AsmGenerator}. + */ +public interface ICreateInfo { + + /** + * Returns the list of class from layoutlib_create to inject in layoutlib. + * The list can be empty but must not be null. + */ + public abstract Class<?>[] getInjectedClasses(); + + /** + * Returns the list of methods to rewrite as delegates. + * The list can be empty but must not be null. + */ + public abstract String[] getDelegateMethods(); + + /** + * Returns the list of classes on which to delegate all native methods. + * The list can be empty but must not be null. + */ + public abstract String[] getDelegateClassNatives(); + + /** + * Returns The list of methods to stub out. Each entry must be in the form + * "package.package.OuterClass$InnerClass#MethodName". + * The list can be empty but must not be null. + */ + public abstract String[] getOverriddenMethods(); + + /** + * Returns the list of classes to rename, must be an even list: the binary FQCN + * of class to replace followed by the new FQCN. + * The list can be empty but must not be null. + */ + public abstract String[] getRenamedClasses(); + + /** + * Returns the 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. + * The list can be empty but must not be null. + */ + public abstract String[] getDeleteReturns(); + +} 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 000000000000..c3ba591513b6 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Log.java @@ -0,0 +1,72 @@ +/* + * 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); + } + } + + /** Similar to debug() but doesn't do a \n automatically. */ + public void debugNoln(String format, Object... args) { + if (mVerbose) { + String s = String.format(format, args); + System.out.print(s); + } + } + + 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 000000000000..dc4b4a7c86df --- /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 000000000000..9cd74db3445b --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java @@ -0,0 +1,212 @@ +/* + * 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.List; +import java.util.Map; +import java.util.Set; + + +/** + * Entry point for the layoutlib_create tool. + * <p/> + * The tool does not currently rely on any external configuration file. + * Instead the configuration is mostly done via the {@link CreateInfo} class. + * <p/> + * For a complete description of the tool and its implementation, please refer to + * the "README.txt" file at the root of this project. + * <p/> + * For a quick test, invoke this as follows: + * <pre> + * $ make layoutlib + * </pre> + * which does: + * <pre> + * $ make layoutlib_create <bunch of framework jars> + * $ java -jar out/host/linux-x86/framework/layoutlib_create.jar \ + * out/host/common/obj/JAVA_LIBRARIES/temp_layoutlib_intermediates/javalib.jar \ + * out/target/common/obj/JAVA_LIBRARIES/core_intermediates/classes.jar \ + * out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar + * </pre> + */ +public class Main { + + public static class Options { + public boolean generatePublicAccess = true; + public boolean listAllDeps = false; + public boolean listOnlyMissingDeps = false; + } + + public static final Options sOptions = new Options(); + + 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] [-p] output.jar input.jar ..."); + log.error("Usage: layoutlib_create [-v] [--list-deps|--missing-deps] input.jar ..."); + System.exit(1); + } + + if (sOptions.listAllDeps || sOptions.listOnlyMissingDeps) { + System.exit(listDeps(osJarPath, log)); + + } else { + System.exit(createLayoutLib(osDestJar[0], osJarPath, log)); + } + + + System.exit(1); + } + + private static int createLayoutLib(String osDestJar, ArrayList<String> osJarPath, Log log) { + log.info("Output: %1$s", osDestJar); + for (String path : osJarPath) { + log.info("Input : %1$s", path); + } + + try { + AsmGenerator agen = new AsmGenerator(log, osDestJar, new CreateInfo()); + + AsmAnalyzer aa = new AsmAnalyzer(log, osJarPath, agen, + new String[] { // derived from + "android.view.View", + "android.app.Fragment" + }, + 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 + "android.database.ContentObserver", // for Digital clock + }); + 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); + } + return 1; + } + + return 0; + } catch (IOException e) { + log.exception(e, "Failed to load jar"); + } catch (LogAbortException e) { + e.error(log); + } + + return 1; + } + + private static int listDeps(ArrayList<String> osJarPath, Log log) { + DependencyFinder df = new DependencyFinder(log); + try { + List<Map<String, Set<String>>> result = df.findDeps(osJarPath); + if (sOptions.listAllDeps) { + df.printAllDeps(result); + } else if (sOptions.listOnlyMissingDeps) { + df.printMissingDeps(result); + } + } catch (IOException e) { + log.exception(e, "Failed to load jar"); + } + + return 0; + } + + /** + * 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) { + boolean needs_dest = true; + for (int i = 0; i < args.length; i++) { + String s = args[i]; + if (s.equals("-v")) { + log.setVerbose(true); + } else if (s.equals("-p")) { + sOptions.generatePublicAccess = false; + } else if (s.equals("--list-deps")) { + sOptions.listAllDeps = true; + needs_dest = false; + } else if (s.equals("--missing-deps")) { + sOptions.listOnlyMissingDeps = true; + needs_dest = false; + } else if (!s.startsWith("-")) { + if (needs_dest && 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 (needs_dest && osDestJar[0] == null) { + log.error("Missing parameter: path to output jar"); + return false; + } + + sOptions.generatePublicAccess = 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 000000000000..7d1e4cf49635 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodAdapter.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.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. + */ + @Override + 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. + */ + @Override + 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. + */ + @Override + 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. + */ + @Override + 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. + */ + @Override + 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. + */ + @Override + 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 000000000000..6fc2b240b84f --- /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 000000000000..a6aff992ee76 --- /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 000000000000..383cbb8e3373 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/RenameClassAdapter.java @@ -0,0 +1,463 @@ +/* + * 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.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.Label; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +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 ClassVisitor { + + + 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(Opcodes.ASM4, 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 MethodVisitor { + + /** + * 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(Opcodes.ASM4, 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 extends SignatureVisitor { + + private final SignatureVisitor mSv; + + public RenameSignatureAdapter(SignatureVisitor sv) { + super(Opcodes.ASM4); + mSv = sv; + } + + @Override + public void visitClassType(String name) { + name = renameInternalType(name); + mSv.visitClassType(name); + } + + @Override + public void visitInnerClassType(String name) { + name = renameInternalType(name); + mSv.visitInnerClassType(name); + } + + @Override + public SignatureVisitor visitArrayType() { + SignatureVisitor sv = mSv.visitArrayType(); + return new RenameSignatureAdapter(sv); + } + + @Override + public void visitBaseType(char descriptor) { + mSv.visitBaseType(descriptor); + } + + @Override + public SignatureVisitor visitClassBound() { + SignatureVisitor sv = mSv.visitClassBound(); + return new RenameSignatureAdapter(sv); + } + + @Override + public void visitEnd() { + mSv.visitEnd(); + } + + @Override + public SignatureVisitor visitExceptionType() { + SignatureVisitor sv = mSv.visitExceptionType(); + return new RenameSignatureAdapter(sv); + } + + @Override + public void visitFormalTypeParameter(String name) { + mSv.visitFormalTypeParameter(name); + } + + @Override + public SignatureVisitor visitInterface() { + SignatureVisitor sv = mSv.visitInterface(); + return new RenameSignatureAdapter(sv); + } + + @Override + public SignatureVisitor visitInterfaceBound() { + SignatureVisitor sv = mSv.visitInterfaceBound(); + return new RenameSignatureAdapter(sv); + } + + @Override + public SignatureVisitor visitParameterType() { + SignatureVisitor sv = mSv.visitParameterType(); + return new RenameSignatureAdapter(sv); + } + + @Override + public SignatureVisitor visitReturnType() { + SignatureVisitor sv = mSv.visitReturnType(); + return new RenameSignatureAdapter(sv); + } + + @Override + public SignatureVisitor visitSuperclass() { + SignatureVisitor sv = mSv.visitSuperclass(); + return new RenameSignatureAdapter(sv); + } + + @Override + public void visitTypeArgument() { + mSv.visitTypeArgument(); + } + + @Override + public SignatureVisitor visitTypeArgument(char wildcard) { + SignatureVisitor sv = mSv.visitTypeArgument(wildcard); + return new RenameSignatureAdapter(sv); + } + + @Override + 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 000000000000..51e7535720fe --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java @@ -0,0 +1,376 @@ +/* + * 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 extends 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) { + super(Opcodes.ASM4); + 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. */ + @Override + 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. + */ + @Override + 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. + */ + @Override + public void visitEnd() { + if (!mIsInitMethod && !mMessageGenerated) { + generateInvoke(); + mMessageGenerated = true; + mParentVisitor.visitMaxs(1, 1); + } + mParentVisitor.visitEnd(); + } + + /* Writes all annotation from the original method. */ + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + return mParentVisitor.visitAnnotation(desc, visible); + } + + /* Writes all annotation default values from the original method. */ + @Override + public AnnotationVisitor visitAnnotationDefault() { + return mParentVisitor.visitAnnotationDefault(); + } + + @Override + public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, + boolean visible) { + return mParentVisitor.visitParameterAnnotation(parameter, desc, visible); + } + + /* Writes all attributes from the original method. */ + @Override + 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. + */ + @Override + 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. + */ + @Override + 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; + //$FALL-THROUGH$ + default: + mParentVisitor.visitInsn(opcode); + } + } + } + + @Override + public void visitLabel(Label label) { + if (mIsInitMethod) { + mParentVisitor.visitLabel(label); + } + } + + @Override + public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + if (mIsInitMethod) { + mParentVisitor.visitTryCatchBlock(start, end, handler, type); + } + } + + @Override + public void visitMethodInsn(int opcode, String owner, String name, String desc) { + if (mIsInitMethod) { + mParentVisitor.visitMethodInsn(opcode, owner, name, desc); + } + } + + @Override + public void visitFieldInsn(int opcode, String owner, String name, String desc) { + if (mIsInitMethod) { + mParentVisitor.visitFieldInsn(opcode, owner, name, desc); + } + } + + @Override + public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) { + if (mIsInitMethod) { + mParentVisitor.visitFrame(type, nLocal, local, nStack, stack); + } + } + + @Override + public void visitIincInsn(int var, int increment) { + if (mIsInitMethod) { + mParentVisitor.visitIincInsn(var, increment); + } + } + + @Override + public void visitIntInsn(int opcode, int operand) { + if (mIsInitMethod) { + mParentVisitor.visitIntInsn(opcode, operand); + } + } + + @Override + public void visitJumpInsn(int opcode, Label label) { + if (mIsInitMethod) { + mParentVisitor.visitJumpInsn(opcode, label); + } + } + + @Override + public void visitLdcInsn(Object cst) { + if (mIsInitMethod) { + mParentVisitor.visitLdcInsn(cst); + } + } + + @Override + 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); + } + } + + @Override + public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) { + if (mIsInitMethod) { + mParentVisitor.visitLookupSwitchInsn(dflt, keys, labels); + } + } + + @Override + public void visitMultiANewArrayInsn(String desc, int dims) { + if (mIsInitMethod) { + mParentVisitor.visitMultiANewArrayInsn(desc, dims); + } + } + + @Override + public void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels) { + if (mIsInitMethod) { + mParentVisitor.visitTableSwitchInsn(min, max, dflt, labels); + } + } + + @Override + public void visitTypeInsn(int opcode, String type) { + if (mIsInitMethod) { + mParentVisitor.visitTypeInsn(opcode, type); + } + } + + @Override + 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 000000000000..d45a18312422 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java @@ -0,0 +1,183 @@ +/* + * 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.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 ClassVisitor { + + /** 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 list of method signatures to always stub out + * @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(Opcodes.ASM4, 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 + if (Main.sOptions.generatePublicAccess) { + 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 + if (Main.sOptions.generatePublicAccess) { + 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 + if (Main.sOptions.generatePublicAccess) { + 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 + if (Main.sOptions.generatePublicAccess) { + 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 000000000000..d6dba6a9fb34 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.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 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 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 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 000000000000..7b76a5b2f914 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java @@ -0,0 +1,119 @@ +/* + * 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 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 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 { + + ICreateInfo ci = new ICreateInfo() { + @Override + public Class<?>[] getInjectedClasses() { + // classes to inject in the final JAR + return new Class<?>[0]; + } + + @Override + public String[] getDelegateMethods() { + return new String[0]; + } + + @Override + public String[] getDelegateClassNatives() { + return new String[0]; + } + + @Override + public String[] getOverriddenMethods() { + // methods to force override + return new String[0]; + } + + @Override + public String[] getRenamedClasses() { + // classes to rename (so that we can replace them) + return new String[] { + "mock_android.view.View", "mock_android.view._Original_View", + "not.an.actual.ClassName", "anoter.fake.NewClassName", + }; + } + + @Override + public String[] getDeleteReturns() { + // methods deleted from their return type. + return new String[0]; + } + }; + + AsmGenerator agen = new AsmGenerator(mLog, mOsDestJar, ci); + + 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/ClassHasNativeVisitorTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/ClassHasNativeVisitorTest.java new file mode 100644 index 000000000000..0135c40e71ab --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/ClassHasNativeVisitorTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.util.ArrayList; + + +/** + * Tests {@link ClassHasNativeVisitor}. + */ +public class ClassHasNativeVisitorTest { + + @Test + public void testHasNative() throws IOException { + MockClassHasNativeVisitor cv = new MockClassHasNativeVisitor(); + String className = + this.getClass().getCanonicalName() + "$" + ClassWithNative.class.getSimpleName(); + ClassReader cr = new ClassReader(className); + + cr.accept(cv, 0 /* flags */); + assertArrayEquals(new String[] { "native_method" }, cv.getMethodsFound()); + assertTrue(cv.hasNativeMethods()); + } + + @Test + public void testHasNoNative() throws IOException { + MockClassHasNativeVisitor cv = new MockClassHasNativeVisitor(); + String className = + this.getClass().getCanonicalName() + "$" + ClassWithoutNative.class.getSimpleName(); + ClassReader cr = new ClassReader(className); + + cr.accept(cv, 0 /* flags */); + assertArrayEquals(new String[0], cv.getMethodsFound()); + assertFalse(cv.hasNativeMethods()); + } + + //------- + + /** + * Overrides {@link ClassHasNativeVisitor} to collec the name of the native methods found. + */ + private static class MockClassHasNativeVisitor extends ClassHasNativeVisitor { + private ArrayList<String> mMethodsFound = new ArrayList<String>(); + + public String[] getMethodsFound() { + return mMethodsFound.toArray(new String[mMethodsFound.size()]); + } + + @Override + protected void setHasNativeMethods(boolean hasNativeMethods, String methodName) { + if (hasNativeMethods) { + mMethodsFound.add(methodName); + } + super.setHasNativeMethods(hasNativeMethods, methodName); + } + } + + /** + * Dummy test class with a native method. + */ + public static class ClassWithNative { + public ClassWithNative() { + } + + public void callTheNativeMethod() { + native_method(); + } + + private native void native_method(); + } + + /** + * Dummy test class with no native method. + */ + public static class ClassWithoutNative { + public ClassWithoutNative() { + } + + public void someMethod() { + } + } +} diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java new file mode 100644 index 000000000000..6e120cefadf5 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.android.tools.layoutlib.create.dataclass.ClassWithNative; +import com.android.tools.layoutlib.create.dataclass.OuterClass; +import com.android.tools.layoutlib.create.dataclass.OuterClass.InnerClass; + +import org.junit.Before; +import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class DelegateClassAdapterTest { + + private MockLog mLog; + + private static final String NATIVE_CLASS_NAME = ClassWithNative.class.getCanonicalName(); + private static final String OUTER_CLASS_NAME = OuterClass.class.getCanonicalName(); + private static final String INNER_CLASS_NAME = OuterClass.class.getCanonicalName() + "$" + + InnerClass.class.getSimpleName(); + + @Before + public void setUp() throws Exception { + mLog = new MockLog(); + mLog.setVerbose(true); // capture debug error too + } + + /** + * Tests that a class not being modified still works. + */ + @SuppressWarnings("unchecked") + @Test + public void testNoOp() throws Throwable { + // create an instance of the class that will be modified + // (load the class in a distinct class loader so that we can trash its definition later) + ClassLoader cl1 = new ClassLoader(this.getClass().getClassLoader()) { }; + Class<ClassWithNative> clazz1 = (Class<ClassWithNative>) cl1.loadClass(NATIVE_CLASS_NAME); + ClassWithNative instance1 = clazz1.newInstance(); + assertEquals(42, instance1.add(20, 22)); + try { + instance1.callNativeInstance(10, 3.1415, new Object[0] ); + fail("Test should have failed to invoke callTheNativeMethod [1]"); + } catch (UnsatisfiedLinkError e) { + // This is expected to fail since the native method is not implemented. + } + + // Now process it but tell the delegate to not modify any method + ClassWriter cw = new ClassWriter(0 /*flags*/); + + HashSet<String> delegateMethods = new HashSet<String>(); + String internalClassName = NATIVE_CLASS_NAME.replace('.', '/'); + DelegateClassAdapter cv = new DelegateClassAdapter( + mLog, cw, internalClassName, delegateMethods); + + ClassReader cr = new ClassReader(NATIVE_CLASS_NAME); + cr.accept(cv, 0 /* flags */); + + // Load the generated class in a different class loader and try it again + + ClassLoader2 cl2 = null; + try { + cl2 = new ClassLoader2() { + @Override + public void testModifiedInstance() throws Exception { + Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME); + Object i2 = clazz2.newInstance(); + assertNotNull(i2); + assertEquals(42, callAdd(i2, 20, 22)); + + try { + callCallNativeInstance(i2, 10, 3.1415, new Object[0]); + fail("Test should have failed to invoke callTheNativeMethod [2]"); + } catch (InvocationTargetException e) { + // This is expected to fail since the native method has NOT been + // overridden here. + assertEquals(UnsatisfiedLinkError.class, e.getCause().getClass()); + } + + // Check that the native method does NOT have the new annotation + Method[] m = clazz2.getDeclaredMethods(); + assertEquals("native_instance", m[2].getName()); + assertTrue(Modifier.isNative(m[2].getModifiers())); + Annotation[] a = m[2].getAnnotations(); + assertEquals(0, a.length); + } + }; + cl2.add(NATIVE_CLASS_NAME, cw); + cl2.testModifiedInstance(); + } catch (Throwable t) { + throw dumpGeneratedClass(t, cl2); + } + } + + /** + * {@link DelegateMethodAdapter2} does not support overriding constructors yet, + * so this should fail with an {@link UnsupportedOperationException}. + * + * Although not tested here, the message of the exception should contain the + * constructor signature. + */ + @Test(expected=UnsupportedOperationException.class) + public void testConstructorsNotSupported() throws IOException { + ClassWriter cw = new ClassWriter(0 /*flags*/); + + String internalClassName = NATIVE_CLASS_NAME.replace('.', '/'); + + HashSet<String> delegateMethods = new HashSet<String>(); + delegateMethods.add("<init>"); + DelegateClassAdapter cv = new DelegateClassAdapter( + mLog, cw, internalClassName, delegateMethods); + + ClassReader cr = new ClassReader(NATIVE_CLASS_NAME); + cr.accept(cv, 0 /* flags */); + } + + @Test + public void testDelegateNative() throws Throwable { + ClassWriter cw = new ClassWriter(0 /*flags*/); + String internalClassName = NATIVE_CLASS_NAME.replace('.', '/'); + + HashSet<String> delegateMethods = new HashSet<String>(); + delegateMethods.add(DelegateClassAdapter.ALL_NATIVES); + DelegateClassAdapter cv = new DelegateClassAdapter( + mLog, cw, internalClassName, delegateMethods); + + ClassReader cr = new ClassReader(NATIVE_CLASS_NAME); + cr.accept(cv, 0 /* flags */); + + // Load the generated class in a different class loader and try it + ClassLoader2 cl2 = null; + try { + cl2 = new ClassLoader2() { + @Override + public void testModifiedInstance() throws Exception { + Class<?> clazz2 = loadClass(NATIVE_CLASS_NAME); + Object i2 = clazz2.newInstance(); + assertNotNull(i2); + + // Use reflection to access inner methods + assertEquals(42, callAdd(i2, 20, 22)); + + Object[] objResult = new Object[] { null }; + int result = callCallNativeInstance(i2, 10, 3.1415, objResult); + assertEquals((int)(10 + 3.1415), result); + assertSame(i2, objResult[0]); + + // Check that the native method now has the new annotation and is not native + Method[] m = clazz2.getDeclaredMethods(); + assertEquals("native_instance", m[2].getName()); + assertFalse(Modifier.isNative(m[2].getModifiers())); + Annotation[] a = m[2].getAnnotations(); + assertEquals("LayoutlibDelegate", a[0].annotationType().getSimpleName()); + } + }; + cl2.add(NATIVE_CLASS_NAME, cw); + cl2.testModifiedInstance(); + } catch (Throwable t) { + throw dumpGeneratedClass(t, cl2); + } + } + + @Test + public void testDelegateInner() throws Throwable { + // We'll delegate the "get" method of both the inner and outer class. + HashSet<String> delegateMethods = new HashSet<String>(); + delegateMethods.add("get"); + delegateMethods.add("privateMethod"); + + // Generate the delegate for the outer class. + ClassWriter cwOuter = new ClassWriter(0 /*flags*/); + String outerClassName = OUTER_CLASS_NAME.replace('.', '/'); + DelegateClassAdapter cvOuter = new DelegateClassAdapter( + mLog, cwOuter, outerClassName, delegateMethods); + ClassReader cr = new ClassReader(OUTER_CLASS_NAME); + cr.accept(cvOuter, 0 /* flags */); + + // Generate the delegate for the inner class. + ClassWriter cwInner = new ClassWriter(0 /*flags*/); + String innerClassName = INNER_CLASS_NAME.replace('.', '/'); + DelegateClassAdapter cvInner = new DelegateClassAdapter( + mLog, cwInner, innerClassName, delegateMethods); + cr = new ClassReader(INNER_CLASS_NAME); + cr.accept(cvInner, 0 /* flags */); + + // Load the generated classes in a different class loader and try them + ClassLoader2 cl2 = null; + try { + cl2 = new ClassLoader2() { + @Override + public void testModifiedInstance() throws Exception { + + // Check the outer class + Class<?> outerClazz2 = loadClass(OUTER_CLASS_NAME); + Object o2 = outerClazz2.newInstance(); + assertNotNull(o2); + + // The original Outer.get returns 1+10+20, + // but the delegate makes it return 4+10+20 + assertEquals(4+10+20, callGet(o2, 10, 20)); + assertEquals(1+10+20, callGet_Original(o2, 10, 20)); + + // The original Outer has a private method that is + // delegated. We should be able to call both the delegate + // and the original (which is now public). + assertEquals("outerPrivateMethod", + callMethod(o2, "privateMethod_Original", false /*makePublic*/)); + + // The original method is private, so by default we can't access it + boolean gotIllegalAccessException = false; + try { + callMethod(o2, "privateMethod", false /*makePublic*/); + } catch(IllegalAccessException e) { + gotIllegalAccessException = true; + } + assertTrue(gotIllegalAccessException); + // Try again, but now making it accessible + assertEquals("outerPrivate_Delegate", + callMethod(o2, "privateMethod", true /*makePublic*/)); + + // Check the inner class. Since it's not a static inner class, we need + // to use the hidden constructor that takes the outer class as first parameter. + Class<?> innerClazz2 = loadClass(INNER_CLASS_NAME); + Constructor<?> innerCons = innerClazz2.getConstructor( + new Class<?>[] { outerClazz2 }); + Object i2 = innerCons.newInstance(new Object[] { o2 }); + assertNotNull(i2); + + // The original Inner.get returns 3+10+20, + // but the delegate makes it return 6+10+20 + assertEquals(6+10+20, callGet(i2, 10, 20)); + assertEquals(3+10+20, callGet_Original(i2, 10, 20)); + } + }; + cl2.add(OUTER_CLASS_NAME, cwOuter.toByteArray()); + cl2.add(INNER_CLASS_NAME, cwInner.toByteArray()); + cl2.testModifiedInstance(); + } catch (Throwable t) { + throw dumpGeneratedClass(t, cl2); + } + } + + //------- + + /** + * A class loader than can define and instantiate our modified classes. + * <p/> + * The trick here is that this class loader will test our <em>modified</em> version + * of the classes, the one with the delegate calls. + * <p/> + * Trying to do so in the original class loader generates all sort of link issues because + * there are 2 different definitions of the same class name. This class loader will + * define and load the class when requested by name and provide helpers to access the + * instance methods via reflection. + */ + private abstract class ClassLoader2 extends ClassLoader { + + private final Map<String, byte[]> mClassDefs = new HashMap<String, byte[]>(); + + public ClassLoader2() { + super(null); + } + + public ClassLoader2 add(String className, byte[] definition) { + mClassDefs.put(className, definition); + return this; + } + + public ClassLoader2 add(String className, ClassWriter rewrittenClass) { + mClassDefs.put(className, rewrittenClass.toByteArray()); + return this; + } + + private Set<Entry<String, byte[]>> getByteCode() { + return mClassDefs.entrySet(); + } + + @SuppressWarnings("unused") + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + try { + return super.findClass(name); + } catch (ClassNotFoundException e) { + + byte[] def = mClassDefs.get(name); + if (def != null) { + // Load the modified ClassWithNative from its bytes representation. + return defineClass(name, def, 0, def.length); + } + + try { + // Load everything else from the original definition into the new class loader. + ClassReader cr = new ClassReader(name); + ClassWriter cw = new ClassWriter(0); + cr.accept(cw, 0); + byte[] bytes = cw.toByteArray(); + return defineClass(name, bytes, 0, bytes.length); + + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + } + + /** + * Accesses {@link OuterClass#get} or {@link InnerClass#get}via reflection. + */ + public int callGet(Object instance, int a, long b) throws Exception { + Method m = instance.getClass().getMethod("get", + new Class<?>[] { int.class, long.class } ); + + Object result = m.invoke(instance, new Object[] { a, b }); + return ((Integer) result).intValue(); + } + + /** + * Accesses the "_Original" methods for {@link OuterClass#get} + * or {@link InnerClass#get}via reflection. + */ + public int callGet_Original(Object instance, int a, long b) throws Exception { + Method m = instance.getClass().getMethod("get_Original", + new Class<?>[] { int.class, long.class } ); + + Object result = m.invoke(instance, new Object[] { a, b }); + return ((Integer) result).intValue(); + } + + /** + * Accesses the any declared method that takes no parameter via reflection. + */ + @SuppressWarnings("unchecked") + public <T> T callMethod(Object instance, String methodName, boolean makePublic) throws Exception { + Method m = instance.getClass().getDeclaredMethod(methodName, (Class<?>[])null); + + boolean wasAccessible = m.isAccessible(); + if (makePublic && !wasAccessible) { + m.setAccessible(true); + } + + Object result = m.invoke(instance, (Object[])null); + + if (makePublic && !wasAccessible) { + m.setAccessible(false); + } + + return (T) result; + } + + /** + * Accesses {@link ClassWithNative#add(int, int)} via reflection. + */ + public int callAdd(Object instance, int a, int b) throws Exception { + Method m = instance.getClass().getMethod("add", + new Class<?>[] { int.class, int.class }); + + Object result = m.invoke(instance, new Object[] { a, b }); + return ((Integer) result).intValue(); + } + + /** + * Accesses {@link ClassWithNative#callNativeInstance(int, double, Object[])} + * via reflection. + */ + public int callCallNativeInstance(Object instance, int a, double d, Object[] o) + throws Exception { + Method m = instance.getClass().getMethod("callNativeInstance", + new Class<?>[] { int.class, double.class, Object[].class }); + + Object result = m.invoke(instance, new Object[] { a, d, o }); + return ((Integer) result).intValue(); + } + + public abstract void testModifiedInstance() throws Exception; + } + + /** + * For debugging, it's useful to dump the content of the generated classes + * along with the exception that was generated. + * + * However to make it work you need to pull in the org.objectweb.asm.util.TraceClassVisitor + * class and associated utilities which are found in the ASM source jar. Since we don't + * want that dependency in the source code, we only put it manually for development and + * access the TraceClassVisitor via reflection if present. + * + * @param t The exception thrown by {@link ClassLoader2#testModifiedInstance()} + * @param cl2 The {@link ClassLoader2} instance with the generated bytecode. + * @return Either original {@code t} or a new wrapper {@link Throwable} + */ + private Throwable dumpGeneratedClass(Throwable t, ClassLoader2 cl2) { + try { + // For debugging, dump the bytecode of the class in case of unexpected error + // if we can find the TraceClassVisitor class. + Class<?> tcvClass = Class.forName("org.objectweb.asm.util.TraceClassVisitor"); + + StringBuilder sb = new StringBuilder(); + sb.append('\n').append(t.getClass().getCanonicalName()); + if (t.getMessage() != null) { + sb.append(": ").append(t.getMessage()); + } + + for (Entry<String, byte[]> entry : cl2.getByteCode()) { + String className = entry.getKey(); + byte[] bytes = entry.getValue(); + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + // next 2 lines do: TraceClassVisitor tcv = new TraceClassVisitor(pw); + Constructor<?> cons = tcvClass.getConstructor(new Class<?>[] { pw.getClass() }); + Object tcv = cons.newInstance(new Object[] { pw }); + ClassReader cr2 = new ClassReader(bytes); + cr2.accept((ClassVisitor) tcv, 0 /* flags */); + + sb.append("\nBytecode dump: <").append(className).append(">:\n") + .append(sw.toString()); + } + + // Re-throw exception with new message + RuntimeException ex = new RuntimeException(sb.toString(), t); + return ex; + } catch (Throwable ignore) { + // In case of problem, just throw the original exception as-is. + return t; + } + } + +} 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 000000000000..1a5f653fd333 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/LogTest.java @@ -0,0 +1,88 @@ +/* + * 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 { + + 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/MockLog.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/MockLog.java new file mode 100644 index 000000000000..de750a3af14d --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/MockLog.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create; + + +public 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'); + } +} 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 000000000000..90c6a9c9ac7c --- /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/com/android/tools/layoutlib/create/dataclass/ClassWithNative.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/ClassWithNative.java new file mode 100644 index 000000000000..c314853d7bb2 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/ClassWithNative.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create.dataclass; + +import com.android.tools.layoutlib.create.DelegateClassAdapterTest; + +/** + * Dummy test class with a native method. + * The native method is not defined and any attempt to invoke it will + * throw an {@link UnsatisfiedLinkError}. + * + * Used by {@link DelegateClassAdapterTest}. + */ +public class ClassWithNative { + public ClassWithNative() { + } + + public int add(int a, int b) { + return a + b; + } + + // Note: it's good to have a long or double for testing parameters since they take + // 2 slots in the stack/locals maps. + + public int callNativeInstance(int a, double d, Object[] o) { + return native_instance(a, d, o); + } + + private native int native_instance(int a, double d, Object[] o); +} + diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/ClassWithNative_Delegate.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/ClassWithNative_Delegate.java new file mode 100644 index 000000000000..a3d4dc60bb19 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/ClassWithNative_Delegate.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create.dataclass; + +import com.android.tools.layoutlib.create.DelegateClassAdapterTest; + +/** + * The delegate that receives the call to {@link ClassWithNative_Delegate}'s overridden methods. + * + * Used by {@link DelegateClassAdapterTest}. + */ +public class ClassWithNative_Delegate { + public static int native_instance(ClassWithNative instance, int a, double d, Object[] o) { + if (o != null && o.length > 0) { + o[0] = instance; + } + return (int)(a + d); + } +} + diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass.java new file mode 100644 index 000000000000..f083e76d995c --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create.dataclass; + +import com.android.tools.layoutlib.create.DelegateClassAdapterTest; + +/** + * Test class with an inner class. + * + * Used by {@link DelegateClassAdapterTest}. + */ +public class OuterClass { + private int mOuterValue = 1; + public OuterClass() { + } + + // Outer.get returns 1 + a + b + // Note: it's good to have a long or double for testing parameters since they take + // 2 slots in the stack/locals maps. + public int get(int a, long b) { + return mOuterValue + a + (int) b; + } + + public class InnerClass { + public InnerClass() { + } + + // Inner.get returns 2 + 1 + a + b + public int get(int a, long b) { + return 2 + mOuterValue + a + (int) b; + } + } + + @SuppressWarnings("unused") + private String privateMethod() { + return "outerPrivateMethod"; + } +} + diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_Delegate.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_Delegate.java new file mode 100644 index 000000000000..774be8e3dcbc --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_Delegate.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create.dataclass; + +import com.android.tools.layoutlib.create.DelegateClassAdapterTest; + +/** + * Used by {@link DelegateClassAdapterTest}. + */ +public class OuterClass_Delegate { + // The delegate override of Outer.get returns 4 + a + b + public static int get(OuterClass instance, int a, long b) { + return 4 + a + (int) b; + } + + public static String privateMethod(OuterClass instance) { + return "outerPrivate_Delegate"; + } +} + diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_InnerClass_Delegate.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_InnerClass_Delegate.java new file mode 100644 index 000000000000..b4722205c328 --- /dev/null +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/dataclass/OuterClass_InnerClass_Delegate.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.layoutlib.create.dataclass; + +import com.android.tools.layoutlib.create.DelegateClassAdapterTest; +import com.android.tools.layoutlib.create.dataclass.OuterClass.InnerClass; + +/** + * Used by {@link DelegateClassAdapterTest}. + */ +public class OuterClass_InnerClass_Delegate { + // The delegate override of Inner.get return 6 + a + b + public static int get(OuterClass outer, InnerClass inner, int a, long b) { + return 6 + a + (int) b; + } +} 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 000000000000..a7ea74f4adf2 --- /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 000000000000..95f7591d7046 --- /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 000000000000..e355ead16a7d --- /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 000000000000..a80a98daf1d4 --- /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 000000000000..466470fc1537 --- /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 000000000000..3870a63d9782 --- /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 000000000000..e455e7d61fd3 --- /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/obbtool/Android.mk b/tools/obbtool/Android.mk new file mode 100644 index 000000000000..9ff56d634c21 --- /dev/null +++ b/tools/obbtool/Android.mk @@ -0,0 +1,48 @@ +# +# Copyright 2010 The Android Open Source Project +# +# Opaque Binary Blob (OBB) Tool +# + +# This tool is prebuilt if we're doing an app-only build. +ifeq ($(TARGET_BUILD_APPS),) + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + Main.cpp + +LOCAL_CFLAGS := -Wall -Werror + +#LOCAL_C_INCLUDES += + +LOCAL_STATIC_LIBRARIES := \ + libandroidfw \ + libutils \ + libcutils \ + liblog + +ifeq ($(HOST_OS),linux) +LOCAL_LDLIBS += -ldl -lpthread +endif + +LOCAL_MODULE := obbtool + +include $(BUILD_HOST_EXECUTABLE) + +##################################################### +include $(CLEAR_VARS) + +LOCAL_MODULE := pbkdf2gen +LOCAL_MODULE_TAGS := optional +LOCAL_CFLAGS := -Wall -Werror +LOCAL_SRC_FILES := pbkdf2gen.cpp +LOCAL_LDLIBS += -ldl +LOCAL_C_INCLUDES := external/openssl/include $(LOCAL_C_INCLUDES) +LOCAL_STATIC_LIBRARIES := libcrypto_static + +include $(BUILD_HOST_EXECUTABLE) + +####################################################### +endif # TARGET_BUILD_APPS diff --git a/tools/obbtool/Main.cpp b/tools/obbtool/Main.cpp new file mode 100644 index 000000000000..b2152e8f8468 --- /dev/null +++ b/tools/obbtool/Main.cpp @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <androidfw/ObbFile.h> +#include <utils/String8.h> + +#include <getopt.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +using namespace android; + +static const char* gProgName = "obbtool"; +static const char* gProgVersion = "1.0"; + +static int wantUsage = 0; +static int wantVersion = 0; + +#define SALT_LEN 8 + +#define ADD_OPTS "n:v:os:" +static const struct option longopts[] = { + {"help", no_argument, &wantUsage, 1}, + {"version", no_argument, &wantVersion, 1}, + + /* Args for "add" */ + {"name", required_argument, NULL, 'n'}, + {"version", required_argument, NULL, 'v'}, + {"overlay", optional_argument, NULL, 'o'}, + {"salt", required_argument, NULL, 's'}, + + {NULL, 0, NULL, '\0'} +}; + +class PackageInfo { +public: + PackageInfo() + : packageName(NULL) + , packageVersion(-1) + , overlay(false) + , salted(false) + { + memset(&salt, 0, sizeof(salt)); + } + + char* packageName; + int packageVersion; + bool overlay; + bool salted; + unsigned char salt[SALT_LEN]; +}; + +/* + * Print usage info. + */ +void usage(void) +{ + fprintf(stderr, "Opaque Binary Blob (OBB) Tool\n\n"); + fprintf(stderr, "Usage:\n"); + fprintf(stderr, + " %s a[dd] [ OPTIONS ] FILENAME\n" + " Adds an OBB signature to the file.\n\n", gProgName); + fprintf(stderr, + " Options:\n" + " -n <package name> sets the OBB package name (required)\n" + " -v <OBB version> sets the OBB version (required)\n" + " -o sets the OBB overlay flag\n" + " -s <8 byte hex salt> sets the crypto key salt (if encrypted)\n" + "\n"); + fprintf(stderr, + " %s r[emove] FILENAME\n" + " Removes the OBB signature from the file.\n\n", gProgName); + fprintf(stderr, + " %s i[nfo] FILENAME\n" + " Prints the OBB signature information of a file.\n\n", gProgName); +} + +void doAdd(const char* filename, struct PackageInfo* info) { + ObbFile *obb = new ObbFile(); + if (obb->readFrom(filename)) { + fprintf(stderr, "ERROR: %s: OBB signature already present\n", filename); + return; + } + + obb->setPackageName(String8(info->packageName)); + obb->setVersion(info->packageVersion); + obb->setOverlay(info->overlay); + if (info->salted) { + obb->setSalt(info->salt, SALT_LEN); + } + + if (!obb->writeTo(filename)) { + fprintf(stderr, "ERROR: %s: couldn't write OBB signature: %s\n", + filename, strerror(errno)); + return; + } + + fprintf(stderr, "OBB signature successfully written\n"); +} + +void doRemove(const char* filename) { + ObbFile *obb = new ObbFile(); + if (!obb->readFrom(filename)) { + fprintf(stderr, "ERROR: %s: no OBB signature present\n", filename); + return; + } + + if (!obb->removeFrom(filename)) { + fprintf(stderr, "ERROR: %s: couldn't remove OBB signature\n", filename); + return; + } + + fprintf(stderr, "OBB signature successfully removed\n"); +} + +void doInfo(const char* filename) { + ObbFile *obb = new ObbFile(); + if (!obb->readFrom(filename)) { + fprintf(stderr, "ERROR: %s: couldn't read OBB signature\n", filename); + return; + } + + printf("OBB info for '%s':\n", filename); + printf("Package name: %s\n", obb->getPackageName().string()); + printf(" Version: %d\n", obb->getVersion()); + printf(" Flags: 0x%08x\n", obb->getFlags()); + printf(" Overlay: %s\n", obb->isOverlay() ? "true" : "false"); + printf(" Salt: "); + + size_t saltLen; + const unsigned char* salt = obb->getSalt(&saltLen); + if (salt != NULL) { + for (int i = 0; i < SALT_LEN; i++) { + printf("%02x", salt[i]); + } + printf("\n"); + } else { + printf("<empty>\n"); + } +} + +bool fromHex(char h, unsigned char *b) { + if (h >= '0' && h <= '9') { + *b = h - '0'; + return true; + } else if (h >= 'a' && h <= 'f') { + *b = h - 'a' + 10; + return true; + } else if (h >= 'A' && h <= 'F') { + *b = h - 'A' + 10; + return true; + } + return false; +} + +bool hexToByte(char h1, char h2, unsigned char* b) { + unsigned char first, second; + if (!fromHex(h1, &first)) return false; + if (!fromHex(h2, &second)) return false; + *b = (first << 4) | second; + return true; +} + +/* + * Parse args. + */ +int main(int argc, char* const argv[]) +{ + int opt; + int option_index = 0; + struct PackageInfo package_info; + + int result = 1; // pessimistically assume an error. + + if (argc < 2) { + wantUsage = 1; + goto bail; + } + + while ((opt = getopt_long(argc, argv, ADD_OPTS, longopts, &option_index)) != -1) { + switch (opt) { + case 0: + if (longopts[option_index].flag) + break; + fprintf(stderr, "'%s' requires an argument\n", longopts[option_index].name); + wantUsage = 1; + goto bail; + case 'n': + package_info.packageName = optarg; + break; + case 'v': { + char* end; + package_info.packageVersion = strtol(optarg, &end, 10); + if (*optarg == '\0' || *end != '\0') { + fprintf(stderr, "ERROR: invalid version; should be integer!\n\n"); + wantUsage = 1; + goto bail; + } + break; + } + case 'o': + package_info.overlay = true; + break; + case 's': + if (strlen(optarg) != SALT_LEN * 2) { + fprintf(stderr, "ERROR: salt must be 8 bytes in hex (e.g., ABCD65031337D00D)\n\n"); + wantUsage = 1; + goto bail; + } + + package_info.salted = true; + + unsigned char b; + for (int i = 0, j = 0; i < SALT_LEN; i++, j+=2) { + if (!hexToByte(optarg[j], optarg[j+1], &b)) { + fprintf(stderr, "ERROR: salt must be in hex (e.g., ABCD65031337D00D)\n"); + wantUsage = 1; + goto bail; + } + package_info.salt[i] = b; + } + break; + case '?': + wantUsage = 1; + goto bail; + } + } + + if (wantVersion) { + fprintf(stderr, "%s %s\n", gProgName, gProgVersion); + } + + if (wantUsage) { + goto bail; + } + +#define CHECK_OP(name) \ + if (strncmp(op, name, opsize)) { \ + fprintf(stderr, "ERROR: unknown function '%s'!\n\n", op); \ + wantUsage = 1; \ + goto bail; \ + } + + if (optind < argc) { + const char* op = argv[optind++]; + const int opsize = strlen(op); + + if (optind >= argc) { + fprintf(stderr, "ERROR: filename required!\n\n"); + wantUsage = 1; + goto bail; + } + + const char* filename = argv[optind++]; + + switch (op[0]) { + case 'a': + CHECK_OP("add"); + if (package_info.packageName == NULL) { + fprintf(stderr, "ERROR: arguments required 'packageName' and 'version'\n"); + goto bail; + } + doAdd(filename, &package_info); + break; + case 'r': + CHECK_OP("remove"); + doRemove(filename); + break; + case 'i': + CHECK_OP("info"); + doInfo(filename); + break; + default: + fprintf(stderr, "ERROR: unknown command '%s'!\n\n", op); + wantUsage = 1; + goto bail; + } + } + +bail: + if (wantUsage) { + usage(); + result = 2; + } + + return result; +} diff --git a/tools/obbtool/mkobb.sh b/tools/obbtool/mkobb.sh new file mode 100755 index 000000000000..725250d25385 --- /dev/null +++ b/tools/obbtool/mkobb.sh @@ -0,0 +1,281 @@ +#!/bin/bash +# +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# mkobb.sh - Creates OBB files on Linux machines + +# Directory where we should temporarily mount the OBB loopback to copy files +MOUNTDIR=/tmp + +# Presets. Changing these will probably break your OBB on the device +CRYPTO=twofish +FS=vfat +MKFS=mkfs.vfat +LOSETUP=losetup +BLOCK_SIZE=512 +SLOP=512 # Amount of filesystem slop in ${BLOCK_SIZE} blocks + +find_binaries() { + MKFSBIN=`which ${MKFS}` + LOSETUPBIN=`which ${LOSETUP}` + MOUNTBIN=`which mount` + UMOUNTBIN=`which umount` + DDBIN=`which dd` + RSYNCBIN=`which rsync` + PBKDF2GEN=`which pbkdf2gen` +} + +check_prereqs() { + if [ "`uname -s`x" != "Linuxx" ]; then \ + echo "ERROR: This script only works on Linux!" + exit 1 + fi + + if ! egrep -q "^cryptoloop " /proc/modules; then \ + echo "ERROR: Could not find cryptoloop in the kernel." + echo "Perhaps you need to: modprobe cryptoloop" + exit 1 + fi + + if ! egrep -q "name\s*:\s*${CRYPTO}$" /proc/crypto; then \ + echo "ERROR: Could not find crypto \`${CRYPTO}' in the kernel." + echo "Perhaps you need to: modprobe ${CRYPTO}" + exit 1 + fi + + if ! egrep -q "^\s*${FS}$" /proc/filesystems; then \ + echo "ERROR: Could not find filesystem \`${FS}' in the kernel." + echo "Perhaps you need to: modprobe ${FS}" + exit 1 + fi + + if [ "${MKFSBIN}x" = "x" ]; then \ + echo "ERROR: Could not find ${MKFS} in your path!" + exit 1 + elif [ ! -x "${MKFSBIN}" ]; then \ + echo "ERROR: ${MKFSBIN} is not executable!" + exit 1 + fi + + if [ "${LOSETUPBIN}x" = "x" ]; then \ + echo "ERROR: Could not find ${LOSETUP} in your path!" + exit 1 + elif [ ! -x "${LOSETUPBIN}" ]; then \ + echo "ERROR: ${LOSETUPBIN} is not executable!" + exit 1 + fi + + if [ "${PBKDF2GEN}x" = "x" ]; then \ + echo "ERROR: Could not find pbkdf2gen in your path!" + exit 1 + fi +} + +cleanup() { + if [ "${loopdev}x" != "x" ]; then \ + ${LOSETUPBIN} -d ${loopdev} + fi +} + +hidden_prompt() { + unset output + prompt="$1" + outvar="$2" + while read -s -n 1 -p "$prompt" c; do \ + if [ "x$c" = "x" ]; then \ + break + fi + prompt='*' + output="${output}${c}" + done + echo + eval $outvar="$output" + unset output +} + +read_key() { + hidden_prompt " Encryption key: " key + + if [ "${key}x" = "x" ]; then \ + echo "ERROR: An empty key is not allowed!" + exit 1 + fi + + hidden_prompt "Encryption key (again): " key2 + + if [ "${key}x" != "${key2}x" ]; then \ + echo "ERROR: Encryption keys do not match!" + exit 1 + fi +} + +onexit() { + if [ "x${temp_mount}" != "x" ]; then \ + ${UMOUNTBIN} ${temp_mount} + rmdir ${temp_mount} + fi + if [ "x${loop_dev}" != "x" ]; then \ + if [ ${use_crypto} -eq 1 ]; then \ + dmsetup remove -f ${loop_dev} + ${LOSETUPBIN} -d ${old_loop_dev} + else \ + ${LOSETUPBIN} -d ${loop_dev} + fi + fi + if [ "x${tempfile}" != "x" -a -f "${tempfile}" ]; then \ + rm -f ${tempfile} + fi + if [ "x${keyfile}" != "x" -a -f "${keyfile}" ]; then \ + rm -f ${keyfile} + fi + echo "Fatal error." + exit 1 +} + +usage() { + echo "mkobb.sh -- Create OBB files for use on Android" + echo "" + echo " -d <directory> Use <directory> as input for OBB files" + echo " -k <key> Use <key> to encrypt OBB file" + echo " -K Prompt for key to encrypt OBB file" + echo " -o <filename> Write OBB file out to <filename>" + echo " -v Verbose mode" + echo " -h Help; this usage screen" +} + +find_binaries +check_prereqs + +use_crypto=0 + +args=`getopt -o d:hk:Ko:v -- "$@"` +eval set -- "$args" + +while true; do \ + case "$1" in + -d) directory=$2; shift 2;; + -h) usage; exit 1;; + -k) key=$2; use_crypto=1; shift 2;; + -K) prompt_key=1; use_crypto=1; shift;; + -v) verbose=1; shift;; + -o) filename=$2; shift 2;; + --) shift; break;; + *) echo "ERROR: Invalid argument in option parsing! Cannot recover. Ever."; exit 1;; + esac +done + +if [ "${directory}x" = "x" -o ! -d "${directory}" ]; then \ + echo "ERROR: Must specify valid input directory" + echo "" + usage + exit 1; +fi + +if [ "${filename}x" = "x" ]; then \ + echo "ERROR: Must specify filename" + echo "" + usage + exit 1; +fi + +if [ ${use_crypto} -eq 1 -a "${key}x" = "x" -a 0${prompt_key} -eq 0 ]; then \ + echo "ERROR: Crypto desired, but no key supplied or requested to prompt for." + exit 1 +fi + +if [ 0${prompt_key} -eq 1 ]; then \ + read_key +fi + +outdir=`dirname ${filename}` +if [ ! -d "${outdir}" ]; then \ + echo "ERROR: Output directory does not exist: ${outdir}" + exit 1 +fi + +# Make sure we clean up any stuff we create from here on during error conditions +trap onexit ERR + +tempfile=$(tempfile -d ${outdir}) || ( echo "ERROR: couldn't create temporary file in ${outdir}"; exit 1 ) + +block_count=`du -s --apparent-size --block-size=512 ${directory} | awk '{ print $1; }'` +if [ $? -ne 0 ]; then \ + echo "ERROR: Couldn't read size of input directory ${directory}" + exit 1 +fi + +echo "Creating temporary file..." +${DDBIN} if=/dev/zero of=${tempfile} bs=${BLOCK_SIZE} count=$((${block_count} + ${SLOP})) > /dev/null 2>&1 +if [ $? -ne 0 ]; then \ + echo "ERROR: creating temporary file: $?" +fi + +loop_dev=$(${LOSETUPBIN} -f) || ( echo "ERROR: losetup wouldn't tell us the next unused device"; exit 1 ) + +${LOSETUPBIN} ${loop_dev} ${tempfile} || ( echo "ERROR: couldn't create loopback device"; exit 1 ) + +if [ ${use_crypto} -eq 1 ]; then \ + eval `${PBKDF2GEN} ${key}` + unique_dm_name=`basename ${tempfile}` + echo "0 `blockdev --getsize ${loop_dev}` crypt ${CRYPTO} ${key} 0 ${loop_dev} 0" | dmsetup create ${unique_dm_name} + old_loop_dev=${loop_dev} + loop_dev=/dev/mapper/${unique_dm_name} +fi + +# +# Create the filesystem +# +echo "" +${MKFSBIN} -I ${loop_dev} +echo "" + +# +# Make the temporary mount point and mount it +# +temp_mount="${MOUNTDIR}/${RANDOM}" +mkdir ${temp_mount} +${MOUNTBIN} -t ${FS} -o loop ${loop_dev} ${temp_mount} + +# +# rsync the files! +# +echo "Copying files:" +${RSYNCBIN} -av --no-owner --no-group ${directory}/ ${temp_mount}/ +echo "" + +echo "Successfully created \`${filename}'" + +if [ ${use_crypto} -eq 1 ]; then \ + echo "salt for use with obbtool is:" + echo "${salt}" +fi + +# +# Undo all the temporaries +# +umount ${temp_mount} +rmdir ${temp_mount} +if [ ${use_crypto} -eq 1 ]; then \ + dmsetup remove -f ${loop_dev} + ${LOSETUPBIN} -d ${old_loop_dev} +else \ + ${LOSETUPBIN} -d ${loop_dev} +fi +mv ${tempfile} ${filename} + +trap - ERR + +exit 0 diff --git a/tools/obbtool/pbkdf2gen.cpp b/tools/obbtool/pbkdf2gen.cpp new file mode 100644 index 000000000000..98d67c0b575c --- /dev/null +++ b/tools/obbtool/pbkdf2gen.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <openssl/evp.h> + +#include <sys/types.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +/** + * Simple program to generate a key based on PBKDF2 with preset inputs. + * + * Will print out the salt and key in hex. + */ + +#define SALT_LEN 8 +#define ROUNDS 1024 +#define KEY_BITS 128 + +int main(int argc, char* argv[]) +{ + if (argc != 2) { + fprintf(stderr, "Usage: %s <password>\n", argv[0]); + exit(1); + } + + int fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) { + fprintf(stderr, "Could not open /dev/urandom: %s\n", strerror(errno)); + close(fd); + exit(1); + } + + unsigned char salt[SALT_LEN]; + + if (read(fd, &salt, SALT_LEN) != SALT_LEN) { + fprintf(stderr, "Could not read salt from /dev/urandom: %s\n", strerror(errno)); + close(fd); + exit(1); + } + close(fd); + + unsigned char rawKey[KEY_BITS]; + + if (PKCS5_PBKDF2_HMAC_SHA1(argv[1], strlen(argv[1]), salt, SALT_LEN, + ROUNDS, KEY_BITS, rawKey) != 1) { + fprintf(stderr, "Could not generate PBKDF2 output: %s\n", strerror(errno)); + exit(1); + } + + printf("salt="); + for (int i = 0; i < SALT_LEN; i++) { + printf("%02x", salt[i]); + } + printf("\n"); + + printf("key="); + for (int i = 0; i < (KEY_BITS / 8); i++) { + printf("%02x", rawKey[i]); + } + printf("\n"); +} diff --git a/tools/orientationplot/README.txt b/tools/orientationplot/README.txt new file mode 100644 index 000000000000..d53f65e6669d --- /dev/null +++ b/tools/orientationplot/README.txt @@ -0,0 +1,87 @@ +This directory contains a simple python script for visualizing +the behavior of the WindowOrientationListener. + + +PREREQUISITES +------------- + +1. Python 2.6 +2. numpy +3. matplotlib + + +USAGE +----- + +The tool works by scaping the debug log output from WindowOrientationListener +for interesting data and then plotting it. + +1. Plug in the device. Ensure that it is the only device plugged in + since this script is of very little brain and will get confused otherwise. + +2. Enable the Window Orientation Listener debugging data log. + adb shell setprop debug.orientation.log true + adb shell stop + adb shell start + +3. Run "orientationplot.py". + + +WHAT IT ALL MEANS +----------------- + +The tool displays several time series graphs that plot the output of the +WindowOrientationListener. Here you can see the raw accelerometer data, +filtered accelerometer data, measured tilt and orientation angle, confidence +intervals for the proposed orientation and accelerometer latency. + +Things to look for: + +1. Ensure the filtering is not too aggressive. If the filter cut-off frequency is + less than about 1Hz, then the filtered accelorometer data becomes too smooth + and the latency for orientation detection goes up. One way to observe this + is by holding the device vertically in one orientation then sharply turning + it 90 degrees to a different orientation. Compared the rapid changes in the + raw accelerometer data with the smoothed out filtered data. If the filtering + is too aggressive, the filter response may lag by hundreds of milliseconds. + +2. Ensure that there is an appropriate gap between adjacent orientation angles + for hysteresis. Try holding the device in one orientation and slowly turning + it 90 degrees. Note that the confidence intervals will all drop to 0 at some + point in between the two orientations; that is the gap. The gap should be + observed between all adjacent pairs of orientations when turning the device + in either direction. + + Next try holding the device in one orientation and rapidly turning it end + over end to a midpoint about 45 degrees between two opposing orientations. + There should be no gap observed initially. The algorithm should pick one + of the orientations and settle into it (since it is obviously quite + different from the original orientation of the device). However, once it + settles, the confidence values should start trending to 0 again because + the measured orientation angle is now within the gap between the new + orientation and the adjacent orientation. + + In other words, the hysteresis gap applies only when the measured orientation + angle (say, 45 degrees) is between the current orientation's ideal angle + (say, 0 degrees) and an adjacent orientation's ideal angle (say, 90 degrees). + +3. Accelerometer jitter. The accelerometer latency graph displays the interval + between sensor events as reported by the SensorEvent.timestamp field. It + should be a fairly constant 60ms. If the latency jumps around wildly or + greatly exceeds 60ms then there is a problem with the accelerometer or the + sensor manager. + +4. The orientation angle is not measured when the tilt is too close to 90 or -90 + degrees (refer to MAX_TILT constant). Consequently, you should expect there + to be no data. Likewise, all dependent calculations are suppressed in this case + so there will be no orientation proposal either. + +5. Each orientation has its own bound on allowable tilt angles. It's a good idea to + verify that these limits are being enforced by gradually varying the tilt of + the device until it is inside/outside the limit for each orientation. + +6. Orientation changes should be significantly harder when the device is held + overhead. People reading on tablets in bed often have their head turned + a little to the side, or they hold the device loosely so its orientation + can be a bit unusual. The tilt is a good indicator of whether the device is + overhead. diff --git a/tools/orientationplot/orientationplot.py b/tools/orientationplot/orientationplot.py new file mode 100755 index 000000000000..6fc392275ffd --- /dev/null +++ b/tools/orientationplot/orientationplot.py @@ -0,0 +1,457 @@ +#!/usr/bin/env python2.6 +# +# Copyright (C) 2011 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Plots debug log output from WindowOrientationListener. +# See README.txt for details. +# + +import numpy as np +import matplotlib.pyplot as plot +import subprocess +import re +import fcntl +import os +import errno +import bisect +from datetime import datetime, timedelta + +# Parameters. +timespan = 15 # seconds total span shown +scrolljump = 5 # seconds jump when scrolling +timeticks = 1 # seconds between each time tick + +# Non-blocking stream wrapper. +class NonBlockingStream: + def __init__(self, stream): + fcntl.fcntl(stream, fcntl.F_SETFL, os.O_NONBLOCK) + self.stream = stream + self.buffer = '' + self.pos = 0 + + def readline(self): + while True: + index = self.buffer.find('\n', self.pos) + if index != -1: + result = self.buffer[self.pos:index] + self.pos = index + 1 + return result + + self.buffer = self.buffer[self.pos:] + self.pos = 0 + try: + chunk = os.read(self.stream.fileno(), 4096) + except OSError, e: + if e.errno == errno.EAGAIN: + return None + raise e + if len(chunk) == 0: + if len(self.buffer) == 0: + raise(EOFError) + else: + result = self.buffer + self.buffer = '' + self.pos = 0 + return result + self.buffer += chunk + +# Plotter +class Plotter: + def __init__(self, adbout): + self.adbout = adbout + + self.fig = plot.figure(1) + self.fig.suptitle('Window Orientation Listener', fontsize=12) + self.fig.set_dpi(96) + self.fig.set_size_inches(16, 12, forward=True) + + self.raw_acceleration_x = self._make_timeseries() + self.raw_acceleration_y = self._make_timeseries() + self.raw_acceleration_z = self._make_timeseries() + self.raw_acceleration_magnitude = self._make_timeseries() + self.raw_acceleration_axes = self._add_timeseries_axes( + 1, 'Raw Acceleration', 'm/s^2', [-20, 20], + yticks=range(-15, 16, 5)) + self.raw_acceleration_line_x = self._add_timeseries_line( + self.raw_acceleration_axes, 'x', 'red') + self.raw_acceleration_line_y = self._add_timeseries_line( + self.raw_acceleration_axes, 'y', 'green') + self.raw_acceleration_line_z = self._add_timeseries_line( + self.raw_acceleration_axes, 'z', 'blue') + self.raw_acceleration_line_magnitude = self._add_timeseries_line( + self.raw_acceleration_axes, 'magnitude', 'orange', linewidth=2) + self._add_timeseries_legend(self.raw_acceleration_axes) + + shared_axis = self.raw_acceleration_axes + + self.filtered_acceleration_x = self._make_timeseries() + self.filtered_acceleration_y = self._make_timeseries() + self.filtered_acceleration_z = self._make_timeseries() + self.filtered_acceleration_magnitude = self._make_timeseries() + self.filtered_acceleration_axes = self._add_timeseries_axes( + 2, 'Filtered Acceleration', 'm/s^2', [-20, 20], + sharex=shared_axis, + yticks=range(-15, 16, 5)) + self.filtered_acceleration_line_x = self._add_timeseries_line( + self.filtered_acceleration_axes, 'x', 'red') + self.filtered_acceleration_line_y = self._add_timeseries_line( + self.filtered_acceleration_axes, 'y', 'green') + self.filtered_acceleration_line_z = self._add_timeseries_line( + self.filtered_acceleration_axes, 'z', 'blue') + self.filtered_acceleration_line_magnitude = self._add_timeseries_line( + self.filtered_acceleration_axes, 'magnitude', 'orange', linewidth=2) + self._add_timeseries_legend(self.filtered_acceleration_axes) + + self.tilt_angle = self._make_timeseries() + self.tilt_angle_axes = self._add_timeseries_axes( + 3, 'Tilt Angle', 'degrees', [-105, 105], + sharex=shared_axis, + yticks=range(-90, 91, 30)) + self.tilt_angle_line = self._add_timeseries_line( + self.tilt_angle_axes, 'tilt', 'black') + self._add_timeseries_legend(self.tilt_angle_axes) + + self.orientation_angle = self._make_timeseries() + self.orientation_angle_axes = self._add_timeseries_axes( + 4, 'Orientation Angle', 'degrees', [-25, 375], + sharex=shared_axis, + yticks=range(0, 361, 45)) + self.orientation_angle_line = self._add_timeseries_line( + self.orientation_angle_axes, 'orientation', 'black') + self._add_timeseries_legend(self.orientation_angle_axes) + + self.current_rotation = self._make_timeseries() + self.proposed_rotation = self._make_timeseries() + self.predicted_rotation = self._make_timeseries() + self.orientation_axes = self._add_timeseries_axes( + 5, 'Current / Proposed Orientation', 'rotation', [-1, 4], + sharex=shared_axis, + yticks=range(0, 4)) + self.current_rotation_line = self._add_timeseries_line( + self.orientation_axes, 'current', 'black', linewidth=2) + self.predicted_rotation_line = self._add_timeseries_line( + self.orientation_axes, 'predicted', 'purple', linewidth=3) + self.proposed_rotation_line = self._add_timeseries_line( + self.orientation_axes, 'proposed', 'green', linewidth=3) + self._add_timeseries_legend(self.orientation_axes) + + self.time_until_settled = self._make_timeseries() + self.time_until_flat_delay_expired = self._make_timeseries() + self.time_until_swing_delay_expired = self._make_timeseries() + self.time_until_acceleration_delay_expired = self._make_timeseries() + self.stability_axes = self._add_timeseries_axes( + 6, 'Proposal Stability', 'ms', [-10, 600], + sharex=shared_axis, + yticks=range(0, 600, 100)) + self.time_until_settled_line = self._add_timeseries_line( + self.stability_axes, 'time until settled', 'black', linewidth=2) + self.time_until_flat_delay_expired_line = self._add_timeseries_line( + self.stability_axes, 'time until flat delay expired', 'green') + self.time_until_swing_delay_expired_line = self._add_timeseries_line( + self.stability_axes, 'time until swing delay expired', 'blue') + self.time_until_acceleration_delay_expired_line = self._add_timeseries_line( + self.stability_axes, 'time until acceleration delay expired', 'red') + self._add_timeseries_legend(self.stability_axes) + + self.sample_latency = self._make_timeseries() + self.sample_latency_axes = self._add_timeseries_axes( + 7, 'Accelerometer Sampling Latency', 'ms', [-10, 500], + sharex=shared_axis, + yticks=range(0, 500, 100)) + self.sample_latency_line = self._add_timeseries_line( + self.sample_latency_axes, 'latency', 'black') + self._add_timeseries_legend(self.sample_latency_axes) + + self.fig.canvas.mpl_connect('button_press_event', self._on_click) + self.paused = False + + self.timer = self.fig.canvas.new_timer(interval=100) + self.timer.add_callback(lambda: self.update()) + self.timer.start() + + self.timebase = None + self._reset_parse_state() + + # Handle a click event to pause or restart the timer. + def _on_click(self, ev): + if not self.paused: + self.paused = True + self.timer.stop() + else: + self.paused = False + self.timer.start() + + # Initialize a time series. + def _make_timeseries(self): + return [[], []] + + # Add a subplot to the figure for a time series. + def _add_timeseries_axes(self, index, title, ylabel, ylim, yticks, sharex=None): + num_graphs = 7 + height = 0.9 / num_graphs + top = 0.95 - height * index + axes = self.fig.add_axes([0.1, top, 0.8, height], + xscale='linear', + xlim=[0, timespan], + ylabel=ylabel, + yscale='linear', + ylim=ylim, + sharex=sharex) + axes.text(0.02, 0.02, title, transform=axes.transAxes, fontsize=10, fontweight='bold') + axes.set_xlabel('time (s)', fontsize=10, fontweight='bold') + axes.set_ylabel(ylabel, fontsize=10, fontweight='bold') + axes.set_xticks(range(0, timespan + 1, timeticks)) + axes.set_yticks(yticks) + axes.grid(True) + + for label in axes.get_xticklabels(): + label.set_fontsize(9) + for label in axes.get_yticklabels(): + label.set_fontsize(9) + + return axes + + # Add a line to the axes for a time series. + def _add_timeseries_line(self, axes, label, color, linewidth=1): + return axes.plot([], label=label, color=color, linewidth=linewidth)[0] + + # Add a legend to a time series. + def _add_timeseries_legend(self, axes): + axes.legend( + loc='upper left', + bbox_to_anchor=(1.01, 1), + borderpad=0.1, + borderaxespad=0.1, + prop={'size': 10}) + + # Resets the parse state. + def _reset_parse_state(self): + self.parse_raw_acceleration_x = None + self.parse_raw_acceleration_y = None + self.parse_raw_acceleration_z = None + self.parse_raw_acceleration_magnitude = None + self.parse_filtered_acceleration_x = None + self.parse_filtered_acceleration_y = None + self.parse_filtered_acceleration_z = None + self.parse_filtered_acceleration_magnitude = None + self.parse_tilt_angle = None + self.parse_orientation_angle = None + self.parse_current_rotation = None + self.parse_proposed_rotation = None + self.parse_predicted_rotation = None + self.parse_time_until_settled = None + self.parse_time_until_flat_delay_expired = None + self.parse_time_until_swing_delay_expired = None + self.parse_time_until_acceleration_delay_expired = None + self.parse_sample_latency = None + + # Update samples. + def update(self): + timeindex = 0 + while True: + try: + line = self.adbout.readline() + except EOFError: + plot.close() + return + if line is None: + break + print line + + try: + timestamp = self._parse_timestamp(line) + except ValueError, e: + continue + if self.timebase is None: + self.timebase = timestamp + delta = timestamp - self.timebase + timeindex = delta.seconds + delta.microseconds * 0.000001 + + if line.find('Raw acceleration vector:') != -1: + self.parse_raw_acceleration_x = self._get_following_number(line, 'x=') + self.parse_raw_acceleration_y = self._get_following_number(line, 'y=') + self.parse_raw_acceleration_z = self._get_following_number(line, 'z=') + self.parse_raw_acceleration_magnitude = self._get_following_number(line, 'magnitude=') + + if line.find('Filtered acceleration vector:') != -1: + self.parse_filtered_acceleration_x = self._get_following_number(line, 'x=') + self.parse_filtered_acceleration_y = self._get_following_number(line, 'y=') + self.parse_filtered_acceleration_z = self._get_following_number(line, 'z=') + self.parse_filtered_acceleration_magnitude = self._get_following_number(line, 'magnitude=') + + if line.find('tiltAngle=') != -1: + self.parse_tilt_angle = self._get_following_number(line, 'tiltAngle=') + + if line.find('orientationAngle=') != -1: + self.parse_orientation_angle = self._get_following_number(line, 'orientationAngle=') + + if line.find('Result:') != -1: + self.parse_current_rotation = self._get_following_number(line, 'currentRotation=') + self.parse_proposed_rotation = self._get_following_number(line, 'proposedRotation=') + self.parse_predicted_rotation = self._get_following_number(line, 'predictedRotation=') + self.parse_sample_latency = self._get_following_number(line, 'timeDeltaMS=') + self.parse_time_until_settled = self._get_following_number(line, 'timeUntilSettledMS=') + self.parse_time_until_flat_delay_expired = self._get_following_number(line, 'timeUntilFlatDelayExpiredMS=') + self.parse_time_until_swing_delay_expired = self._get_following_number(line, 'timeUntilSwingDelayExpiredMS=') + self.parse_time_until_acceleration_delay_expired = self._get_following_number(line, 'timeUntilAccelerationDelayExpiredMS=') + + self._append(self.raw_acceleration_x, timeindex, self.parse_raw_acceleration_x) + self._append(self.raw_acceleration_y, timeindex, self.parse_raw_acceleration_y) + self._append(self.raw_acceleration_z, timeindex, self.parse_raw_acceleration_z) + self._append(self.raw_acceleration_magnitude, timeindex, self.parse_raw_acceleration_magnitude) + self._append(self.filtered_acceleration_x, timeindex, self.parse_filtered_acceleration_x) + self._append(self.filtered_acceleration_y, timeindex, self.parse_filtered_acceleration_y) + self._append(self.filtered_acceleration_z, timeindex, self.parse_filtered_acceleration_z) + self._append(self.filtered_acceleration_magnitude, timeindex, self.parse_filtered_acceleration_magnitude) + self._append(self.tilt_angle, timeindex, self.parse_tilt_angle) + self._append(self.orientation_angle, timeindex, self.parse_orientation_angle) + self._append(self.current_rotation, timeindex, self.parse_current_rotation) + if self.parse_proposed_rotation >= 0: + self._append(self.proposed_rotation, timeindex, self.parse_proposed_rotation) + else: + self._append(self.proposed_rotation, timeindex, None) + if self.parse_predicted_rotation >= 0: + self._append(self.predicted_rotation, timeindex, self.parse_predicted_rotation) + else: + self._append(self.predicted_rotation, timeindex, None) + self._append(self.time_until_settled, timeindex, self.parse_time_until_settled) + self._append(self.time_until_flat_delay_expired, timeindex, self.parse_time_until_flat_delay_expired) + self._append(self.time_until_swing_delay_expired, timeindex, self.parse_time_until_swing_delay_expired) + self._append(self.time_until_acceleration_delay_expired, timeindex, self.parse_time_until_acceleration_delay_expired) + self._append(self.sample_latency, timeindex, self.parse_sample_latency) + self._reset_parse_state() + + # Scroll the plots. + if timeindex > timespan: + bottom = int(timeindex) - timespan + scrolljump + self.timebase += timedelta(seconds=bottom) + self._scroll(self.raw_acceleration_x, bottom) + self._scroll(self.raw_acceleration_y, bottom) + self._scroll(self.raw_acceleration_z, bottom) + self._scroll(self.raw_acceleration_magnitude, bottom) + self._scroll(self.filtered_acceleration_x, bottom) + self._scroll(self.filtered_acceleration_y, bottom) + self._scroll(self.filtered_acceleration_z, bottom) + self._scroll(self.filtered_acceleration_magnitude, bottom) + self._scroll(self.tilt_angle, bottom) + self._scroll(self.orientation_angle, bottom) + self._scroll(self.current_rotation, bottom) + self._scroll(self.proposed_rotation, bottom) + self._scroll(self.predicted_rotation, bottom) + self._scroll(self.time_until_settled, bottom) + self._scroll(self.time_until_flat_delay_expired, bottom) + self._scroll(self.time_until_swing_delay_expired, bottom) + self._scroll(self.time_until_acceleration_delay_expired, bottom) + self._scroll(self.sample_latency, bottom) + + # Redraw the plots. + self.raw_acceleration_line_x.set_data(self.raw_acceleration_x) + self.raw_acceleration_line_y.set_data(self.raw_acceleration_y) + self.raw_acceleration_line_z.set_data(self.raw_acceleration_z) + self.raw_acceleration_line_magnitude.set_data(self.raw_acceleration_magnitude) + self.filtered_acceleration_line_x.set_data(self.filtered_acceleration_x) + self.filtered_acceleration_line_y.set_data(self.filtered_acceleration_y) + self.filtered_acceleration_line_z.set_data(self.filtered_acceleration_z) + self.filtered_acceleration_line_magnitude.set_data(self.filtered_acceleration_magnitude) + self.tilt_angle_line.set_data(self.tilt_angle) + self.orientation_angle_line.set_data(self.orientation_angle) + self.current_rotation_line.set_data(self.current_rotation) + self.proposed_rotation_line.set_data(self.proposed_rotation) + self.predicted_rotation_line.set_data(self.predicted_rotation) + self.time_until_settled_line.set_data(self.time_until_settled) + self.time_until_flat_delay_expired_line.set_data(self.time_until_flat_delay_expired) + self.time_until_swing_delay_expired_line.set_data(self.time_until_swing_delay_expired) + self.time_until_acceleration_delay_expired_line.set_data(self.time_until_acceleration_delay_expired) + self.sample_latency_line.set_data(self.sample_latency) + + self.fig.canvas.draw_idle() + + # Scroll a time series. + def _scroll(self, timeseries, bottom): + bottom_index = bisect.bisect_left(timeseries[0], bottom) + del timeseries[0][:bottom_index] + del timeseries[1][:bottom_index] + for i, timeindex in enumerate(timeseries[0]): + timeseries[0][i] = timeindex - bottom + + # Extract a word following the specified prefix. + def _get_following_word(self, line, prefix): + prefix_index = line.find(prefix) + if prefix_index == -1: + return None + start_index = prefix_index + len(prefix) + delim_index = line.find(',', start_index) + if delim_index == -1: + return line[start_index:] + else: + return line[start_index:delim_index] + + # Extract a number following the specified prefix. + def _get_following_number(self, line, prefix): + word = self._get_following_word(line, prefix) + if word is None: + return None + return float(word) + + # Extract an array of numbers following the specified prefix. + def _get_following_array_of_numbers(self, line, prefix): + prefix_index = line.find(prefix + '[') + if prefix_index == -1: + return None + start_index = prefix_index + len(prefix) + 1 + delim_index = line.find(']', start_index) + if delim_index == -1: + return None + + result = [] + while start_index < delim_index: + comma_index = line.find(', ', start_index, delim_index) + if comma_index == -1: + result.append(float(line[start_index:delim_index])) + break; + result.append(float(line[start_index:comma_index])) + start_index = comma_index + 2 + return result + + # Add a value to a time series. + def _append(self, timeseries, timeindex, number): + timeseries[0].append(timeindex) + timeseries[1].append(number) + + # Parse the logcat timestamp. + # Timestamp has the form '01-21 20:42:42.930' + def _parse_timestamp(self, line): + return datetime.strptime(line[0:18], '%m-%d %H:%M:%S.%f') + +# Notice +print "Window Orientation Listener plotting tool" +print "-----------------------------------------\n" +print "Please turn on the Window Orientation Listener logging in Development Settings." + +# Start adb. +print "Starting adb logcat.\n" + +adb = subprocess.Popen(['adb', 'logcat', '-s', '-v', 'time', 'WindowOrientationListener:V'], + stdout=subprocess.PIPE) +adbout = NonBlockingStream(adb.stdout) + +# Prepare plotter. +plotter = Plotter(adbout) +plotter.update() + +# Main loop. +plot.show() diff --git a/tools/preload/20080522.compiled b/tools/preload/20080522.compiled Binary files differnew file mode 100644 index 000000000000..a2af422866eb --- /dev/null +++ b/tools/preload/20080522.compiled diff --git a/tools/preload/20090811.compiled b/tools/preload/20090811.compiled Binary files differnew file mode 100644 index 000000000000..6dbeca094df5 --- /dev/null +++ b/tools/preload/20090811.compiled diff --git a/tools/preload/20100223.compiled b/tools/preload/20100223.compiled Binary files differnew file mode 100644 index 000000000000..305638889a6d --- /dev/null +++ b/tools/preload/20100223.compiled diff --git a/tools/preload/Android.mk b/tools/preload/Android.mk new file mode 100644 index 000000000000..f32587072788 --- /dev/null +++ b/tools/preload/Android.mk @@ -0,0 +1,23 @@ +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + Compile.java \ + LoadedClass.java \ + MemoryUsage.java \ + Operation.java \ + Policy.java \ + PrintCsv.java \ + PrintHtmlDiff.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/Compile.java b/tools/preload/Compile.java new file mode 100644 index 000000000000..67258ef84209 --- /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 000000000000..86e5dfc0331a --- /dev/null +++ b/tools/preload/LoadedClass.java @@ -0,0 +1,130 @@ +/* + * 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.*; + +/** + * 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); + } + + int medianTimeMicros() { + return medianInitTimeMicros() + medianLoadTimeMicros(); + } + + /** 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; + } + } + + /** Returns names of processes that loaded this class. */ + Set<String> processNames() { + Set<String> names = new HashSet<String>(); + addProcessNames(loads, names); + addProcessNames(initializations, names); + return names; + } + + private void addProcessNames(List<Operation> ops, Set<String> names) { + for (Operation operation : ops) { + if (operation.process.fromZygote()) { + names.add(operation.process.name); + } + } + } + + public int compareTo(LoadedClass o) { + return name.compareTo(o.name); + } + + @Override + public String toString() { + return name; + } +} diff --git a/tools/preload/MemoryUsage.java b/tools/preload/MemoryUsage.java new file mode 100644 index 000000000000..d8f95f4371a5 --- /dev/null +++ b/tools/preload/MemoryUsage.java @@ -0,0 +1,298 @@ +/* + * 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; + + // These values are in 1kB increments (not 4kB like you'd expect). + 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 totalHeap() { + return javaHeapSize() + (int) nativeHeapSize; + } + + int javaPagesInK() { + return javaSharedPages + javaPrivatePages; + } + + int nativePagesInK() { + return nativeSharedPages + nativePrivatePages; + } + int otherPagesInK() { + return otherSharedPages + otherPrivatePages; + } + + int totalPages() { + return javaSharedPages + javaPrivatePages + nativeSharedPages + + nativePrivatePages + otherSharedPages + otherPrivatePages; + } + + /** + * 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", "shell", "dalvikvm", CLASS_PATH, "LoadClass" }; + + /** + * Measures memory usage for the given class. + */ + static MemoryUsage forClass(String className) { + 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 + "; command was " + Arrays.toString(commands)); + 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(); + } + } + + /** Measures memory usage information and stores it in the model. */ + public static void main(String[] args) throws IOException, + ClassNotFoundException { + Root root = Root.fromFile(args[0]); + root.baseline = baseline(); + for (LoadedClass loadedClass : root.loadedClasses.values()) { + if (loadedClass.systemClass) { + loadedClass.measureMemoryUsage(); + } + } + root.toFile(args[0]); + } +} diff --git a/tools/preload/Operation.java b/tools/preload/Operation.java new file mode 100644 index 000000000000..4f1938e66ebd --- /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 000000000000..af46820cd460 --- /dev/null +++ b/tools/preload/Policy.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.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Policy that governs which classes are preloaded. + */ +public class Policy { + + /** + * No constructor - use static methods only + */ + private Policy() {} + + /** + * This location (in the build system) of the preloaded-classes file. + */ + static final String PRELOADED_CLASS_FILE + = "frameworks/base/preloaded-classes"; + + /** + * Long running services. These are restricted in their contribution to the + * preloader because their launch time is less critical. + */ + // TODO: Generate this automatically from package manager. + private static final Set<String> SERVICES = new HashSet<String>(Arrays.asList( + "system_server", + "com.google.process.content", + "android.process.media", + "com.android.bluetooth", + "com.android.calendar", + "com.android.inputmethod.latin", + "com.android.phone", + "com.google.android.apps.maps.FriendService", // pre froyo + "com.google.android.apps.maps:FriendService", // froyo + "com.google.android.apps.maps.LocationFriendService", + "com.google.android.deskclock", + "com.google.process.gapps", + "android.tts" + )); + + /** + * 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", + "android.webkit.WebViewClassic$1", + "java.lang.ProcessManager" + )); + + /** + * Returns true 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 be preloaded. */ + public static boolean isPreloadable(LoadedClass clazz) { + return clazz.systemClass && !EXCLUDED_CLASSES.contains(clazz.name) + && !clazz.name.endsWith("$NoPreloadHolder"); + } +} diff --git a/tools/preload/PrintCsv.java b/tools/preload/PrintCsv.java new file mode 100644 index 000000000000..182083046471 --- /dev/null +++ b/tools/preload/PrintCsv.java @@ -0,0 +1,127 @@ +/* + * 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; +import java.io.Writer; +import java.io.PrintStream; +import java.util.Set; +import java.util.HashSet; +import java.util.TreeSet; +import java.util.Iterator; + +/** + * 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]); + + printHeaders(System.out); + + MemoryUsage baseline = MemoryUsage.baseline(); + + for (LoadedClass loadedClass : root.loadedClasses.values()) { + if (!loadedClass.systemClass) { + continue; + } + + printRow(System.out, baseline, loadedClass); + } + } + + static void printHeaders(PrintStream out) { + out.println("Name" + + ",Preloaded" + + ",Median Load Time (us)" + + ",Median Init Time (us)" + + ",Process Names" + + ",Load Count" + + ",Init Count" + + ",Managed Heap (B)" + + ",Native Heap (B)" + + ",Managed Pages (kB)" + + ",Native Pages (kB)" + + ",Other Pages (kB)"); + } + + static void printRow(PrintStream out, MemoryUsage baseline, + LoadedClass loadedClass) { + out.print(loadedClass.name); + out.print(','); + out.print(loadedClass.preloaded); + out.print(','); + out.print(loadedClass.medianLoadTimeMicros()); + out.print(','); + out.print(loadedClass.medianInitTimeMicros()); + out.print(','); + out.print('"'); + + Set<String> procNames = new TreeSet<String>(); + for (Operation op : loadedClass.loads) + procNames.add(op.process.name); + for (Operation op : loadedClass.initializations) + procNames.add(op.process.name); + + if (procNames.size() <= 3) { + for (String name : procNames) { + out.print(name + "\n"); + } + } else { + Iterator<String> i = procNames.iterator(); + out.print(i.next() + "\n"); + out.print(i.next() + "\n"); + out.print("...and " + (procNames.size() - 2) + + " others."); + } + + out.print('"'); + out.print(','); + out.print(loadedClass.loads.size()); + out.print(','); + out.print(loadedClass.initializations.size()); + + if (loadedClass.memoryUsage.isAvailable()) { + MemoryUsage subtracted + = loadedClass.memoryUsage.subtract(baseline); + + out.print(','); + out.print(subtracted.javaHeapSize()); + out.print(','); + out.print(subtracted.nativeHeapSize); + out.print(','); + out.print(subtracted.javaPagesInK()); + out.print(','); + out.print(subtracted.nativePagesInK()); + out.print(','); + out.print(subtracted.otherPagesInK()); + + } else { + out.print(",n/a,n/a,n/a,n/a,n/a"); + } + + out.println(); + } +} diff --git a/tools/preload/PrintHtmlDiff.java b/tools/preload/PrintHtmlDiff.java new file mode 100644 index 000000000000..b101c85185c0 --- /dev/null +++ b/tools/preload/PrintHtmlDiff.java @@ -0,0 +1,142 @@ +/* + * 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. + */ + +import java.io.IOException; +import java.io.FileReader; +import java.io.BufferedReader; +import java.io.PrintStream; +import java.util.Set; +import java.util.TreeSet; +import java.util.HashSet; +import java.util.Iterator; + +/** + * Prints HTML containing removed and added files. + */ +public class PrintHtmlDiff { + + private static final String OLD_PRELOADED_CLASSES + = "old-preloaded-classes"; + + public static void main(String[] args) throws IOException, + ClassNotFoundException { + Root root = Root.fromFile(args[0]); + + BufferedReader oldClasses = new BufferedReader( + new FileReader(OLD_PRELOADED_CLASSES)); + + // Classes loaded implicitly by the zygote. + Set<LoadedClass> zygote = new HashSet<LoadedClass>(); + for (Proc proc : root.processes.values()) { + if (proc.name.equals("zygote")) { + for (Operation op : proc.operations) { + zygote.add(op.loadedClass); + } + break; + } + } + + Set<LoadedClass> removed = new TreeSet<LoadedClass>(); + Set<LoadedClass> added = new TreeSet<LoadedClass>(); + + for (LoadedClass loadedClass : root.loadedClasses.values()) { + if (loadedClass.preloaded && !zygote.contains(loadedClass)) { + added.add(loadedClass); + } + } + + String line; + while ((line = oldClasses.readLine()) != null) { + line = line.trim(); + LoadedClass clazz = root.loadedClasses.get(line); + if (clazz != null) { + added.remove(clazz); + if (!clazz.preloaded) removed.add(clazz); + } + } + + PrintStream out = System.out; + + out.println("<html><body>"); + out.println("<style>"); + out.println("a, th, td, h2 { font-family: arial }"); + out.println("th, td { font-size: small }"); + out.println("</style>"); + out.println("<script src=\"sorttable.js\"></script>"); + out.println("<p><a href=\"#removed\">Removed</a>"); + out.println("<a name=\"added\"/><h2>Added</h2>"); + printTable(out, root.baseline, added); + out.println("<a name=\"removed\"/><h2>Removed</h2>"); + printTable(out, root.baseline, removed); + out.println("</body></html>"); + } + + static void printTable(PrintStream out, MemoryUsage baseline, + Iterable<LoadedClass> classes) { + out.println("<table border=\"1\" cellpadding=\"5\"" + + " class=\"sortable\">"); + + out.println("<thead><tr>"); + out.println("<th>Name</th>"); + out.println("<th>Load Time (us)</th>"); + out.println("<th>Loaded By</th>"); + out.println("<th>Heap (B)</th>"); + out.println("<th>Pages</th>"); + out.println("</tr></thead>"); + + for (LoadedClass clazz : classes) { + out.println("<tr>"); + out.println("<td>" + clazz.name + "</td>"); + out.println("<td>" + clazz.medianTimeMicros() + "</td>"); + + out.println("<td>"); + Set<String> procNames = new TreeSet<String>(); + for (Operation op : clazz.loads) procNames.add(op.process.name); + for (Operation op : clazz.initializations) { + procNames.add(op.process.name); + } + if (procNames.size() <= 3) { + for (String name : procNames) { + out.print(name + "<br/>"); + } + } else { + Iterator<String> i = procNames.iterator(); + out.print(i.next() + "<br/>"); + out.print(i.next() + "<br/>"); + out.print("...and " + (procNames.size() - 2) + + " others."); + } + out.println("</td>"); + + if (clazz.memoryUsage.isAvailable()) { + MemoryUsage subtracted + = clazz.memoryUsage.subtract(baseline); + + out.println("<td>" + (subtracted.javaHeapSize() + + subtracted.nativeHeapSize) + "</td>"); + out.println("<td>" + subtracted.totalPages() + "</td>"); + } else { + for (int i = 0; i < 2; i++) { + out.println("<td>n/a</td>"); + } + } + + out.println("</tr>"); + } + + out.println("</table>"); + } +} diff --git a/tools/preload/PrintPsTree.java b/tools/preload/PrintPsTree.java new file mode 100644 index 000000000000..22701faba707 --- /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 000000000000..21050218ff8c --- /dev/null +++ b/tools/preload/Proc.java @@ -0,0 +1,168 @@ +/* + * 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.util.LinkedList; +import java.util.Map; +import java.util.HashMap; +import java.io.Serializable; + +/** + * A Dalvik process. + */ +class Proc implements Serializable { + + private static final long serialVersionUID = 0; + + /** 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 true if this process comes from the zygote. + */ + public boolean fromZygote() { + return parent != null && parent.name.equals("zygote") + && !name.equals("com.android.development"); + } + + /** + * 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 000000000000..d0a2af4d7965 --- /dev/null +++ b/tools/preload/Record.java @@ -0,0 +1,182 @@ +/* + * 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 { + + /** + * The delimiter character we use, {@code :}, conflicts with some other + * names. In that case, manually replace the delimiter with something else. + */ + private static final String[] REPLACE_CLASSES = { + "com.google.android.apps.maps:FriendService", + "com.google.android.apps.maps\\u003AFriendService", + "com.google.android.apps.maps:driveabout", + "com.google.android.apps.maps\\u003Adriveabout", + "com.google.android.apps.maps:GoogleLocationService", + "com.google.android.apps.maps\\u003AGoogleLocationService", + "com.google.android.apps.maps:LocationFriendService", + "com.google.android.apps.maps\\u003ALocationFriendService", + "com.google.android.apps.maps:MapsBackgroundService", + "com.google.android.apps.maps\\u003AMapsBackgroundService", + "com.google.android.apps.maps:NetworkLocationService", + "com.google.android.apps.maps\\u003ANetworkLocationService", + "com.android.chrome:sandboxed_process", + "com.android.chrome\\u003Asandboxed_process", + "com.android.fakeoemfeatures:background", + "com.android.fakeoemfeatures\\u003Abackground", + "com.android.fakeoemfeatures:core", + "com.android.fakeoemfeatures\\u003Acore", + "com.android.launcher:wallpaper_chooser", + "com.android.launcher\\u003Awallpaper_chooser", + "com.android.nfc:handover", + "com.android.nfc\\u003Ahandover", + "com.google.android.music:main", + "com.google.android.music\\u003Amain", + "com.google.android.music:ui", + "com.google.android.music\\u003Aui", + "com.google.android.setupwarlock:broker", + "com.google.android.setupwarlock\\u003Abroker", + "mobi.mgeek.TunnyBrowser:DolphinNotification", + "mobi.mgeek.TunnyBrowser\\u003ADolphinNotification", + "com.qo.android.sp.oem:Quickword", + "com.qo.android.sp.oem\\u003AQuickword", + "android:ui", + "android\\u003Aui", + "system:ui", + "system\\u003Aui", + }; + + 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; + + for (int i = 0; i < REPLACE_CLASSES.length; i+= 2) { + line = line.replace(REPLACE_CLASSES[i], REPLACE_CLASSES[i+1]); + } + + 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 000000000000..0bc29bfdd937 --- /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>(); + + 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 000000000000..b067bc278b1a --- /dev/null +++ b/tools/preload/WritePreloadedClassFile.java @@ -0,0 +1,155 @@ +/* + * 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.Set; +import java.util.TreeSet; + +/** + * Writes /frameworks/base/preloaded-classes. Also updates + * {@link LoadedClass#preloaded} fields and writes over compiled log file. + */ +public class WritePreloadedClassFile { + + /** + * Preload any class that take longer to load than MIN_LOAD_TIME_MICROS us. + */ + static final int MIN_LOAD_TIME_MICROS = 1250; + + /** + * Preload any class that was loaded by at least MIN_PROCESSES processes. + */ + static final int MIN_PROCESSES = 10; + + public static void main(String[] args) throws IOException, + ClassNotFoundException { + if (args.length != 1) { + System.err.println("Usage: WritePreloadedClassFile [compiled log]"); + System.exit(-1); + } + String rootFile = args[0]; + Root root = Root.fromFile(rootFile); + + // No classes are preloaded to start. + for (LoadedClass loadedClass : root.loadedClasses.values()) { + loadedClass.preloaded = false; + } + + // Open preloaded-classes file for output. + Writer out = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(Policy.PRELOADED_CLASS_FILE), + 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/" + + WritePreloadedClassFile.class.getSimpleName() + ".java.\n"); + out.write("# MIN_LOAD_TIME_MICROS=" + MIN_LOAD_TIME_MICROS + "\n"); + out.write("# MIN_PROCESSES=" + MIN_PROCESSES + "\n"); + + /* + * The set of classes to preload. We preload a class if: + * + * a) it's loaded in the bootclasspath (i.e., is a system class) + * b) it takes > MIN_LOAD_TIME_MICROS us to load, and + * c) it's loaded by more than one process, or it's loaded by an + * application (i.e., not a long running service) + */ + Set<LoadedClass> toPreload = new TreeSet<LoadedClass>(); + + // Preload classes that were loaded by at least 2 processes. Hopefully, + // the memory associated with these classes will be shared. + for (LoadedClass loadedClass : root.loadedClasses.values()) { + Set<String> names = loadedClass.processNames(); + if (!Policy.isPreloadable(loadedClass)) { + continue; + } + + if (names.size() >= MIN_PROCESSES || + (loadedClass.medianTimeMicros() > MIN_LOAD_TIME_MICROS && names.size() > 1)) { + toPreload.add(loadedClass); + } + } + + int initialSize = toPreload.size(); + System.out.println(initialSize + + " classses were loaded by more than one app."); + + // Preload eligable classes from applications (not long-running + // services). + for (Proc proc : root.processes.values()) { + if (proc.fromZygote() && !Policy.isService(proc.name)) { + for (Operation operation : proc.operations) { + LoadedClass loadedClass = operation.loadedClass; + if (shouldPreload(loadedClass)) { + toPreload.add(loadedClass); + } + } + } + } + + System.out.println("Added " + (toPreload.size() - initialSize) + + " more to speed up applications."); + + System.out.println(toPreload.size() + + " total classes will be preloaded."); + + // Make classes that were implicitly loaded by the zygote explicit. + // This adds minimal overhead but avoid confusion about classes not + // appearing in the list. + addAllClassesFrom("zygote", root, toPreload); + + for (LoadedClass loadedClass : toPreload) { + out.write(loadedClass.name + "\n"); + } + + out.close(); + + // Update data to reflect LoadedClass.preloaded changes. + for (LoadedClass loadedClass : toPreload) { + loadedClass.preloaded = true; + } + root.toFile(rootFile); + } + + private static void addAllClassesFrom(String processName, Root root, + Set<LoadedClass> toPreload) { + for (Proc proc : root.processes.values()) { + if (proc.name.equals(processName)) { + for (Operation operation : proc.operations) { + boolean preloadable + = Policy.isPreloadable(operation.loadedClass); + if (preloadable) { + toPreload.add(operation.loadedClass); + } + } + } + } + } + + /** + * Returns true if the class should be preloaded. + */ + private static boolean shouldPreload(LoadedClass clazz) { + return Policy.isPreloadable(clazz) + && clazz.medianTimeMicros() > MIN_LOAD_TIME_MICROS; + } +} diff --git a/tools/preload/loadclass/Android.mk b/tools/preload/loadclass/Android.mk new file mode 100644 index 000000000000..65828be617df --- /dev/null +++ b/tools/preload/loadclass/Android.mk @@ -0,0 +1,9 @@ +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := $(call all-subdir-java-files) +LOCAL_MODULE_TAGS := tests + +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 000000000000..a71b6a8b145e --- /dev/null +++ b/tools/preload/loadclass/LoadClass.java @@ -0,0 +1,84 @@ +/* + * 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 { + long start = System.currentTimeMillis(); + Class.forName(args[0]); + long elapsed = System.currentTimeMillis() - start; + Log.i("LoadClass", "Loaded " + args[0] + " in " + elapsed + + "ms."); + } 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 000000000000..2d87c55ae2ca --- /dev/null +++ b/tools/preload/preload.iml @@ -0,0 +1,14 @@ +<?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" /> + <output-test url="file:///tmp/preload" /> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$" isTestSource="false" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> + diff --git a/tools/preload/preload.ipr b/tools/preload/preload.ipr new file mode 100644 index 000000000000..0c9621c6ed43 --- /dev/null +++ b/tools/preload/preload.ipr @@ -0,0 +1,495 @@ +<?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"> + <value> + <ADDITIONAL_INDENT_OPTIONS fileType="java"> + <option name="INDENT_SIZE" value="4" /> + <option name="CONTINUATION_INDENT_SIZE" value="8" /> + <option name="TAB_SIZE" value="4" /> + <option name="USE_TAB_CHARACTER" value="false" /> + <option name="SMART_TABS" value="false" /> + <option name="LABEL_INDENT_SIZE" value="0" /> + <option name="LABEL_INDENT_ABSOLUTE" value="false" /> + </ADDITIONAL_INDENT_OPTIONS> + <ADDITIONAL_INDENT_OPTIONS fileType="xml"> + <option name="INDENT_SIZE" value="4" /> + <option name="CONTINUATION_INDENT_SIZE" value="8" /> + <option name="TAB_SIZE" value="4" /> + <option name="USE_TAB_CHARACTER" value="false" /> + <option name="SMART_TABS" value="false" /> + <option name="LABEL_INDENT_SIZE" value="0" /> + <option name="LABEL_INDENT_ABSOLUTE" value="false" /> + </ADDITIONAL_INDENT_OPTIONS> + </value> + </option> + <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="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" /> + <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_PROFILE" value="true" /> + <version value="1.0" /> + <profiles> + <profile version="1.0" is_locked="false"> + <option name="myName" value="Project Default" /> + <option name="myLocal" value="false" /> + <inspection_tool class="JavaDoc" enabled="false" level="WARNING" enabled_by_default="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="JavaLangImport" enabled="true" level="WARNING" enabled_by_default="true" /> + <inspection_tool class="OnDemandImport" enabled="true" level="WARNING" enabled_by_default="true" /> + <inspection_tool class="RedundantImport" enabled="true" level="WARNING" enabled_by_default="true" /> + <inspection_tool class="SamePackageImport" enabled="true" level="WARNING" enabled_by_default="true" /> + <inspection_tool class="UnusedImport" enabled="true" level="WARNING" enabled_by_default="true" /> + </profile> + </profiles> + <list size="4"> + <item index="0" class="java.lang.String" itemvalue="WARNING" /> + <item index="1" class="java.lang.String" itemvalue="SERVER PROBLEM" /> + <item index="2" class="java.lang.String" itemvalue="INFO" /> + <item index="3" class="java.lang.String" itemvalue="ERROR" /> + </list> + </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="ProjectDetails"> + <option name="projectName" value="preload" /> + </component> + <component name="ProjectFileVersion" converted="true" /> + <component name="ProjectKey"> + <option name="state" value="project:///Volumes/Android/donut/frameworks/base/tools/preload/preload.ipr" /> + </component> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/preload.iml" filepath="$PROJECT_DIR$/preload.iml" /> + </modules> + </component> + <component name="ProjectRootManager" version="2" languageLevel="JDK_1_5" 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="SvnBranchConfigurationManager"> + <option name="mySupportsUserInfoFilter" 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="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" /> + <mapping directory="/Volumes/Android/donut/frameworks/base" vcs="Git" /> + </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> + diff --git a/tools/preload/sorttable.js b/tools/preload/sorttable.js new file mode 100644 index 000000000000..25bccb2b6b91 --- /dev/null +++ b/tools/preload/sorttable.js @@ -0,0 +1,493 @@ +/* + SortTable + version 2 + 7th April 2007 + Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ + + Instructions: + Download this file + Add <script src="sorttable.js"></script> to your HTML + Add class="sortable" to any table you'd like to make sortable + Click on the headers to sort + + Thanks to many, many people for contributions and suggestions. + Licenced as X11: http://www.kryogenix.org/code/browser/licence.html + This basically means: do what you want with it. +*/ + + +var stIsIE = /*@cc_on!@*/false; + +sorttable = { + init: function() { + // quit if this function has already been called + if (arguments.callee.done) return; + // flag this function so we don't do the same thing twice + arguments.callee.done = true; + // kill the timer + if (_timer) clearInterval(_timer); + + if (!document.createElement || !document.getElementsByTagName) return; + + sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; + + forEach(document.getElementsByTagName('table'), function(table) { + if (table.className.search(/\bsortable\b/) != -1) { + sorttable.makeSortable(table); + } + }); + + }, + + makeSortable: function(table) { + if (table.getElementsByTagName('thead').length == 0) { + // table doesn't have a tHead. Since it should have, create one and + // put the first table row in it. + the = document.createElement('thead'); + the.appendChild(table.rows[0]); + table.insertBefore(the,table.firstChild); + } + // Safari doesn't support table.tHead, sigh + if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; + + if (table.tHead.rows.length != 1) return; // can't cope with two header rows + + // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as + // "total" rows, for example). This is B&R, since what you're supposed + // to do is put them in a tfoot. So, if there are sortbottom rows, + // for backwards compatibility, move them to tfoot (creating it if needed). + sortbottomrows = []; + for (var i=0; i<table.rows.length; i++) { + if (table.rows[i].className.search(/\bsortbottom\b/) != -1) { + sortbottomrows[sortbottomrows.length] = table.rows[i]; + } + } + if (sortbottomrows) { + if (table.tFoot == null) { + // table doesn't have a tfoot. Create one. + tfo = document.createElement('tfoot'); + table.appendChild(tfo); + } + for (var i=0; i<sortbottomrows.length; i++) { + tfo.appendChild(sortbottomrows[i]); + } + delete sortbottomrows; + } + + // work through each column and calculate its type + headrow = table.tHead.rows[0].cells; + for (var i=0; i<headrow.length; i++) { + // manually override the type with a sorttable_type attribute + if (!headrow[i].className.match(/\bsorttable_nosort\b/)) { // skip this col + mtch = headrow[i].className.match(/\bsorttable_([a-z0-9]+)\b/); + if (mtch) { override = mtch[1]; } + if (mtch && typeof sorttable["sort_"+override] == 'function') { + headrow[i].sorttable_sortfunction = sorttable["sort_"+override]; + } else { + headrow[i].sorttable_sortfunction = sorttable.guessType(table,i); + } + // make it clickable to sort + headrow[i].sorttable_columnindex = i; + headrow[i].sorttable_tbody = table.tBodies[0]; + dean_addEvent(headrow[i],"click", function(e) { + + if (this.className.search(/\bsorttable_sorted\b/) != -1) { + // if we're already sorted by this column, just + // reverse the table, which is quicker + sorttable.reverse(this.sorttable_tbody); + this.className = this.className.replace('sorttable_sorted', + 'sorttable_sorted_reverse'); + this.removeChild(document.getElementById('sorttable_sortfwdind')); + sortrevind = document.createElement('span'); + sortrevind.id = "sorttable_sortrevind"; + sortrevind.innerHTML = stIsIE ? ' <font face="webdings">5</font>' : ' ▴'; + this.appendChild(sortrevind); + return; + } + if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { + // if we're already sorted by this column in reverse, just + // re-reverse the table, which is quicker + sorttable.reverse(this.sorttable_tbody); + this.className = this.className.replace('sorttable_sorted_reverse', + 'sorttable_sorted'); + this.removeChild(document.getElementById('sorttable_sortrevind')); + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾'; + this.appendChild(sortfwdind); + return; + } + + // remove sorttable_sorted classes + theadrow = this.parentNode; + forEach(theadrow.childNodes, function(cell) { + if (cell.nodeType == 1) { // an element + cell.className = cell.className.replace('sorttable_sorted_reverse',''); + cell.className = cell.className.replace('sorttable_sorted',''); + } + }); + sortfwdind = document.getElementById('sorttable_sortfwdind'); + if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } + sortrevind = document.getElementById('sorttable_sortrevind'); + if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } + + this.className += ' sorttable_sorted'; + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' <font face="webdings">6</font>' : ' ▾'; + this.appendChild(sortfwdind); + + // build an array to sort. This is a Schwartzian transform thing, + // i.e., we "decorate" each row with the actual sort key, + // sort based on the sort keys, and then put the rows back in order + // which is a lot faster because you only do getInnerText once per row + row_array = []; + col = this.sorttable_columnindex; + rows = this.sorttable_tbody.rows; + for (var j=0; j<rows.length; j++) { + row_array[row_array.length] = [sorttable.getInnerText(rows[j].cells[col]), rows[j]]; + } + /* If you want a stable sort, uncomment the following line */ + //sorttable.shaker_sort(row_array, this.sorttable_sortfunction); + /* and comment out this one */ + row_array.sort(this.sorttable_sortfunction); + + tb = this.sorttable_tbody; + for (var j=0; j<row_array.length; j++) { + tb.appendChild(row_array[j][1]); + } + + delete row_array; + }); + } + } + }, + + guessType: function(table, column) { + // guess the type of a column based on its first non-blank row + sortfn = sorttable.sort_alpha; + for (var i=0; i<table.tBodies[0].rows.length; i++) { + text = sorttable.getInnerText(table.tBodies[0].rows[i].cells[column]); + if (text != '') { + if (text.match(/^-?[£$¤]?[\d,.]+%?$/)) { + return sorttable.sort_numeric; + } + // check for a date: dd/mm/yyyy or dd/mm/yy + // can have / or . or - as separator + // can be mm/dd as well + possdate = text.match(sorttable.DATE_RE) + if (possdate) { + // looks like a date + first = parseInt(possdate[1]); + second = parseInt(possdate[2]); + if (first > 12) { + // definitely dd/mm + return sorttable.sort_ddmm; + } else if (second > 12) { + return sorttable.sort_mmdd; + } else { + // looks like a date, but we can't tell which, so assume + // that it's dd/mm (English imperialism!) and keep looking + sortfn = sorttable.sort_ddmm; + } + } + } + } + return sortfn; + }, + + getInnerText: function(node) { + // gets the text we want to use for sorting for a cell. + // strips leading and trailing whitespace. + // this is *not* a generic getInnerText function; it's special to sorttable. + // for example, you can override the cell text with a customkey attribute. + // it also gets .value for <input> fields. + + hasInputs = (typeof node.getElementsByTagName == 'function') && + node.getElementsByTagName('input').length; + + if (node.getAttribute("sorttable_customkey") != null) { + return node.getAttribute("sorttable_customkey"); + } + else if (typeof node.textContent != 'undefined' && !hasInputs) { + return node.textContent.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.innerText != 'undefined' && !hasInputs) { + return node.innerText.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.text != 'undefined' && !hasInputs) { + return node.text.replace(/^\s+|\s+$/g, ''); + } + else { + switch (node.nodeType) { + case 3: + if (node.nodeName.toLowerCase() == 'input') { + return node.value.replace(/^\s+|\s+$/g, ''); + } + case 4: + return node.nodeValue.replace(/^\s+|\s+$/g, ''); + break; + case 1: + case 11: + var innerText = ''; + for (var i = 0; i < node.childNodes.length; i++) { + innerText += sorttable.getInnerText(node.childNodes[i]); + } + return innerText.replace(/^\s+|\s+$/g, ''); + break; + default: + return ''; + } + } + }, + + reverse: function(tbody) { + // reverse the rows in a tbody + newrows = []; + for (var i=0; i<tbody.rows.length; i++) { + newrows[newrows.length] = tbody.rows[i]; + } + for (var i=newrows.length-1; i>=0; i--) { + tbody.appendChild(newrows[i]); + } + delete newrows; + }, + + /* sort functions + each sort function takes two parameters, a and b + you are comparing a[0] and b[0] */ + sort_numeric: function(a,b) { + aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); + if (isNaN(aa)) aa = 0; + bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); + if (isNaN(bb)) bb = 0; + return aa-bb; + }, + sort_alpha: function(a,b) { + if (a[0]==b[0]) return 0; + if (a[0]<b[0]) return -1; + return 1; + }, + sort_ddmm: function(a,b) { + mtch = a[0].match(sorttable.DATE_RE); + y = mtch[3]; m = mtch[2]; d = mtch[1]; + if (m.length == 1) m = '0'+m; + if (d.length == 1) d = '0'+d; + dt1 = y+m+d; + mtch = b[0].match(sorttable.DATE_RE); + y = mtch[3]; m = mtch[2]; d = mtch[1]; + if (m.length == 1) m = '0'+m; + if (d.length == 1) d = '0'+d; + dt2 = y+m+d; + if (dt1==dt2) return 0; + if (dt1<dt2) return -1; + return 1; + }, + sort_mmdd: function(a,b) { + mtch = a[0].match(sorttable.DATE_RE); + y = mtch[3]; d = mtch[2]; m = mtch[1]; + if (m.length == 1) m = '0'+m; + if (d.length == 1) d = '0'+d; + dt1 = y+m+d; + mtch = b[0].match(sorttable.DATE_RE); + y = mtch[3]; d = mtch[2]; m = mtch[1]; + if (m.length == 1) m = '0'+m; + if (d.length == 1) d = '0'+d; + dt2 = y+m+d; + if (dt1==dt2) return 0; + if (dt1<dt2) return -1; + return 1; + }, + + shaker_sort: function(list, comp_func) { + // A stable sort function to allow multi-level sorting of data + // see: http://en.wikipedia.org/wiki/Cocktail_sort + // thanks to Joseph Nahmias + var b = 0; + var t = list.length - 1; + var swap = true; + + while(swap) { + swap = false; + for(var i = b; i < t; ++i) { + if ( comp_func(list[i], list[i+1]) > 0 ) { + var q = list[i]; list[i] = list[i+1]; list[i+1] = q; + swap = true; + } + } // for + t--; + + if (!swap) break; + + for(var i = t; i > b; --i) { + if ( comp_func(list[i], list[i-1]) < 0 ) { + var q = list[i]; list[i] = list[i-1]; list[i-1] = q; + swap = true; + } + } // for + b++; + + } // while(swap) + } +} + +/* ****************************************************************** + Supporting functions: bundled here to avoid depending on a library + ****************************************************************** */ + +// Dean Edwards/Matthias Miller/John Resig + +/* for Mozilla/Opera9 */ +if (document.addEventListener) { + document.addEventListener("DOMContentLoaded", sorttable.init, false); +} + +/* for Internet Explorer */ +/*@cc_on @*/ +/*@if (@_win32) + document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>"); + var script = document.getElementById("__ie_onload"); + script.onreadystatechange = function() { + if (this.readyState == "complete") { + sorttable.init(); // call the onload handler + } + }; +/*@end @*/ + +/* for Safari */ +if (/WebKit/i.test(navigator.userAgent)) { // sniff + var _timer = setInterval(function() { + if (/loaded|complete/.test(document.readyState)) { + sorttable.init(); // call the onload handler + } + }, 10); +} + +/* for other browsers */ +window.onload = sorttable.init; + +// written by Dean Edwards, 2005 +// with input from Tino Zijdel, Matthias Miller, Diego Perini + +// http://dean.edwards.name/weblog/2005/10/add-event/ + +function dean_addEvent(element, type, handler) { + if (element.addEventListener) { + element.addEventListener(type, handler, false); + } else { + // assign each event handler a unique ID + if (!handler.$$guid) handler.$$guid = dean_addEvent.guid++; + // create a hash table of event types for the element + if (!element.events) element.events = {}; + // create a hash table of event handlers for each element/event pair + var handlers = element.events[type]; + if (!handlers) { + handlers = element.events[type] = {}; + // store the existing event handler (if there is one) + if (element["on" + type]) { + handlers[0] = element["on" + type]; + } + } + // store the event handler in the hash table + handlers[handler.$$guid] = handler; + // assign a global event handler to do all the work + element["on" + type] = handleEvent; + } +}; +// a counter used to create unique IDs +dean_addEvent.guid = 1; + +function removeEvent(element, type, handler) { + if (element.removeEventListener) { + element.removeEventListener(type, handler, false); + } else { + // delete the event handler from the hash table + if (element.events && element.events[type]) { + delete element.events[type][handler.$$guid]; + } + } +}; + +function handleEvent(event) { + var returnValue = true; + // grab the event object (IE uses a global event object) + event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event); + // get a reference to the hash table of event handlers + var handlers = this.events[event.type]; + // execute each event handler + for (var i in handlers) { + this.$$handleEvent = handlers[i]; + if (this.$$handleEvent(event) === false) { + returnValue = false; + } + } + return returnValue; +}; + +function fixEvent(event) { + // add W3C standard event methods + event.preventDefault = fixEvent.preventDefault; + event.stopPropagation = fixEvent.stopPropagation; + return event; +}; +fixEvent.preventDefault = function() { + this.returnValue = false; +}; +fixEvent.stopPropagation = function() { + this.cancelBubble = true; +} + +// Dean's forEach: http://dean.edwards.name/base/forEach.js +/* + forEach, version 1.0 + Copyright 2006, Dean Edwards + License: http://www.opensource.org/licenses/mit-license.php +*/ + +// array-like enumeration +if (!Array.forEach) { // mozilla already supports this + Array.forEach = function(array, block, context) { + for (var i = 0; i < array.length; i++) { + block.call(context, array[i], i, array); + } + }; +} + +// generic enumeration +Function.prototype.forEach = function(object, block, context) { + for (var key in object) { + if (typeof this.prototype[key] == "undefined") { + block.call(context, object[key], key, object); + } + } +}; + +// character enumeration +String.forEach = function(string, block, context) { + Array.forEach(string.split(""), function(chr, index) { + block.call(context, chr, index, string); + }); +}; + +// globally resolve forEach enumeration +var forEach = function(object, block, context) { + if (object) { + var resolve = Object; // default + if (object instanceof Function) { + // functions have a "length" property + resolve = Function; + } else if (object.forEach instanceof Function) { + // the object implements a custom forEach method so use that + object.forEach(block, context); + return; + } else if (typeof object == "string") { + // the object is a string + resolve = String; + } else if (typeof object.length == "number") { + // the object is array-like + resolve = Array; + } + resolve.forEach(object, block, context); + } +}; + diff --git a/tools/validatekeymaps/Android.mk b/tools/validatekeymaps/Android.mk new file mode 100644 index 000000000000..9af721de97db --- /dev/null +++ b/tools/validatekeymaps/Android.mk @@ -0,0 +1,33 @@ +# +# Copyright 2010 The Android Open Source Project +# +# Keymap validation tool. +# + +# This tool is prebuilt if we're doing an app-only build. +ifeq ($(TARGET_BUILD_APPS),) + +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES := \ + Main.cpp + +LOCAL_CFLAGS := -Wall -Werror + +LOCAL_STATIC_LIBRARIES := \ + libinput \ + libutils \ + libcutils \ + liblog + +ifeq ($(HOST_OS),linux) +LOCAL_LDLIBS += -ldl -lpthread +endif + +LOCAL_MODULE := validatekeymaps +LOCAL_MODULE_TAGS := optional + +include $(BUILD_HOST_EXECUTABLE) + +endif # TARGET_BUILD_APPS diff --git a/tools/validatekeymaps/Main.cpp b/tools/validatekeymaps/Main.cpp new file mode 100644 index 000000000000..5b45c551c930 --- /dev/null +++ b/tools/validatekeymaps/Main.cpp @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <input/KeyCharacterMap.h> +#include <input/KeyLayoutMap.h> +#include <input/VirtualKeyMap.h> +#include <utils/PropertyMap.h> +#include <utils/String8.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +using namespace android; + +static const char* gProgName = "validatekeymaps"; + +enum FileType { + FILETYPE_UNKNOWN, + FILETYPE_KEYLAYOUT, + FILETYPE_KEYCHARACTERMAP, + FILETYPE_VIRTUALKEYDEFINITION, + FILETYPE_INPUTDEVICECONFIGURATION, +}; + + +static void usage() { + fprintf(stderr, "Keymap Validation Tool\n\n"); + fprintf(stderr, "Usage:\n"); + fprintf(stderr, + " %s [*.kl] [*.kcm] [*.idc] [virtualkeys.*] [...]\n" + " Validates the specified key layouts, key character maps, \n" + " input device configurations, or virtual key definitions.\n\n", + gProgName); +} + +static FileType getFileType(const char* filename) { + const char *extension = strrchr(filename, '.'); + if (extension) { + if (strcmp(extension, ".kl") == 0) { + return FILETYPE_KEYLAYOUT; + } + if (strcmp(extension, ".kcm") == 0) { + return FILETYPE_KEYCHARACTERMAP; + } + if (strcmp(extension, ".idc") == 0) { + return FILETYPE_INPUTDEVICECONFIGURATION; + } + } + + if (strstr(filename, "virtualkeys.")) { + return FILETYPE_VIRTUALKEYDEFINITION; + } + + return FILETYPE_UNKNOWN; +} + +static bool validateFile(const char* filename) { + fprintf(stdout, "Validating file '%s'...\n", filename); + + FileType fileType = getFileType(filename); + switch (fileType) { + case FILETYPE_UNKNOWN: + fprintf(stderr, "Supported file types: *.kl, *.kcm, virtualkeys.*\n\n"); + return false; + + case FILETYPE_KEYLAYOUT: { + sp<KeyLayoutMap> map; + status_t status = KeyLayoutMap::load(String8(filename), &map); + if (status) { + fprintf(stderr, "Error %d parsing key layout file.\n\n", status); + return false; + } + break; + } + + case FILETYPE_KEYCHARACTERMAP: { + sp<KeyCharacterMap> map; + status_t status = KeyCharacterMap::load(String8(filename), + KeyCharacterMap::FORMAT_ANY, &map); + if (status) { + fprintf(stderr, "Error %d parsing key character map file.\n\n", status); + return false; + } + break; + } + + case FILETYPE_INPUTDEVICECONFIGURATION: { + PropertyMap* map; + status_t status = PropertyMap::load(String8(filename), &map); + if (status) { + fprintf(stderr, "Error %d parsing input device configuration file.\n\n", status); + return false; + } + delete map; + break; + } + + case FILETYPE_VIRTUALKEYDEFINITION: { + VirtualKeyMap* map; + status_t status = VirtualKeyMap::load(String8(filename), &map); + if (status) { + fprintf(stderr, "Error %d parsing virtual key definition file.\n\n", status); + return false; + } + delete map; + break; + } + } + + fputs("No errors.\n\n", stdout); + return true; +} + +int main(int argc, const char** argv) { + if (argc < 2) { + usage(); + return 1; + } + + int result = 0; + for (int i = 1; i < argc; i++) { + if (!validateFile(argv[i])) { + result = 1; + } + } + + if (result) { + fputs("Failed!\n", stderr); + } else { + fputs("Success.\n", stdout); + } + return result; +} diff --git a/tools/velocityplot/velocityplot.py b/tools/velocityplot/velocityplot.py new file mode 100755 index 000000000000..421bed43957c --- /dev/null +++ b/tools/velocityplot/velocityplot.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python2.6 +# +# Copyright (C) 2011 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# +# Plots debug log output from VelocityTracker. +# Enable DEBUG_VELOCITY to print the output. +# +# This code supports side-by-side comparison of two algorithms. +# The old algorithm should be modified to emit debug log messages containing +# the word "OLD". +# + +import numpy as np +import matplotlib.pyplot as plot +import subprocess +import re +import fcntl +import os +import errno +import bisect +from datetime import datetime, timedelta + +# Parameters. +timespan = 15 # seconds total span shown +scrolljump = 5 # seconds jump when scrolling +timeticks = 1 # seconds between each time tick + +# Non-blocking stream wrapper. +class NonBlockingStream: + def __init__(self, stream): + fcntl.fcntl(stream, fcntl.F_SETFL, os.O_NONBLOCK) + self.stream = stream + self.buffer = '' + self.pos = 0 + + def readline(self): + while True: + index = self.buffer.find('\n', self.pos) + if index != -1: + result = self.buffer[self.pos:index] + self.pos = index + 1 + return result + + self.buffer = self.buffer[self.pos:] + self.pos = 0 + try: + chunk = os.read(self.stream.fileno(), 4096) + except OSError, e: + if e.errno == errno.EAGAIN: + return None + raise e + if len(chunk) == 0: + if len(self.buffer) == 0: + raise(EOFError) + else: + result = self.buffer + self.buffer = '' + self.pos = 0 + return result + self.buffer += chunk + +# Plotter +class Plotter: + def __init__(self, adbout): + self.adbout = adbout + + self.fig = plot.figure(1) + self.fig.suptitle('Velocity Tracker', fontsize=12) + self.fig.set_dpi(96) + self.fig.set_size_inches(16, 12, forward=True) + + self.velocity_x = self._make_timeseries() + self.velocity_y = self._make_timeseries() + self.velocity_magnitude = self._make_timeseries() + self.velocity_axes = self._add_timeseries_axes( + 1, 'Velocity', 'px/s', [-5000, 5000], + yticks=range(-5000, 5000, 1000)) + self.velocity_line_x = self._add_timeseries_line( + self.velocity_axes, 'vx', 'red') + self.velocity_line_y = self._add_timeseries_line( + self.velocity_axes, 'vy', 'green') + self.velocity_line_magnitude = self._add_timeseries_line( + self.velocity_axes, 'magnitude', 'blue') + self._add_timeseries_legend(self.velocity_axes) + + shared_axis = self.velocity_axes + + self.old_velocity_x = self._make_timeseries() + self.old_velocity_y = self._make_timeseries() + self.old_velocity_magnitude = self._make_timeseries() + self.old_velocity_axes = self._add_timeseries_axes( + 2, 'Old Algorithm Velocity', 'px/s', [-5000, 5000], + sharex=shared_axis, + yticks=range(-5000, 5000, 1000)) + self.old_velocity_line_x = self._add_timeseries_line( + self.old_velocity_axes, 'vx', 'red') + self.old_velocity_line_y = self._add_timeseries_line( + self.old_velocity_axes, 'vy', 'green') + self.old_velocity_line_magnitude = self._add_timeseries_line( + self.old_velocity_axes, 'magnitude', 'blue') + self._add_timeseries_legend(self.old_velocity_axes) + + self.timer = self.fig.canvas.new_timer(interval=100) + self.timer.add_callback(lambda: self.update()) + self.timer.start() + + self.timebase = None + self._reset_parse_state() + + # Initialize a time series. + def _make_timeseries(self): + return [[], []] + + # Add a subplot to the figure for a time series. + def _add_timeseries_axes(self, index, title, ylabel, ylim, yticks, sharex=None): + num_graphs = 2 + height = 0.9 / num_graphs + top = 0.95 - height * index + axes = self.fig.add_axes([0.1, top, 0.8, height], + xscale='linear', + xlim=[0, timespan], + ylabel=ylabel, + yscale='linear', + ylim=ylim, + sharex=sharex) + axes.text(0.02, 0.02, title, transform=axes.transAxes, fontsize=10, fontweight='bold') + axes.set_xlabel('time (s)', fontsize=10, fontweight='bold') + axes.set_ylabel(ylabel, fontsize=10, fontweight='bold') + axes.set_xticks(range(0, timespan + 1, timeticks)) + axes.set_yticks(yticks) + axes.grid(True) + + for label in axes.get_xticklabels(): + label.set_fontsize(9) + for label in axes.get_yticklabels(): + label.set_fontsize(9) + + return axes + + # Add a line to the axes for a time series. + def _add_timeseries_line(self, axes, label, color, linewidth=1): + return axes.plot([], label=label, color=color, linewidth=linewidth)[0] + + # Add a legend to a time series. + def _add_timeseries_legend(self, axes): + axes.legend( + loc='upper left', + bbox_to_anchor=(1.01, 1), + borderpad=0.1, + borderaxespad=0.1, + prop={'size': 10}) + + # Resets the parse state. + def _reset_parse_state(self): + self.parse_velocity_x = None + self.parse_velocity_y = None + self.parse_velocity_magnitude = None + self.parse_old_velocity_x = None + self.parse_old_velocity_y = None + self.parse_old_velocity_magnitude = None + + # Update samples. + def update(self): + timeindex = 0 + while True: + try: + line = self.adbout.readline() + except EOFError: + plot.close() + return + if line is None: + break + print line + + try: + timestamp = self._parse_timestamp(line) + except ValueError, e: + continue + if self.timebase is None: + self.timebase = timestamp + delta = timestamp - self.timebase + timeindex = delta.seconds + delta.microseconds * 0.000001 + + if line.find(': position') != -1: + self.parse_velocity_x = self._get_following_number(line, 'vx=') + self.parse_velocity_y = self._get_following_number(line, 'vy=') + self.parse_velocity_magnitude = self._get_following_number(line, 'speed=') + self._append(self.velocity_x, timeindex, self.parse_velocity_x) + self._append(self.velocity_y, timeindex, self.parse_velocity_y) + self._append(self.velocity_magnitude, timeindex, self.parse_velocity_magnitude) + + if line.find(': OLD') != -1: + self.parse_old_velocity_x = self._get_following_number(line, 'vx=') + self.parse_old_velocity_y = self._get_following_number(line, 'vy=') + self.parse_old_velocity_magnitude = self._get_following_number(line, 'speed=') + self._append(self.old_velocity_x, timeindex, self.parse_old_velocity_x) + self._append(self.old_velocity_y, timeindex, self.parse_old_velocity_y) + self._append(self.old_velocity_magnitude, timeindex, self.parse_old_velocity_magnitude) + + # Scroll the plots. + if timeindex > timespan: + bottom = int(timeindex) - timespan + scrolljump + self.timebase += timedelta(seconds=bottom) + self._scroll(self.velocity_x, bottom) + self._scroll(self.velocity_y, bottom) + self._scroll(self.velocity_magnitude, bottom) + self._scroll(self.old_velocity_x, bottom) + self._scroll(self.old_velocity_y, bottom) + self._scroll(self.old_velocity_magnitude, bottom) + + # Redraw the plots. + self.velocity_line_x.set_data(self.velocity_x) + self.velocity_line_y.set_data(self.velocity_y) + self.velocity_line_magnitude.set_data(self.velocity_magnitude) + self.old_velocity_line_x.set_data(self.old_velocity_x) + self.old_velocity_line_y.set_data(self.old_velocity_y) + self.old_velocity_line_magnitude.set_data(self.old_velocity_magnitude) + + self.fig.canvas.draw_idle() + + # Scroll a time series. + def _scroll(self, timeseries, bottom): + bottom_index = bisect.bisect_left(timeseries[0], bottom) + del timeseries[0][:bottom_index] + del timeseries[1][:bottom_index] + for i, timeindex in enumerate(timeseries[0]): + timeseries[0][i] = timeindex - bottom + + # Extract a word following the specified prefix. + def _get_following_word(self, line, prefix): + prefix_index = line.find(prefix) + if prefix_index == -1: + return None + start_index = prefix_index + len(prefix) + delim_index = line.find(',', start_index) + if delim_index == -1: + return line[start_index:] + else: + return line[start_index:delim_index] + + # Extract a number following the specified prefix. + def _get_following_number(self, line, prefix): + word = self._get_following_word(line, prefix) + if word is None: + return None + return float(word) + + # Add a value to a time series. + def _append(self, timeseries, timeindex, number): + timeseries[0].append(timeindex) + timeseries[1].append(number) + + # Parse the logcat timestamp. + # Timestamp has the form '01-21 20:42:42.930' + def _parse_timestamp(self, line): + return datetime.strptime(line[0:18], '%m-%d %H:%M:%S.%f') + +# Notice +print "Velocity Tracker plotting tool" +print "-----------------------------------------\n" +print "Please enable debug logging and recompile the code." + +# Start adb. +print "Starting adb logcat.\n" + +adb = subprocess.Popen(['adb', 'logcat', '-s', '-v', 'time', 'Input:*', 'VelocityTracker:*'], + stdout=subprocess.PIPE) +adbout = NonBlockingStream(adb.stdout) + +# Prepare plotter. +plotter = Plotter(adbout) +plotter.update() + +# Main loop. +plot.show() |