diff options
Diffstat (limited to 'tools')
103 files changed, 5458 insertions, 649 deletions
diff --git a/tools/aapt/AaptAssets.cpp b/tools/aapt/AaptAssets.cpp index 2849c84..2d35129 100644 --- a/tools/aapt/AaptAssets.cpp +++ b/tools/aapt/AaptAssets.cpp @@ -1140,9 +1140,10 @@ bail: ssize_t AaptAssets::slurpFullTree(Bundle* bundle, const String8& srcDir, const AaptGroupEntry& kind, const String8& resType, - sp<FilePathStore>& fullResPaths) + sp<FilePathStore>& fullResPaths, + const bool overwrite) { - ssize_t res = AaptDir::slurpFullTree(bundle, srcDir, kind, resType, fullResPaths); + ssize_t res = AaptDir::slurpFullTree(bundle, srcDir, kind, resType, fullResPaths, overwrite); if (res > 0) { mGroupEntries.add(kind); } diff --git a/tools/aapt/AaptAssets.h b/tools/aapt/AaptAssets.h index d809c5b..7ae5368 100644 --- a/tools/aapt/AaptAssets.h +++ b/tools/aapt/AaptAssets.h @@ -591,7 +591,8 @@ private: const String8& srcDir, const AaptGroupEntry& kind, const String8& resType, - sp<FilePathStore>& fullResPaths); + sp<FilePathStore>& fullResPaths, + const bool overwrite=false); ssize_t slurpResourceTree(Bundle* bundle, const String8& srcDir); ssize_t slurpResourceZip(Bundle* bundle, const char* filename); diff --git a/tools/aapt/AaptConfig.cpp b/tools/aapt/AaptConfig.cpp index 32a0cd3..e88c27a 100644 --- a/tools/aapt/AaptConfig.cpp +++ b/tools/aapt/AaptConfig.cpp @@ -21,6 +21,7 @@ #include "AaptAssets.h" #include "AaptUtil.h" #include "ResourceFilter.h" +#include "SdkConstants.h" using android::String8; using android::Vector; @@ -240,7 +241,9 @@ void applyVersionForCompatibility(ConfigDescription* config) { } uint16_t minSdk = 0; - if (config->smallestScreenWidthDp != ResTable_config::SCREENWIDTH_ANY + if (config->density == ResTable_config::DENSITY_ANY) { + minSdk = SDK_LOLLIPOP; + } else if (config->smallestScreenWidthDp != ResTable_config::SCREENWIDTH_ANY || config->screenWidthDp != ResTable_config::SCREENWIDTH_ANY || config->screenHeightDp != ResTable_config::SCREENHEIGHT_ANY) { minSdk = SDK_HONEYCOMB_MR2; @@ -255,8 +258,6 @@ void applyVersionForCompatibility(ConfigDescription* config) { != ResTable_config::SCREENLONG_ANY || config->density != ResTable_config::DENSITY_DEFAULT) { minSdk = SDK_DONUT; - } else if ((config->density == ResTable_config::DENSITY_ANY)) { - minSdk = SDK_L; } if (minSdk > config->sdkVersion) { @@ -794,4 +795,23 @@ bool isSameExcept(const ResTable_config& a, const ResTable_config& b, int axisMa return a.diff(b) == axisMask; } +bool isDensityOnly(const ResTable_config& config) { + if (config.density == ResTable_config::DENSITY_NONE) { + return false; + } + + if (config.density == ResTable_config::DENSITY_ANY) { + if (config.sdkVersion != SDK_LOLLIPOP) { + // Someone modified the sdkVersion from the default, this is not safe to assume. + return false; + } + } else if (config.sdkVersion != SDK_DONUT) { + return false; + } + + const uint32_t mask = ResTable_config::CONFIG_DENSITY | ResTable_config::CONFIG_VERSION; + const ConfigDescription nullConfig; + return (nullConfig.diff(config) & ~mask) == 0; +} + } // namespace AaptConfig diff --git a/tools/aapt/AaptConfig.h b/tools/aapt/AaptConfig.h index 2963539..f73a508 100644 --- a/tools/aapt/AaptConfig.h +++ b/tools/aapt/AaptConfig.h @@ -80,6 +80,12 @@ android::String8 getVersion(const android::ResTable_config& config); */ bool isSameExcept(const android::ResTable_config& a, const android::ResTable_config& b, int configMask); +/** + * Returns true if the configuration only has the density specified. In the case + * of 'anydpi', the version is ignored. + */ +bool isDensityOnly(const android::ResTable_config& config); + } // namespace AaptConfig #endif // __AAPT_CONFIG_H diff --git a/tools/aapt/AaptUtil.h b/tools/aapt/AaptUtil.h index 47a704a..89e1ee8 100644 --- a/tools/aapt/AaptUtil.h +++ b/tools/aapt/AaptUtil.h @@ -14,9 +14,11 @@ * limitations under the License. */ -#ifndef __AAPT_UTIL_H -#define __AAPT_UTIL_H +#ifndef H_AAPT_UTIL +#define H_AAPT_UTIL +#include <utils/KeyedVector.h> +#include <utils/SortedVector.h> #include <utils/String8.h> #include <utils/Vector.h> @@ -25,6 +27,38 @@ namespace AaptUtil { android::Vector<android::String8> split(const android::String8& str, const char sep); android::Vector<android::String8> splitAndLowerCase(const android::String8& str, const char sep); +template <typename KEY, typename VALUE> +void appendValue(android::KeyedVector<KEY, android::Vector<VALUE> >& keyedVector, + const KEY& key, const VALUE& value); + +template <typename KEY, typename VALUE> +void appendValue(android::KeyedVector<KEY, android::SortedVector<VALUE> >& keyedVector, + const KEY& key, const VALUE& value); + +// +// Implementations +// + +template <typename KEY, typename VALUE> +void appendValue(android::KeyedVector<KEY, android::Vector<VALUE> >& keyedVector, + const KEY& key, const VALUE& value) { + ssize_t idx = keyedVector.indexOfKey(key); + if (idx < 0) { + idx = keyedVector.add(key, android::Vector<VALUE>()); + } + keyedVector.editValueAt(idx).add(value); +} + +template <typename KEY, typename VALUE> +void appendValue(android::KeyedVector<KEY, android::SortedVector<VALUE> >& keyedVector, + const KEY& key, const VALUE& value) { + ssize_t idx = keyedVector.indexOfKey(key); + if (idx < 0) { + idx = keyedVector.add(key, android::SortedVector<VALUE>()); + } + keyedVector.editValueAt(idx).add(value); +} + } // namespace AaptUtil -#endif // __AAPT_UTIL_H +#endif // H_AAPT_UTIL diff --git a/tools/aapt/Android.mk b/tools/aapt/Android.mk index ba1411e..c5495a5 100644 --- a/tools/aapt/Android.mk +++ b/tools/aapt/Android.mk @@ -33,19 +33,19 @@ aaptSources := \ Command.cpp \ CrunchCache.cpp \ FileFinder.cpp \ + Images.cpp \ Package.cpp \ - StringPool.cpp \ - XMLNode.cpp \ + pseudolocalize.cpp \ + Resource.cpp \ ResourceFilter.cpp \ ResourceIdCache.cpp \ ResourceTable.cpp \ - Images.cpp \ - Resource.cpp \ - pseudolocalize.cpp \ SourcePos.cpp \ + StringPool.cpp \ WorkQueue.cpp \ + XMLNode.cpp \ ZipEntry.cpp \ - ZipFile.cpp \ + ZipFile.cpp aaptTests := \ tests/AaptConfig_test.cpp \ @@ -88,16 +88,13 @@ endif include $(CLEAR_VARS) LOCAL_MODULE := libaapt - -LOCAL_SRC_FILES := $(aaptSources) -LOCAL_C_INCLUDES += $(aaptCIncludes) - -LOCAL_CFLAGS += -Wno-format-y2k -LOCAL_CFLAGS += -DSTATIC_ANDROIDFW_FOR_TOOLS -LOCAL_CFLAGS += $(aaptCFlags) +LOCAL_CFLAGS += -Wno-format-y2k -DSTATIC_ANDROIDFW_FOR_TOOLS $(aaptCFlags) +LOCAL_CPPFLAGS += $(aaptCppFlags) ifeq (darwin,$(HOST_OS)) LOCAL_CFLAGS += -D_DARWIN_UNLIMITED_STREAMS endif +LOCAL_C_INCLUDES += $(aaptCIncludes) +LOCAL_SRC_FILES := $(aaptSources) include $(BUILD_HOST_STATIC_LIBRARY) @@ -108,15 +105,11 @@ include $(BUILD_HOST_STATIC_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := aapt - -LOCAL_SRC_FILES := $(aaptMain) - -LOCAL_STATIC_LIBRARIES += \ - libaapt \ - $(aaptHostStaticLibs) - -LOCAL_LDLIBS += $(aaptHostLdLibs) LOCAL_CFLAGS += $(aaptCFlags) +LOCAL_CPPFLAGS += $(aaptCppFlags) +LOCAL_LDLIBS += $(aaptHostLdLibs) +LOCAL_SRC_FILES := $(aaptMain) +LOCAL_STATIC_LIBRARIES += libaapt $(aaptHostStaticLibs) include $(BUILD_HOST_EXECUTABLE) @@ -125,18 +118,15 @@ include $(BUILD_HOST_EXECUTABLE) # Build the host tests: libaapt_tests # ========================================================== include $(CLEAR_VARS) +LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk LOCAL_MODULE := libaapt_tests - +LOCAL_CFLAGS += $(aaptCFlags) +LOCAL_CPPFLAGS += $(aaptCppFlags) +LOCAL_LDLIBS += $(aaptHostLdLibs) LOCAL_SRC_FILES += $(aaptTests) LOCAL_C_INCLUDES += $(LOCAL_PATH) - -LOCAL_STATIC_LIBRARIES += \ - libaapt \ - $(aaptHostStaticLibs) - -LOCAL_LDLIBS += $(aaptHostLdLibs) -LOCAL_CFLAGS += $(aaptCFlags) +LOCAL_STATIC_LIBRARIES += libaapt $(aaptHostStaticLibs) include $(BUILD_HOST_NATIVE_TEST) @@ -148,11 +138,9 @@ ifneq ($(SDK_ONLY),true) include $(CLEAR_VARS) LOCAL_MODULE := aapt - +LOCAL_CFLAGS += $(aaptCFlags) LOCAL_SRC_FILES := $(aaptSources) $(aaptMain) -LOCAL_C_INCLUDES += \ - $(aaptCIncludes) \ - +LOCAL_C_INCLUDES += $(aaptCIncludes) LOCAL_SHARED_LIBRARIES := \ libandroidfw \ libutils \ @@ -160,13 +148,9 @@ LOCAL_SHARED_LIBRARIES := \ libpng \ liblog \ libz - LOCAL_STATIC_LIBRARIES := \ libexpat_static -LOCAL_CFLAGS += $(aaptCFlags) -LOCAL_CPPFLAGS += -Wno-non-virtual-dtor - include $(BUILD_EXECUTABLE) endif # Not SDK_ONLY diff --git a/tools/aapt/Bundle.h b/tools/aapt/Bundle.h index cb34448..e7cde74 100644 --- a/tools/aapt/Bundle.h +++ b/tools/aapt/Bundle.h @@ -14,18 +14,7 @@ #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, - SDK_L = 21, -}; +#include "SdkConstants.h" /* * Things we can do. @@ -190,6 +179,8 @@ public: void setVersionName(const char* val) { mVersionName = val; } bool getReplaceVersion() { return mReplaceVersion; } void setReplaceVersion(bool val) { mReplaceVersion = val; } + const android::String8& getRevisionCode() { return mRevisionCode; } + void setRevisionCode(const char* val) { mRevisionCode = android::String8(val); } const char* getCustomPackage() const { return mCustomPackage; } void setCustomPackage(const char* val) { mCustomPackage = val; } const char* getExtraPackages() const { return mExtraPackages; } @@ -308,6 +299,7 @@ private: android::String8 mFeatureOfPackage; android::String8 mFeatureAfterPackage; + android::String8 mRevisionCode; const char* mManifestMinSdkVersion; const char* mMinSdkVersion; const char* mTargetSdkVersion; diff --git a/tools/aapt/CacheUpdater.h b/tools/aapt/CacheUpdater.h index cacab03..fade53a 100644 --- a/tools/aapt/CacheUpdater.h +++ b/tools/aapt/CacheUpdater.h @@ -30,6 +30,8 @@ using namespace android; */ class CacheUpdater { public: + virtual ~CacheUpdater() {} + // Make sure all the directories along this path exist virtual void ensureDirectoriesExist(String8 path) = 0; @@ -38,8 +40,6 @@ public: // Process an image from source out to dest virtual void processImage(String8 source, String8 dest) = 0; - - virtual ~CacheUpdater() {} private: }; diff --git a/tools/aapt/Command.cpp b/tools/aapt/Command.cpp index 70044f2..f4581d0 100644 --- a/tools/aapt/Command.cpp +++ b/tools/aapt/Command.cpp @@ -308,6 +308,7 @@ enum { PUBLIC_KEY_ATTR = 0x010103a6, CATEGORY_ATTR = 0x010103e8, BANNER_ATTR = 0x10103f2, + ISGAME_ATTR = 0x10103f4, }; String8 getComponentName(String8 &pkgName, String8 &componentName) { @@ -516,12 +517,10 @@ static void printFeatureGroup(const FeatureGroup& grp, const size_t numFeatures = grp.features.size(); for (size_t i = 0; i < numFeatures; i++) { - if (!grp.features[i]) { - continue; - } + const bool required = grp.features[i]; const String8& featureName = grp.features.keyAt(i); - printf(" uses-feature: name='%s'\n", + printf(" uses-feature%s: name='%s'\n", (required ? "" : "-not-required"), ResTable::normalizeForOutput(featureName.string()).string()); } @@ -1125,13 +1124,35 @@ int doDump(Bundle* bundle) error.string()); goto bail; } + + String8 banner = AaptXml::getResolvedAttribute(res, tree, BANNER_ATTR, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:banner' attribute: %s\n", + error.string()); + goto bail; + } printf("application: label='%s' ", ResTable::normalizeForOutput(label.string()).string()); - printf("icon='%s'\n", ResTable::normalizeForOutput(icon.string()).string()); + printf("icon='%s'", ResTable::normalizeForOutput(icon.string()).string()); + if (banner != "") { + printf(" banner='%s'", ResTable::normalizeForOutput(banner.string()).string()); + } + printf("\n"); if (testOnly != 0) { printf("testOnly='%d'\n", testOnly); } + int32_t isGame = AaptXml::getResolvedIntegerAttribute(res, tree, + ISGAME_ATTR, 0, &error); + if (error != "") { + fprintf(stderr, "ERROR getting 'android:isGame' attribute: %s\n", + error.string()); + goto bail; + } + if (isGame != 0) { + printf("application-isGame\n"); + } + int32_t debuggable = AaptXml::getResolvedIntegerAttribute(res, tree, DEBUGGABLE_ATTR, 0, &error); if (error != "") { @@ -1819,7 +1840,7 @@ int doDump(Bundle* bundle) } } - if (!grp.features.isEmpty()) { + if (!grp.features.isEmpty()) { printFeatureGroup(grp); } } diff --git a/tools/aapt/ConfigDescription.h b/tools/aapt/ConfigDescription.h index 779c423..4f999a2 100644 --- a/tools/aapt/ConfigDescription.h +++ b/tools/aapt/ConfigDescription.h @@ -28,10 +28,12 @@ struct ConfigDescription : public android::ResTable_config { memset(this, 0, sizeof(*this)); size = sizeof(android::ResTable_config); } + ConfigDescription(const android::ResTable_config&o) { *static_cast<android::ResTable_config*>(this) = o; size = sizeof(android::ResTable_config); } + ConfigDescription(const ConfigDescription&o) { *static_cast<android::ResTable_config*>(this) = o; } @@ -41,6 +43,7 @@ struct ConfigDescription : public android::ResTable_config { size = sizeof(android::ResTable_config); return *this; } + ConfigDescription& operator=(const ConfigDescription& o) { *static_cast<android::ResTable_config*>(this) = o; return *this; diff --git a/tools/aapt/Main.cpp b/tools/aapt/Main.cpp index 4ce4b2c..8b416aa 100644 --- a/tools/aapt/Main.cpp +++ b/tools/aapt/Main.cpp @@ -11,9 +11,9 @@ #include <utils/List.h> #include <utils/Errors.h> -#include <stdlib.h> +#include <cstdlib> #include <getopt.h> -#include <assert.h> +#include <cassert> using namespace android; diff --git a/tools/aapt/Resource.cpp b/tools/aapt/Resource.cpp index b559002..36299c2 100644 --- a/tools/aapt/Resource.cpp +++ b/tools/aapt/Resource.cpp @@ -4,6 +4,7 @@ // Build resource files from raw assets. // #include "AaptAssets.h" +#include "AaptUtil.h" #include "AaptXml.h" #include "CacheUpdater.h" #include "CrunchCache.h" @@ -13,10 +14,14 @@ #include "Main.h" #include "ResourceTable.h" #include "StringPool.h" +#include "Symbol.h" #include "WorkQueue.h" #include "XMLNode.h" +#include <algorithm> + // STATUST: mingw does seem to redefine UNKNOWN_ERROR from our enum value, so a cast is necessary. + #if HAVE_PRINTF_ZD # define ZD "%zd" # define ZD_TYPE ssize_t @@ -261,6 +266,11 @@ static status_t parsePackage(Bundle* bundle, const sp<AaptAssets>& assets, assets->setPackage(String8(block.getAttributeStringValue(nameIndex, &len))); + ssize_t revisionCodeIndex = block.indexOfAttribute(RESOURCES_ANDROID_NAMESPACE, "revisionCode"); + if (revisionCodeIndex >= 0) { + bundle->setRevisionCode(String8(block.getAttributeStringValue(revisionCodeIndex, &len)).string()); + } + String16 uses_sdk16("uses-sdk"); while ((code=block.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { @@ -523,7 +533,7 @@ static int validateAttr(const String8& path, const ResTable& table, } if (validChars) { for (size_t i=0; i<len; i++) { - uint16_t c = str[i]; + char16_t c = str[i]; const char* p = validChars; bool okay = false; while (*p) { @@ -1098,6 +1108,14 @@ status_t generateAndroidManifestForSplit(Bundle* bundle, const sp<AaptAssets>& a return UNKNOWN_ERROR; } + // Add the 'revisionCode' attribute, which is set to the original revisionCode. + if (bundle->getRevisionCode().size() > 0) { + if (!addTagAttribute(manifest, RESOURCES_ANDROID_NAMESPACE, "revisionCode", + bundle->getRevisionCode().string(), true, true)) { + return UNKNOWN_ERROR; + } + } + // Add the 'split' attribute which describes the configurations included. String8 splitName("config."); splitName.append(split->getPackageSafeName()); @@ -1580,6 +1598,7 @@ status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuil // Re-flatten because we may have added new resource IDs // -------------------------------------------------------------- + ResTable finalResTable; sp<AaptFile> resFile; @@ -1590,6 +1609,13 @@ status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuil return err; } + KeyedVector<Symbol, Vector<SymbolDefinition> > densityVaryingResources; + if (builder->getSplits().size() > 1) { + // Only look for density varying resources if we're generating + // splits. + table.getDensityVaryingResources(densityVaryingResources); + } + Vector<sp<ApkSplit> >& splits = builder->getSplits(); const size_t numSplits = splits.size(); for (size_t i = 0; i < numSplits; i++) { @@ -1613,6 +1639,63 @@ status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuil return err; } } else { + ResTable resTable; + err = resTable.add(flattenedTable->getData(), flattenedTable->getSize()); + if (err != NO_ERROR) { + fprintf(stderr, "Generated resource table for split '%s' is corrupt.\n", + split->getPrintableName().string()); + return err; + } + + bool hasError = false; + const std::set<ConfigDescription>& splitConfigs = split->getConfigs(); + for (std::set<ConfigDescription>::const_iterator iter = splitConfigs.begin(); + iter != splitConfigs.end(); + ++iter) { + const ConfigDescription& config = *iter; + if (AaptConfig::isDensityOnly(config)) { + // Each density only split must contain all + // density only resources. + Res_value val; + resTable.setParameters(&config); + const size_t densityVaryingResourceCount = densityVaryingResources.size(); + for (size_t k = 0; k < densityVaryingResourceCount; k++) { + const Symbol& symbol = densityVaryingResources.keyAt(k); + ssize_t block = resTable.getResource(symbol.id, &val, true); + if (block < 0) { + // Maybe it's in the base? + finalResTable.setParameters(&config); + block = finalResTable.getResource(symbol.id, &val, true); + } + + if (block < 0) { + hasError = true; + SourcePos().error("%s has no definition for density split '%s'", + symbol.toString().string(), config.toString().string()); + + if (bundle->getVerbose()) { + const Vector<SymbolDefinition>& defs = densityVaryingResources[k]; + const size_t defCount = std::min(size_t(5), defs.size()); + for (size_t d = 0; d < defCount; d++) { + const SymbolDefinition& def = defs[d]; + def.source.error("%s has definition for %s", + symbol.toString().string(), def.config.toString().string()); + } + + if (defCount < defs.size()) { + SourcePos().error("and %d more ...", (int) (defs.size() - defCount)); + } + } + } + } + } + } + + if (hasError) { + return UNKNOWN_ERROR; + } + + // Generate the AndroidManifest for this split. sp<AaptFile> generatedManifest = new AaptFile(String8("AndroidManifest.xml"), AaptGroupEntry(), String8()); err = generateAndroidManifestForSplit(bundle, assets, split, @@ -2946,17 +3029,26 @@ status_t writeProguardForLayouts(ProguardKeepSet* keep, const sp<AaptAssets>& assets) { status_t err; + const char* kClass = "class"; + const char* kFragment = "fragment"; + const String8 kTransition("transition"); + const String8 kTransitionPrefix("transition-"); // 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"); + addTagAttrPair(&kLayoutTagAttrPairs, "view", NULL, kClass); + addTagAttrPair(&kLayoutTagAttrPairs, kFragment, NULL, kClass); + addTagAttrPair(&kLayoutTagAttrPairs, kFragment, 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"); + addTagAttrPair(&kXmlTagAttrPairs, "PreferenceScreen", RESOURCES_ANDROID_NAMESPACE, kFragment); + addTagAttrPair(&kXmlTagAttrPairs, "header", RESOURCES_ANDROID_NAMESPACE, kFragment); + + // tag:attribute pairs that should be checked in transition files. + KeyedVector<String8, Vector<NamespaceAttributePair> > kTransitionTagAttrPairs; + addTagAttrPair(&kTransitionTagAttrPairs, kTransition.string(), NULL, kClass); + addTagAttrPair(&kTransitionTagAttrPairs, "pathMotion", NULL, kClass); const Vector<sp<AaptDir> >& dirs = assets->resDirs(); const size_t K = dirs.size(); @@ -2975,6 +3067,9 @@ writeProguardForLayouts(ProguardKeepSet* keep, const sp<AaptAssets>& assets) } else if ((dirName == String8("menu")) || (strncmp(dirName.string(), "menu-", 5) == 0)) { startTags.add(String8("menu")); tagAttrPairs = NULL; + } else if (dirName == kTransition || (strncmp(dirName.string(), kTransitionPrefix.string(), + kTransitionPrefix.size()) == 0)) { + tagAttrPairs = &kTransitionTagAttrPairs; } else { continue; } diff --git a/tools/aapt/ResourceFilter.cpp b/tools/aapt/ResourceFilter.cpp index fc95e14..8693999 100644 --- a/tools/aapt/ResourceFilter.cpp +++ b/tools/aapt/ResourceFilter.cpp @@ -41,6 +41,13 @@ WeakResourceFilter::parse(const String8& str) // Ignore the version entry.second &= ~ResTable_config::CONFIG_VERSION; + // Ignore any densities. Those are best handled in --preferred-density + if ((entry.second & ResTable_config::CONFIG_DENSITY) != 0) { + fprintf(stderr, "warning: ignoring flag -c %s. Use --preferred-density instead.\n", entry.first.toString().string()); + entry.first.density = 0; + entry.second &= ~ResTable_config::CONFIG_DENSITY; + } + mConfigMask |= entry.second; } diff --git a/tools/aapt/ResourceIdCache.cpp b/tools/aapt/ResourceIdCache.cpp index d7b2d10..8835fb0 100644 --- a/tools/aapt/ResourceIdCache.cpp +++ b/tools/aapt/ResourceIdCache.cpp @@ -10,7 +10,6 @@ #include "ResourceIdCache.h" #include <map> - static size_t mHits = 0; static size_t mMisses = 0; static size_t mCollisions = 0; diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp index 0ec1aeb..5d15ffc 100644 --- a/tools/aapt/ResourceTable.cpp +++ b/tools/aapt/ResourceTable.cpp @@ -6,10 +6,13 @@ #include "ResourceTable.h" +#include "AaptUtil.h" #include "XMLNode.h" #include "ResourceFilter.h" #include "ResourceIdCache.h" +#include "SdkConstants.h" +#include <algorithm> #include <androidfw/ResourceTypes.h> #include <utils/ByteOrder.h> #include <utils/TypeHelpers.h> @@ -34,6 +37,8 @@ static const bool kPrintStringMetrics = true; static const bool kPrintStringMetrics = false; #endif +static const char* kAttrPrivateType = "^attr-private"; + status_t compileXmlFile(const Bundle* bundle, const sp<AaptAssets>& assets, const String16& resourceName, @@ -2151,8 +2156,16 @@ uint32_t ResourceTable::getResId(const String16& 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; + sp<ConfigList> c = t->getConfigs().valueFor(name); + if (c == NULL) { + if (type != String16("attr")) { + return 0; + } + t = p->getTypes().valueFor(String16(kAttrPrivateType)); + if (t == NULL) return 0; + c = t->getConfigs().valueFor(name); + if (c == NULL) return 0; + } int32_t ei = c->getEntryIndex(); if (ei < 0) return 0; @@ -2291,7 +2304,15 @@ uint32_t ResourceTable::getCustomResource( sp<Type> t = p->getTypes().valueFor(type); if (t == NULL) return 0; sp<ConfigList> c = t->getConfigs().valueFor(name); - if (c == NULL) return 0; + if (c == NULL) { + if (type != String16("attr")) { + return 0; + } + t = p->getTypes().valueFor(String16(kAttrPrivateType)); + if (t == NULL) return 0; + 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); @@ -2495,6 +2516,10 @@ status_t ResourceTable::assignResourceIds() continue; } + if (mPackageType == System) { + p->movePrivateAttrs(); + } + // This has no sense for packages being built as AppFeature (aka with a non-zero offset). status_t err = p->applyPublicTypeOrder(); if (err != NO_ERROR && firstError == NO_ERROR) { @@ -2565,15 +2590,20 @@ status_t ResourceTable::assignResourceIds() } } + // Assign resource IDs to keys in bags... for (size_t ti = 0; ti < typeCount; 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; + } //printf("Ordered config #%d: %p\n", ci, c.get()); const size_t N = c->getEntries().size(); for (size_t ei=0; ei<N; ei++) { @@ -2611,9 +2641,15 @@ status_t ResourceTable::addSymbols(const sp<AaptSymbols>& outSymbols) { if (t == NULL) { continue; } + const size_t N = t->getOrderedConfigs().size(); - sp<AaptSymbols> typeSymbols = - outSymbols->addNestedSymbol(String8(t->getName()), t->getPos()); + sp<AaptSymbols> typeSymbols; + if (t->getName() == String16(kAttrPrivateType)) { + typeSymbols = outSymbols->addNestedSymbol(String8("attr"), t->getPos()); + } else { + typeSymbols = outSymbols->addNestedSymbol(String8(t->getName()), t->getPos()); + } + if (typeSymbols == NULL) { return UNKNOWN_ERROR; } @@ -2979,6 +3015,10 @@ status_t ResourceTable::flatten(Bundle* bundle, const sp<const ResourceFilter>& for (size_t ei=0; ei<N; ei++) { sp<ConfigList> cl = t->getOrderedConfigs().itemAt(ei); + if (cl == NULL) { + continue; + } + if (cl->getPublic()) { typeSpecFlags[ei] |= htodl(ResTable_typeSpec::SPEC_PUBLIC); } @@ -3009,12 +3049,16 @@ status_t ResourceTable::flatten(Bundle* bundle, const sp<const ResourceFilter>& // We need to write one type chunk for each configuration for // which we have entries in this type. - const size_t NC = t != NULL ? t->getUniqueConfigs().size() : 0; + SortedVector<ConfigDescription> uniqueConfigs; + if (t != NULL) { + uniqueConfigs = t->getUniqueConfigs(); + } const size_t typeSize = sizeof(ResTable_type) + sizeof(uint32_t)*N; + const size_t NC = uniqueConfigs.size(); for (size_t ci=0; ci<NC; ci++) { - ConfigDescription config = t->getUniqueConfigs().itemAt(ci); + const ConfigDescription& config = uniqueConfigs[ci]; if (kIsDebug) { printf("Writing config %zu config: imsi:%d/%d lang:%c%c cnt:%c%c " @@ -3090,7 +3134,10 @@ status_t ResourceTable::flatten(Bundle* bundle, const sp<const ResourceFilter>& // 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); + sp<Entry> e = NULL; + if (cl != NULL) { + e = cl->getEntries().valueFor(config); + } // Set the offset for this entry in its type. uint32_t* index = (uint32_t*) @@ -3125,9 +3172,11 @@ status_t ResourceTable::flatten(Bundle* bundle, const sp<const ResourceFilter>& for (size_t i = 0; i < N; ++i) { if (!validResources[i]) { sp<ConfigList> c = t->getOrderedConfigs().itemAt(i); - fprintf(stderr, "%s: no entries written for %s/%s (0x%08x)\n", log_prefix, - String8(typeName).string(), String8(c->getName()).string(), - Res_MAKEID(p->getAssignedId() - 1, ti, i)); + if (c != NULL) { + fprintf(stderr, "%s: no entries written for %s/%s (0x%08x)\n", log_prefix, + String8(typeName).string(), String8(c->getName()).string(), + Res_MAKEID(p->getAssignedId() - 1, ti, i)); + } missing_entry = true; } } @@ -3839,11 +3888,45 @@ sp<ResourceTable::Entry> ResourceTable::Type::getEntry(const String16& entry, */ } - mUniqueConfigs.add(cdesc); - return e; } +sp<ResourceTable::ConfigList> ResourceTable::Type::removeEntry(const String16& entry) { + ssize_t idx = mConfigs.indexOfKey(entry); + if (idx < 0) { + return NULL; + } + + sp<ConfigList> removed = mConfigs.valueAt(idx); + mConfigs.removeItemsAt(idx); + + Vector<sp<ConfigList> >::iterator iter = std::find( + mOrderedConfigs.begin(), mOrderedConfigs.end(), removed); + if (iter != mOrderedConfigs.end()) { + mOrderedConfigs.erase(iter); + } + + mPublic.removeItem(entry); + return removed; +} + +SortedVector<ConfigDescription> ResourceTable::Type::getUniqueConfigs() const { + SortedVector<ConfigDescription> unique; + const size_t entryCount = mOrderedConfigs.size(); + for (size_t i = 0; i < entryCount; i++) { + if (mOrderedConfigs[i] == NULL) { + continue; + } + const DefaultKeyedVector<ConfigDescription, sp<Entry> >& configs = + mOrderedConfigs[i]->getEntries(); + const size_t configCount = configs.size(); + for (size_t j = 0; j < configCount; j++) { + unique.add(configs.keyAt(j)); + } + } + return unique; +} + status_t ResourceTable::Type::applyPublicEntryOrder() { size_t N = mOrderedConfigs.size(); @@ -3870,11 +3953,10 @@ status_t ResourceTable::Type::applyPublicEntryOrder() //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) { + mOrderedConfigs.resize(idx + 1); + } + + if (mOrderedConfigs.itemAt(idx) == NULL) { e->setPublic(true); e->setPublicSourcePos(p.sourcePos); mOrderedConfigs.replaceAt(e, idx); @@ -4047,6 +4129,61 @@ status_t ResourceTable::Package::applyPublicTypeOrder() return NO_ERROR; } +void ResourceTable::Package::movePrivateAttrs() { + sp<Type> attr = mTypes.valueFor(String16("attr")); + if (attr == NULL) { + // Nothing to do. + return; + } + + Vector<sp<ConfigList> > privateAttrs; + + bool hasPublic = false; + const Vector<sp<ConfigList> >& configs = attr->getOrderedConfigs(); + const size_t configCount = configs.size(); + for (size_t i = 0; i < configCount; i++) { + if (configs[i] == NULL) { + continue; + } + + if (attr->isPublic(configs[i]->getName())) { + hasPublic = true; + } else { + privateAttrs.add(configs[i]); + } + } + + // Only if we have public attributes do we create a separate type for + // private attributes. + if (!hasPublic) { + return; + } + + // Create a new type for private attributes. + sp<Type> privateAttrType = getType(String16(kAttrPrivateType), SourcePos()); + + const size_t privateAttrCount = privateAttrs.size(); + for (size_t i = 0; i < privateAttrCount; i++) { + const sp<ConfigList>& cl = privateAttrs[i]; + + // Remove the private attributes from their current type. + attr->removeEntry(cl->getName()); + + // Add it to the new type. + const DefaultKeyedVector<ConfigDescription, sp<Entry> >& entries = cl->getEntries(); + const size_t entryCount = entries.size(); + for (size_t j = 0; j < entryCount; j++) { + const sp<Entry>& oldEntry = entries[j]; + sp<Entry> entry = privateAttrType->getEntry( + cl->getName(), oldEntry->getPos(), &entries.keyAt(j)); + *entry = *oldEntry; + } + + // Move the symbols to the new type. + + } +} + sp<ResourceTable::Package> ResourceTable::getPackage(const String16& package) { if (package != mAssetsPackage) { @@ -4253,7 +4390,7 @@ static bool isMinSdkVersionLOrAbove(const Bundle* bundle) { } const int minSdk = atoi(bundle->getMinSdkVersion()); - if (minSdk >= SDK_L) { + if (minSdk >= SDK_LOLLIPOP) { return true; } } @@ -4344,7 +4481,7 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle) { } const ConfigDescription& config = entries.keyAt(ei); - if (config.sdkVersion >= SDK_L) { + if (config.sdkVersion >= SDK_LOLLIPOP) { // We don't need to do anything if the resource is // already qualified for version 21 or higher. continue; @@ -4366,9 +4503,9 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle) { } // Duplicate the entry under the same configuration - // but with sdkVersion == SDK_L. + // but with sdkVersion == SDK_LOLLIPOP. ConfigDescription newConfig(config); - newConfig.sdkVersion = SDK_L; + newConfig.sdkVersion = SDK_LOLLIPOP; entriesToAdd.add(key_value_pair_t<ConfigDescription, sp<Entry> >( newConfig, new Entry(*e))); @@ -4391,7 +4528,7 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle) { if (bundle->getVerbose()) { entriesToAdd[i].value->getPos() .printf("using v%d attributes; synthesizing resource %s:%s/%s for configuration %s.", - SDK_L, + SDK_LOLLIPOP, String8(p->getName()).string(), String8(t->getName()).string(), String8(entriesToAdd[i].value->getName()).string(), @@ -4418,13 +4555,13 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle, return NO_ERROR; } - if (target->getResourceType() == "" || target->getGroupEntry().toParams().sdkVersion >= SDK_L) { + if (target->getResourceType() == "" || target->getGroupEntry().toParams().sdkVersion >= SDK_LOLLIPOP) { // Skip resources that have no type (AndroidManifest.xml) or are already version qualified with v21 // or higher. return NO_ERROR; } - Vector<key_value_pair_t<sp<XMLNode>, size_t> > attrsToRemove; + sp<XMLNode> newRoot = NULL; Vector<sp<XMLNode> > nodesToVisit; nodesToVisit.push(root); @@ -4433,11 +4570,23 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle, nodesToVisit.pop(); const Vector<XMLNode::attribute_entry>& attrs = node->getAttributes(); - const size_t attrCount = attrs.size(); - for (size_t i = 0; i < attrCount; i++) { + for (size_t i = 0; i < attrs.size(); i++) { const XMLNode::attribute_entry& attr = attrs[i]; if (isAttributeFromL(attr.nameResId)) { - attrsToRemove.add(key_value_pair_t<sp<XMLNode>, size_t>(node, i)); + if (newRoot == NULL) { + newRoot = root->clone(); + } + + if (bundle->getVerbose()) { + SourcePos(node->getFilename(), node->getStartLineNumber()).printf( + "removing attribute %s%s%s from <%s>", + String8(attr.ns).string(), + (attr.ns.size() == 0 ? "" : ":"), + String8(attr.name).string(), + String8(node->getElementName()).string()); + } + node->removeAttribute(i); + i--; } } @@ -4449,22 +4598,18 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle, } } - if (attrsToRemove.isEmpty()) { + if (newRoot == NULL) { return NO_ERROR; } ConfigDescription newConfig(target->getGroupEntry().toParams()); - newConfig.sdkVersion = SDK_L; + newConfig.sdkVersion = SDK_LOLLIPOP; // Look to see if we already have an overriding v21 configuration. sp<ConfigList> cl = getConfigList(String16(mAssets->getPackage()), String16(target->getResourceType()), resourceName); - //if (cl == NULL) { - // fprintf(stderr, "fuuuuck\n"); - //} if (cl->getEntries().indexOfKey(newConfig) < 0) { // We don't have an overriding entry for v21, so we must duplicate this one. - sp<XMLNode> newRoot = root->clone(); sp<AaptFile> newFile = new AaptFile(target->getSourceFile(), AaptGroupEntry(newConfig), target->getResourceType()); String8 resPath = String8::format("res/%s/%s", @@ -4476,7 +4621,7 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle, if (bundle->getVerbose()) { SourcePos(target->getSourceFile(), -1).printf( "using v%d attributes; synthesizing resource %s:%s/%s for configuration %s.", - SDK_L, + SDK_LOLLIPOP, mAssets->getPackage().string(), newFile->getResourceType().string(), String8(resourceName).string(), @@ -4499,21 +4644,36 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle, mWorkQueue.push(item); } - const size_t removeCount = attrsToRemove.size(); - for (size_t i = 0; i < removeCount; i++) { - sp<XMLNode> node = attrsToRemove[i].key; - size_t attrIndex = attrsToRemove[i].value; - const XMLNode::attribute_entry& ae = node->getAttributes()[attrIndex]; - if (bundle->getVerbose()) { - SourcePos(node->getFilename(), node->getStartLineNumber()).printf( - "removing attribute %s%s%s from <%s>", - String8(ae.ns).string(), - (ae.ns.size() == 0 ? "" : ":"), - String8(ae.name).string(), - String8(node->getElementName()).string()); + return NO_ERROR; +} + +void ResourceTable::getDensityVaryingResources(KeyedVector<Symbol, Vector<SymbolDefinition> >& resources) { + const ConfigDescription nullConfig; + + const size_t packageCount = mOrderedPackages.size(); + for (size_t p = 0; p < packageCount; p++) { + const Vector<sp<Type> >& types = mOrderedPackages[p]->getOrderedTypes(); + const size_t typeCount = types.size(); + for (size_t t = 0; t < typeCount; t++) { + const Vector<sp<ConfigList> >& configs = types[t]->getOrderedConfigs(); + const size_t configCount = configs.size(); + for (size_t c = 0; c < configCount; c++) { + const DefaultKeyedVector<ConfigDescription, sp<Entry> >& configEntries = configs[c]->getEntries(); + const size_t configEntryCount = configEntries.size(); + for (size_t ce = 0; ce < configEntryCount; ce++) { + const ConfigDescription& config = configEntries.keyAt(ce); + if (AaptConfig::isDensityOnly(config)) { + // This configuration only varies with regards to density. + const Symbol symbol(mOrderedPackages[p]->getName(), + types[t]->getName(), + configs[c]->getName(), + getResId(mOrderedPackages[p], types[t], configs[c]->getEntryIndex())); + + const sp<Entry>& entry = configEntries.valueAt(ce); + AaptUtil::appendValue(resources, symbol, SymbolDefinition(symbol, config, entry->getPos())); + } + } + } } - node->removeAttribute(attrIndex); } - - return NO_ERROR; } diff --git a/tools/aapt/ResourceTable.h b/tools/aapt/ResourceTable.h index eac5dd3..81590bc 100644 --- a/tools/aapt/ResourceTable.h +++ b/tools/aapt/ResourceTable.h @@ -7,15 +7,16 @@ #ifndef RESOURCE_TABLE_H #define RESOURCE_TABLE_H -#include "ConfigDescription.h" -#include "StringPool.h" -#include "SourcePos.h" -#include "ResourceFilter.h" - #include <map> #include <queue> #include <set> +#include "ConfigDescription.h" +#include "ResourceFilter.h" +#include "SourcePos.h" +#include "StringPool.h" +#include "Symbol.h" + using namespace std; class XMLNode; @@ -469,6 +470,14 @@ public: bool overlay = false, bool autoAddOverlay = false); + bool isPublic(const String16& entry) const { + return mPublic.indexOfKey(entry) >= 0; + } + + sp<ConfigList> removeEntry(const String16& entry); + + SortedVector<ConfigDescription> getUniqueConfigs() const; + const SourcePos& getFirstPublicSourcePos() const { return *mFirstPublicSourcePos; } int32_t getPublicIndex() const { return mPublicIndex; } @@ -478,19 +487,16 @@ public: 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; @@ -526,6 +532,8 @@ public: const DefaultKeyedVector<String16, sp<Type> >& getTypes() const { return mTypes; } const Vector<sp<Type> >& getOrderedTypes() const { return mOrderedTypes; } + void movePrivateAttrs(); + private: status_t setStrings(const sp<AaptFile>& data, ResStringPool* strings, @@ -543,6 +551,8 @@ public: DefaultKeyedVector<String16, uint32_t> mKeyStringsMapping; }; + void getDensityVaryingResources(KeyedVector<Symbol, Vector<SymbolDefinition> >& resources); + private: void writePublicDefinitions(const String16& package, FILE* fp, bool pub); sp<Package> getPackage(const String16& package); diff --git a/tools/aapt/SdkConstants.h b/tools/aapt/SdkConstants.h new file mode 100644 index 0000000..7fd1030 --- /dev/null +++ b/tools/aapt/SdkConstants.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 H_AAPT_SDK_CONSTANTS +#define H_AAPT_SDK_CONSTANTS + +enum { + SDK_CUPCAKE = 3, + SDK_DONUT = 4, + SDK_ECLAIR = 5, + SDK_ECLAIR_0_1 = 6, + SDK_ECLAIR_MR1 = 7, + SDK_FROYO = 8, + SDK_GINGERBREAD = 9, + SDK_GINGERBREAD_MR1 = 10, + SDK_HONEYCOMB = 11, + SDK_HONEYCOMB_MR1 = 12, + SDK_HONEYCOMB_MR2 = 13, + SDK_ICE_CREAM_SANDWICH = 14, + SDK_ICE_CREAM_SANDWICH_MR1 = 15, + SDK_JELLY_BEAN = 16, + SDK_JELLY_BEAN_MR1 = 17, + SDK_JELLY_BEAN_MR2 = 18, + SDK_KITKAT = 19, + SDK_KITKAT_WATCH = 20, + SDK_LOLLIPOP = 21, +}; + +#endif // H_AAPT_SDK_CONSTANTS diff --git a/tools/aapt/SourcePos.cpp b/tools/aapt/SourcePos.cpp index ae25047..3864320 100644 --- a/tools/aapt/SourcePos.cpp +++ b/tools/aapt/SourcePos.cpp @@ -141,6 +141,12 @@ SourcePos::printf(const char* fmt, ...) const } bool +SourcePos::operator<(const SourcePos& rhs) const +{ + return (file < rhs.file) || (line < rhs.line); +} + +bool SourcePos::hasErrors() { return g_errors.size() > 0; diff --git a/tools/aapt/SourcePos.h b/tools/aapt/SourcePos.h index 4ce817f..13cfb9d 100644 --- a/tools/aapt/SourcePos.h +++ b/tools/aapt/SourcePos.h @@ -21,6 +21,8 @@ public: void warning(const char* fmt, ...) const; void printf(const char* fmt, ...) const; + bool operator<(const SourcePos& rhs) const; + static bool hasErrors(); static void printErrors(FILE* to); }; diff --git a/tools/aapt/StringPool.cpp b/tools/aapt/StringPool.cpp index e3ec0ae..a18e9f1 100644 --- a/tools/aapt/StringPool.cpp +++ b/tools/aapt/StringPool.cpp @@ -26,6 +26,7 @@ // Set to true for noisy debug output. static const bool kIsDebug = false; +#if __cplusplus >= 201103L void strcpy16_htod(char16_t* dst, const char16_t* src) { while (*src) { @@ -35,6 +36,17 @@ void strcpy16_htod(char16_t* dst, const char16_t* src) } *dst = 0; } +#endif + +void strcpy16_htod(uint16_t* dst, const char16_t* src) +{ + while (*src) { + uint16_t s = htods(static_cast<uint16_t>(*src)); + *dst++ = s; + src++; + } + *dst = 0; +} void printStringPool(const ResStringPool* pool) { @@ -434,7 +446,7 @@ status_t StringPool::writeStringBlock(const sp<AaptFile>& pool) return NO_MEMORY; } - const size_t charSize = mUTF8 ? sizeof(uint8_t) : sizeof(char16_t); + const size_t charSize = mUTF8 ? sizeof(uint8_t) : sizeof(uint16_t); size_t strPos = 0; for (i=0; i<STRINGS; i++) { diff --git a/tools/aapt/StringPool.h b/tools/aapt/StringPool.h index 0b26538..dbe8c85 100644 --- a/tools/aapt/StringPool.h +++ b/tools/aapt/StringPool.h @@ -26,7 +26,10 @@ using namespace android; #define PRINT_STRING_METRICS 0 +#if __cplusplus >= 201103L void strcpy16_htod(char16_t* dst, const char16_t* src); +#endif +void strcpy16_htod(uint16_t* dst, const char16_t* src); void printStringPool(const ResStringPool* pool); diff --git a/tools/aapt/Symbol.h b/tools/aapt/Symbol.h new file mode 100644 index 0000000..e157541 --- /dev/null +++ b/tools/aapt/Symbol.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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_SYMBOL_H +#define AAPT_SYMBOL_H + +#include <utils/String8.h> +#include <utils/String16.h> + +#include "ConfigDescription.h" +#include "SourcePos.h" + +/** + * A resource symbol, not attached to any configuration or context. + */ +struct Symbol { + inline Symbol(); + inline Symbol(const android::String16& p, const android::String16& t, const android::String16& n, uint32_t i); + inline android::String8 toString() const; + inline bool operator<(const Symbol& rhs) const; + + android::String16 package; + android::String16 type; + android::String16 name; + uint32_t id; + +}; + +/** + * A specific defintion of a symbol, defined with a configuration and a definition site. + */ +struct SymbolDefinition { + inline SymbolDefinition(); + inline SymbolDefinition(const Symbol& s, const ConfigDescription& c, const SourcePos& src); + inline bool operator<(const SymbolDefinition& rhs) const; + + Symbol symbol; + ConfigDescription config; + SourcePos source; +}; + +// +// Implementations +// + +Symbol::Symbol() { +} + +Symbol::Symbol(const android::String16& p, const android::String16& t, const android::String16& n, uint32_t i) + : package(p) + , type(t) + , name(n) + , id(i) { +} + +android::String8 Symbol::toString() const { + return android::String8::format("%s:%s/%s (0x%08x)", + android::String8(package).string(), + android::String8(type).string(), + android::String8(name).string(), + (int) id); +} + +bool Symbol::operator<(const Symbol& rhs) const { + return (package < rhs.package) || (type < rhs.type) || (name < rhs.name) || (id < rhs.id); +} + +SymbolDefinition::SymbolDefinition() { +} + +SymbolDefinition::SymbolDefinition(const Symbol& s, const ConfigDescription& c, const SourcePos& src) + : symbol(s) + , config(c) + , source(src) { +} + +bool SymbolDefinition::operator<(const SymbolDefinition& rhs) const { + return (symbol < rhs.symbol) || (config < rhs.config) || (source < rhs.source); +} + +#endif // AAPT_SYMBOL_H + diff --git a/tools/layoutlib/.idea/codeStyleSettings.xml b/tools/layoutlib/.idea/codeStyleSettings.xml index b324213..a04e440 100644 --- a/tools/layoutlib/.idea/codeStyleSettings.xml +++ b/tools/layoutlib/.idea/codeStyleSettings.xml @@ -67,9 +67,13 @@ </groups> </arrangement> </codeStyleSettings> + <codeStyleSettings language="XML"> + <indentOptions> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + </indentOptions> + </codeStyleSettings> </value> </option> <option name="USE_PER_PROJECT_SETTINGS" value="true" /> </component> -</project> - +</project>
\ No newline at end of file diff --git a/tools/layoutlib/bridge/resources/icons/shadow-b.png b/tools/layoutlib/bridge/resources/icons/shadow-b.png Binary files differnew file mode 100644 index 0000000..68f4f4b --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow-b.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow-bl.png b/tools/layoutlib/bridge/resources/icons/shadow-bl.png Binary files differnew file mode 100644 index 0000000..ee7dbe8 --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow-bl.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow-br.png b/tools/layoutlib/bridge/resources/icons/shadow-br.png Binary files differnew file mode 100644 index 0000000..c45ad77 --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow-br.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow-l.png b/tools/layoutlib/bridge/resources/icons/shadow-l.png Binary files differnew file mode 100644 index 0000000..77d0bd0 --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow-l.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow-r.png b/tools/layoutlib/bridge/resources/icons/shadow-r.png Binary files differnew file mode 100644 index 0000000..4af7a33 --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow-r.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow-tl.png b/tools/layoutlib/bridge/resources/icons/shadow-tl.png Binary files differnew file mode 100644 index 0000000..424fb36 --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow-tl.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow-tr.png b/tools/layoutlib/bridge/resources/icons/shadow-tr.png Binary files differnew file mode 100644 index 0000000..1fd0c77 --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow-tr.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow2-b.png b/tools/layoutlib/bridge/resources/icons/shadow2-b.png Binary files differnew file mode 100644 index 0000000..963973e --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow2-b.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow2-bl.png b/tools/layoutlib/bridge/resources/icons/shadow2-bl.png Binary files differnew file mode 100644 index 0000000..7612487 --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow2-bl.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow2-br.png b/tools/layoutlib/bridge/resources/icons/shadow2-br.png Binary files differnew file mode 100644 index 0000000..8e20252 --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow2-br.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow2-l.png b/tools/layoutlib/bridge/resources/icons/shadow2-l.png Binary files differnew file mode 100644 index 0000000..2db18a0 --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow2-l.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow2-r.png b/tools/layoutlib/bridge/resources/icons/shadow2-r.png Binary files differnew file mode 100644 index 0000000..8e026f1 --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow2-r.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow2-tl.png b/tools/layoutlib/bridge/resources/icons/shadow2-tl.png Binary files differnew file mode 100644 index 0000000..a8045ed --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow2-tl.png diff --git a/tools/layoutlib/bridge/resources/icons/shadow2-tr.png b/tools/layoutlib/bridge/resources/icons/shadow2-tr.png Binary files differnew file mode 100644 index 0000000..590373c --- /dev/null +++ b/tools/layoutlib/bridge/resources/icons/shadow2-tr.png diff --git a/tools/layoutlib/bridge/src/android/animation/AnimatorInflater_Delegate.java b/tools/layoutlib/bridge/src/android/animation/AnimatorInflater_Delegate.java new file mode 100644 index 0000000..4475fa4 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/animation/AnimatorInflater_Delegate.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.content.res.Resources.Theme; +import android.util.AttributeSet; + +/** + * Delegate providing alternate implementation to static methods in {@link AnimatorInflater}. + */ +public class AnimatorInflater_Delegate { + + @LayoutlibDelegate + /*package*/ static Animator loadAnimator(Context context, int id) + throws NotFoundException { + return loadAnimator(context.getResources(), context.getTheme(), id); + } + + @LayoutlibDelegate + /*package*/ static Animator loadAnimator(Resources resources, Theme theme, int id) + throws NotFoundException { + return loadAnimator(resources, theme, id, 1); + } + + @LayoutlibDelegate + /*package*/ static Animator loadAnimator(Resources resources, Theme theme, int id, + float pathErrorScale) throws NotFoundException { + // This is a temporary fix to http://b.android.com/77865. This skips loading the + // animation altogether. + // TODO: Remove this override when Path.approximate() is supported. + return new FakeAnimator(); + } + + @LayoutlibDelegate + /*package*/ static ValueAnimator loadAnimator(Resources res, Theme theme, + AttributeSet attrs, ValueAnimator anim, float pathErrorScale) + throws NotFoundException { + return AnimatorInflater.loadAnimator_Original(res, theme, attrs, anim, pathErrorScale); + } +} diff --git a/tools/layoutlib/bridge/src/android/animation/FakeAnimator.java b/tools/layoutlib/bridge/src/android/animation/FakeAnimator.java new file mode 100644 index 0000000..78aedc5 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/animation/FakeAnimator.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +/** + * A fake implementation of Animator which doesn't do anything. + */ +public class FakeAnimator extends Animator { + @Override + public long getStartDelay() { + return 0; + } + + @Override + public void setStartDelay(long startDelay) { + + } + + @Override + public Animator setDuration(long duration) { + return this; + } + + @Override + public long getDuration() { + return 0; + } + + @Override + public void setInterpolator(TimeInterpolator value) { + + } + + @Override + public boolean isRunning() { + return false; + } +} diff --git a/tools/layoutlib/bridge/src/android/content/res/AssetManager_Delegate.java b/tools/layoutlib/bridge/src/android/content/res/AssetManager_Delegate.java index 914a359..e0d3b8c 100644 --- a/tools/layoutlib/bridge/src/android/content/res/AssetManager_Delegate.java +++ b/tools/layoutlib/bridge/src/android/content/res/AssetManager_Delegate.java @@ -38,8 +38,4 @@ public class AssetManager_Delegate { Resources_Theme_Delegate.getDelegateManager().removeJavaReferenceFor(theme); } - @LayoutlibDelegate - /*package*/ static void applyThemeStyle(long theme, int styleRes, boolean force) { - Resources_Theme_Delegate.getDelegateManager().getDelegate(theme).force = force; - } } diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeAssetManager.java b/tools/layoutlib/bridge/src/android/content/res/BridgeAssetManager.java index 93814b2..c41a4ee 100644 --- a/tools/layoutlib/bridge/src/android/content/res/BridgeAssetManager.java +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeAssetManager.java @@ -23,7 +23,7 @@ import android.content.res.AssetManager; public class BridgeAssetManager extends AssetManager { /** - * This initializes the static field {@link AssetManager#mSystem} which is used + * This initializes the static field {@link AssetManager#sSystem} 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. diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java b/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java index dd573be..66126af 100644 --- a/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeResources.java @@ -163,7 +163,7 @@ public final class BridgeResources extends Resources { Pair<String, ResourceValue> value = getResourceValue(id, mPlatformResourceFlag); if (value != null) { - return ResourceHelper.getDrawable(value.getSecond(), mContext); + return ResourceHelper.getDrawable(value.getSecond(), mContext, theme); } // id was not found or not resolved. Throw a NotFoundException. diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java index 28a109d..a2bd6d7 100644 --- a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java @@ -32,6 +32,7 @@ import com.android.resources.ResourceType; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.content.res.Resources.Theme; import android.graphics.drawable.Drawable; import android.util.DisplayMetrics; import android.util.TypedValue; @@ -116,6 +117,13 @@ public final class BridgeTypedArray extends TypedArray { } /** + * Set the theme to be used for inflating drawables. + */ + public void setTheme(Theme theme) { + mTheme = theme; + } + + /** * Return the number of values in this array. */ @Override @@ -663,7 +671,7 @@ public final class BridgeTypedArray extends TypedArray { } ResourceValue value = mResourceData[index]; - return ResourceHelper.getDrawable(value, mContext); + return ResourceHelper.getDrawable(value, mContext, mTheme); } 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 index f4a9f52..4bd83e9 100644 --- a/tools/layoutlib/bridge/src/android/content/res/Resources_Theme_Delegate.java +++ b/tools/layoutlib/bridge/src/android/content/res/Resources_Theme_Delegate.java @@ -39,9 +39,6 @@ import android.util.TypedValue; */ public class Resources_Theme_Delegate { - // Whether to use the Theme.mThemeResId as primary theme. - boolean force; - // ---- delegate manager ---- private static final DelegateManager<Resources_Theme_Delegate> sManager = @@ -58,7 +55,8 @@ public class Resources_Theme_Delegate { Resources thisResources, Theme thisTheme, int[] attrs) { boolean changed = setupResources(thisTheme); - TypedArray ta = RenderSessionImpl.getCurrentContext().obtainStyledAttributes(attrs); + BridgeTypedArray ta = RenderSessionImpl.getCurrentContext().obtainStyledAttributes(attrs); + ta.setTheme(thisTheme); restoreResources(changed); return ta; } @@ -69,7 +67,9 @@ public class Resources_Theme_Delegate { int resid, int[] attrs) throws NotFoundException { boolean changed = setupResources(thisTheme); - TypedArray ta = RenderSessionImpl.getCurrentContext().obtainStyledAttributes(resid, attrs); + BridgeTypedArray ta = RenderSessionImpl.getCurrentContext().obtainStyledAttributes(resid, + attrs); + ta.setTheme(thisTheme); restoreResources(changed); return ta; } @@ -79,8 +79,9 @@ public class Resources_Theme_Delegate { Resources thisResources, Theme thisTheme, AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) { boolean changed = setupResources(thisTheme); - TypedArray ta = RenderSessionImpl.getCurrentContext().obtainStyledAttributes(set, attrs, - defStyleAttr, defStyleRes); + BridgeTypedArray ta = RenderSessionImpl.getCurrentContext().obtainStyledAttributes(set, + attrs, defStyleAttr, defStyleRes); + ta.setTheme(thisTheme); restoreResources(changed); return ta; } @@ -91,8 +92,8 @@ public class Resources_Theme_Delegate { int resid, TypedValue outValue, boolean resolveRefs) { boolean changed = setupResources(thisTheme); - boolean found = RenderSessionImpl.getCurrentContext().resolveThemeAttribute( - resid, outValue, resolveRefs); + boolean found = RenderSessionImpl.getCurrentContext().resolveThemeAttribute(resid, + outValue, resolveRefs); restoreResources(changed); return found; } @@ -107,14 +108,29 @@ public class Resources_Theme_Delegate { // ---- private helper methods ---- private static boolean setupResources(Theme thisTheme) { - Resources_Theme_Delegate themeDelegate = sManager.getDelegate(thisTheme.getNativeTheme()); - StyleResourceValue style = resolveStyle(thisTheme.getAppliedStyleResId()); - if (style != null) { - RenderSessionImpl.getCurrentContext().getRenderResources() - .applyStyle(style, themeDelegate.force); - return true; + // Key is a space-separated list of theme ids applied that have been merged into the + // BridgeContext's theme to make thisTheme. + String[] appliedStyles = thisTheme.getKey().split(" "); + boolean changed = false; + for (String s : appliedStyles) { + if (s.isEmpty()) { + continue; + } + // See the definition of force parameter in Theme.applyStyle(). + boolean force = false; + if (s.charAt(s.length() - 1) == '!') { + force = true; + s = s.substring(0, s.length() - 1); + } + int styleId = Integer.parseInt(s, 16); + StyleResourceValue style = resolveStyle(styleId); + if (style != null) { + RenderSessionImpl.getCurrentContext().getRenderResources().applyStyle(style, force); + changed = true; + } + } - return false; + return changed; } private static void restoreResources(boolean changed) { diff --git a/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java index f4282ad..8d24d38 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Bitmap_Delegate.java @@ -141,7 +141,6 @@ public final class Bitmap_Delegate { * Creates and returns a {@link Bitmap} initialized with the given stream content. * * @param input the stream from which to read the bitmap content - * @param createFlags * @param density the density associated with the bitmap * * @see Bitmap#isPremultiplied() @@ -166,8 +165,7 @@ public final class Bitmap_Delegate { * @see Bitmap#isMutable() * @see Bitmap#getDensity() */ - public static Bitmap createBitmap(BufferedImage image, boolean isMutable, - Density density) throws IOException { + public static Bitmap createBitmap(BufferedImage image, boolean isMutable, Density density) { return createBitmap(image, getPremultipliedBitmapCreateFlags(isMutable), density); } @@ -175,7 +173,6 @@ public final class Bitmap_Delegate { * Creates and returns a {@link Bitmap} initialized with the given {@link BufferedImage} * * @param image the bitmap content - * @param createFlags * @param density the density associated with the bitmap * * @see Bitmap#isPremultiplied() @@ -183,7 +180,7 @@ public final class Bitmap_Delegate { * @see Bitmap#getDensity() */ public static Bitmap createBitmap(BufferedImage image, Set<BitmapCreateFlags> createFlags, - Density density) throws IOException { + Density density) { // create a delegate with the given image. Bitmap_Delegate delegate = new Bitmap_Delegate(image, Config.ARGB_8888); diff --git a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java index bef5181..ab79664 100644 --- a/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/FontFamily_Delegate.java @@ -213,6 +213,10 @@ public class FontFamily_Delegate { return null; } + @Nullable + /*package*/ static String getFontLocation() { + return sFontLocation; + } // ---- native methods ---- @@ -278,7 +282,7 @@ public class FontFamily_Delegate { @LayoutlibDelegate /*package*/ static boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr, String path) { Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED, - "FontFamily.addFontFromAsset is not supported.", null, null); + "Typeface.createFromAsset is not supported.", null, null); return false; } diff --git a/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java index 276e134..b9460b4 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Typeface_Delegate.java @@ -27,6 +27,8 @@ import java.io.File; import java.util.ArrayList; import java.util.List; +import static android.graphics.FontFamily_Delegate.getFontLocation; + /** * Delegate implementing the native methods of android.graphics.Typeface * @@ -48,8 +50,6 @@ public final class Typeface_Delegate { private static final DelegateManager<Typeface_Delegate> sManager = new DelegateManager<Typeface_Delegate>(Typeface_Delegate.class); - // ---- delegate helper data ---- - private static String sFontLocation; // ---- delegate data ---- @@ -61,11 +61,8 @@ public final class Typeface_Delegate { private static long sDefaultTypeface; + // ---- Public Helper methods ---- - public static synchronized void setFontLocation(String fontLocation) { - sFontLocation = fontLocation; - FontFamily_Delegate.setFontLocation(fontLocation); - } public static Typeface_Delegate getDelegate(long nativeTypeface) { return sManager.getDelegate(nativeTypeface); @@ -131,6 +128,18 @@ public final class Typeface_Delegate { return fonts; } + /** + * Clear the default typefaces when disposing bridge. + */ + public static void resetDefaults() { + // Sometimes this is called before the Bridge is initialized. In that case, we don't want to + // initialize Typeface because the SDK fonts location hasn't been set. + if (FontFamily_Delegate.getFontLocation() != null) { + Typeface.sDefaults = null; + } + } + + // ---- native methods ---- @LayoutlibDelegate @@ -193,7 +202,7 @@ public final class Typeface_Delegate { @LayoutlibDelegate /*package*/ static File getSystemFontConfigLocation() { - return new File(sFontLocation); + return new File(getFontLocation()); } // ---- Private delegate/helper methods ---- diff --git a/tools/layoutlib/bridge/src/android/preference/BridgePreferenceInflater.java b/tools/layoutlib/bridge/src/android/preference/BridgePreferenceInflater.java new file mode 100644 index 0000000..4f00b5d --- /dev/null +++ b/tools/layoutlib/bridge/src/android/preference/BridgePreferenceInflater.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.preference; + +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; + +import android.content.Context; +import android.util.AttributeSet; + +public class BridgePreferenceInflater extends PreferenceInflater { + + public BridgePreferenceInflater(Context context, PreferenceManager preferenceManager) { + super(context, preferenceManager); + } + + @Override + protected Preference onCreateItem(String name, AttributeSet attrs) + throws ClassNotFoundException { + Object viewKey = null; + BridgeContext bc = null; + + Context context = getContext(); + if (context instanceof BridgeContext) { + bc = (BridgeContext) context; + } + if (attrs instanceof BridgeXmlBlockParser) { + viewKey = ((BridgeXmlBlockParser) attrs).getViewCookie(); + } + + Preference preference = super.onCreateItem(name, attrs); + + if (viewKey != null && bc != null) { + bc.addCookie(preference, viewKey); + } + return preference; + } +} diff --git a/tools/layoutlib/bridge/src/android/preference/Preference_Delegate.java b/tools/layoutlib/bridge/src/android/preference/Preference_Delegate.java new file mode 100644 index 0000000..49ee642 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/preference/Preference_Delegate.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.preference; + +import com.android.internal.R; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import org.xmlpull.v1.XmlPullParser; + +import android.content.Context; +import android.content.res.TypedArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; + +import java.util.HashMap; +import java.util.Map; + +/** + * Delegate that provides implementation for native methods in {@link Preference} + * <p/> + * Through the layoutlib_create tool, selected methods of Preference have been replaced by calls to + * methods of the same name in this delegate class. + */ +public class Preference_Delegate { + + @LayoutlibDelegate + /*package*/ static View getView(Preference pref, View convertView, ViewGroup parent) { + Context context = pref.getContext(); + BridgeContext bc = context instanceof BridgeContext ? ((BridgeContext) context) : null; + convertView = pref.getView_Original(convertView, parent); + if (bc != null) { + Object cookie = bc.getCookie(pref); + if (cookie != null) { + bc.addViewKey(convertView, cookie); + } + } + return convertView; + } + + /** + * Inflates the parser and returns the ListView containing the Preferences. + */ + public static View inflatePreference(Context context, XmlPullParser parser, ViewGroup root) { + PreferenceManager pm = new PreferenceManager(context); + PreferenceScreen ps = pm.getPreferenceScreen(); + PreferenceInflater inflater = new BridgePreferenceInflater(context, pm); + ps = (PreferenceScreen) inflater.inflate(parser, ps, true); + ListView preferenceView = createContainerView(context, root); + ps.bind(preferenceView); + return preferenceView; + } + + private static ListView createContainerView(Context context, ViewGroup root) { + TypedArray a = context.obtainStyledAttributes(null, R.styleable.PreferenceFragment, + R.attr.preferenceFragmentStyle, 0); + int mLayoutResId = a.getResourceId(R.styleable.PreferenceFragment_layout, + R.layout.preference_list_fragment); + a.recycle(); + + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(mLayoutResId, root, true); + + return (ListView) root.findViewById(android.R.id.list); + } +} diff --git a/tools/layoutlib/bridge/src/android/text/StaticLayout_Delegate.java b/tools/layoutlib/bridge/src/android/text/StaticLayout_Delegate.java index 5a467b2..b0d79a8 100644 --- a/tools/layoutlib/bridge/src/android/text/StaticLayout_Delegate.java +++ b/tools/layoutlib/bridge/src/android/text/StaticLayout_Delegate.java @@ -13,8 +13,8 @@ import javax.swing.text.Segment; /** * Delegate that provides implementation for native methods in {@link android.text.StaticLayout} - * - * Through the layoutlib_create tool, selected methods of Handler have been replaced + * <p/> + * Through the layoutlib_create tool, selected methods of StaticLayout have been replaced * by calls to methods of the same name in this delegate class. * */ diff --git a/tools/layoutlib/bridge/src/android/text/format/DateFormat_Delegate.java b/tools/layoutlib/bridge/src/android/text/format/DateFormat_Delegate.java index 8cd1a69..1e4f213 100644 --- a/tools/layoutlib/bridge/src/android/text/format/DateFormat_Delegate.java +++ b/tools/layoutlib/bridge/src/android/text/format/DateFormat_Delegate.java @@ -34,4 +34,9 @@ public class DateFormat_Delegate { /*package*/ static boolean is24HourFormat(Context context) { return false; } + + @LayoutlibDelegate + /*package*/ static boolean is24HourFormat(Context context, int userHandle) { + return false; + } } diff --git a/tools/layoutlib/bridge/src/android/text/format/Time_Delegate.java b/tools/layoutlib/bridge/src/android/text/format/Time_Delegate.java deleted file mode 100644 index ed8498f..0000000 --- a/tools/layoutlib/bridge/src/android/text/format/Time_Delegate.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 java.util.Calendar; -import java.util.TimeZone; -import java.util.UnknownFormatConversionException; -import java.util.regex.Pattern; - -import com.android.ide.common.rendering.api.LayoutLog; -import com.android.layoutlib.bridge.Bridge; -import com.android.tools.layoutlib.annotations.LayoutlibDelegate; - -/** - * Delegate used to provide new implementation for native methods of {@link Time} - * - * Through the layoutlib_create tool, some native methods of Time have been replaced by calls to - * methods of the same name in this delegate class. - */ -public class Time_Delegate { - - // Regex to match odd number of '%'. - private static final Pattern p = Pattern.compile("(?<!%)(%%)*%(?!%)"); - - // Format used by toString() - private static final String FORMAT = "%1$tY%1$tm%1$tdT%1$tH%1$tM%1$tS<%1$tZ>"; - - // ---- private helper methods ---- - - private static Calendar timeToCalendar(Time time) { - Calendar calendar = getCalendarInstance(time); - calendar.set(time.year, time.month, time.monthDay, time.hour, time.minute, time.second); - return calendar; - } - - private static void calendarToTime(Calendar c, Time time) { - time.timezone = c.getTimeZone().getID(); - time.set(c.get(Calendar.SECOND), c.get(Calendar.MINUTE), c.get(Calendar.HOUR_OF_DAY), - c.get(Calendar.DATE), c.get(Calendar.MONTH), c.get(Calendar.YEAR)); - time.weekDay = c.get(Calendar.DAY_OF_WEEK); - time.yearDay = c.get(Calendar.DAY_OF_YEAR); - time.isDst = c.getTimeZone().inDaylightTime(c.getTime()) ? 1 : 0; - // gmtoff is in seconds and TimeZone.getOffset() returns milliseconds. - time.gmtoff = c.getTimeZone().getOffset(c.getTimeInMillis()) / DateUtils.SECOND_IN_MILLIS; - } - - /** - * Return a calendar instance with the correct timezone. - * - * @param time Time to obtain the timezone from. - */ - private static Calendar getCalendarInstance(Time time) { - // TODO: Check platform code to make sure the behavior is same for null/invalid timezone. - if (time == null || time.timezone == null) { - // Default to local timezone. - return Calendar.getInstance(); - } - // If timezone is invalid, use GMT. - return Calendar.getInstance(TimeZone.getTimeZone(time.timezone)); - } -} diff --git a/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java b/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java index c403ce6..5176419 100644 --- a/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java +++ b/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java @@ -228,6 +228,11 @@ public class IWindowManagerImpl implements IWindowManager { } @Override + public void overridePendingAppTransitionInPlace(String packageName, int anim) { + // TODO Auto-generated method stub + } + + @Override public void pauseKeyDispatching(IBinder arg0) throws RemoteException { // TODO Auto-generated method stub diff --git a/tools/layoutlib/bridge/src/android/view/RenderNode_Delegate.java b/tools/layoutlib/bridge/src/android/view/RenderNode_Delegate.java new file mode 100644 index 0000000..6c949d9 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/RenderNode_Delegate.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.impl.DelegateManager; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +/** + * Delegate implementing the native methods of {@link RenderNode} + * <p/> + * Through the layoutlib_create tool, some native methods of RenderNode have been replaced by calls + * to methods of the same name in this delegate class. + * + * @see DelegateManager + */ +public class RenderNode_Delegate { + + + // ---- delegate manager ---- + private static final DelegateManager<RenderNode_Delegate> sManager = + new DelegateManager<RenderNode_Delegate>(RenderNode_Delegate.class); + + + private float mLift; + @SuppressWarnings("UnusedDeclaration") + private String mName; + + @LayoutlibDelegate + /*package*/ static long nCreate(String name) { + RenderNode_Delegate renderNodeDelegate = new RenderNode_Delegate(); + renderNodeDelegate.mName = name; + return sManager.addNewDelegate(renderNodeDelegate); + } + + @LayoutlibDelegate + /*package*/ static void nDestroyRenderNode(long renderNode) { + sManager.removeJavaReferenceFor(renderNode); + } + + @LayoutlibDelegate + /*package*/ static boolean nSetElevation(long renderNode, float lift) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null && delegate.mLift != lift) { + delegate.mLift = lift; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static float nGetElevation(long renderNode) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null) { + return delegate.mLift; + } + return 0f; + } +} diff --git a/tools/layoutlib/bridge/src/android/view/ShadowPainter.java b/tools/layoutlib/bridge/src/android/view/ShadowPainter.java new file mode 100644 index 0000000..38846bd --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/ShadowPainter.java @@ -0,0 +1,415 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.annotations.NonNull; + +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; + +public class ShadowPainter { + + /** + * Adds a drop shadow to a semi-transparent image (of an arbitrary shape) and returns it as a + * new image. This method attempts to mimic the same visual characteristics as the rectangular + * shadow painting methods in this class, {@link #createRectangularDropShadow(java.awt.image.BufferedImage)} + * and {@link #createSmallRectangularDropShadow(java.awt.image.BufferedImage)}. + * + * @param source the source image + * @param shadowSize the size of the shadow, normally {@link #SHADOW_SIZE or {@link + * #SMALL_SHADOW_SIZE}} + * + * @return a new image with the shadow painted in + */ + @NonNull + public static BufferedImage createDropShadow(BufferedImage source, int shadowSize) { + shadowSize /= 2; // make shadow size have the same meaning as in the other shadow paint methods in this class + + return createDropShadow(source, shadowSize, 0.7f, 0); + } + + /** + * Creates a drop shadow of a given image and returns a new image which shows the input image on + * top of its drop shadow. + * <p/> + * <b>NOTE: If the shape is rectangular and opaque, consider using {@link + * #drawRectangleShadow(Graphics2D, int, int, int, int)} instead.</b> + * + * @param source the source image to be shadowed + * @param shadowSize the size of the shadow in pixels + * @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque + * @param shadowRgb the RGB int to use for the shadow color + * + * @return a new image with the source image on top of its shadow + */ + @SuppressWarnings({"SuspiciousNameCombination", "UnnecessaryLocalVariable"}) // Imported code + public static BufferedImage createDropShadow(BufferedImage source, int shadowSize, + float shadowOpacity, int shadowRgb) { + + // This code is based on + // http://www.jroller.com/gfx/entry/non_rectangular_shadow + + BufferedImage image; + int width = source.getWidth(); + int height = source.getHeight(); + image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE, + BufferedImage.TYPE_INT_ARGB); + + Graphics2D g2 = image.createGraphics(); + g2.drawImage(image, shadowSize, shadowSize, null); + + int dstWidth = image.getWidth(); + int dstHeight = image.getHeight(); + + int left = (shadowSize - 1) >> 1; + int right = shadowSize - left; + int xStart = left; + int xStop = dstWidth - right; + int yStart = left; + int yStop = dstHeight - right; + + shadowRgb &= 0x00FFFFFF; + + int[] aHistory = new int[shadowSize]; + int historyIdx; + + int aSum; + + int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + int lastPixelOffset = right * dstWidth; + float sumDivider = shadowOpacity / shadowSize; + + // horizontal pass + for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) { + aSum = 0; + historyIdx = 0; + for (int x = 0; x < shadowSize; x++, bufferOffset++) { + int a = dataBuffer[bufferOffset] >>> 24; + aHistory[x] = a; + aSum += a; + } + + bufferOffset -= right; + + for (int x = xStart; x < xStop; x++, bufferOffset++) { + int a = (int) (aSum * sumDivider); + dataBuffer[bufferOffset] = a << 24 | shadowRgb; + + // subtract the oldest pixel from the sum + aSum -= aHistory[historyIdx]; + + // get the latest pixel + a = dataBuffer[bufferOffset + right] >>> 24; + aHistory[historyIdx] = a; + aSum += a; + + if (++historyIdx >= shadowSize) { + historyIdx -= shadowSize; + } + } + } + // vertical pass + for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) { + aSum = 0; + historyIdx = 0; + for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) { + int a = dataBuffer[bufferOffset] >>> 24; + aHistory[y] = a; + aSum += a; + } + + bufferOffset -= lastPixelOffset; + + for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) { + int a = (int) (aSum * sumDivider); + dataBuffer[bufferOffset] = a << 24 | shadowRgb; + + // subtract the oldest pixel from the sum + aSum -= aHistory[historyIdx]; + + // get the latest pixel + a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24; + aHistory[historyIdx] = a; + aSum += a; + + if (++historyIdx >= shadowSize) { + historyIdx -= shadowSize; + } + } + } + + g2.drawImage(source, null, 0, 0); + g2.dispose(); + + return image; + } + + /** + * Draws a rectangular drop shadow (of size {@link #SHADOW_SIZE} by {@link #SHADOW_SIZE} around + * the given source and returns a new image with both combined + * + * @param source the source image + * + * @return the source image with a drop shadow on the bottom and right + */ + @SuppressWarnings("UnusedDeclaration") + public static BufferedImage createRectangularDropShadow(BufferedImage source) { + int type = source.getType(); + if (type == BufferedImage.TYPE_CUSTOM) { + type = BufferedImage.TYPE_INT_ARGB; + } + + int width = source.getWidth(); + int height = source.getHeight(); + BufferedImage image; + image = new BufferedImage(width + SHADOW_SIZE, height + SHADOW_SIZE, type); + Graphics2D g = image.createGraphics(); + g.drawImage(source, 0, 0, null); + drawRectangleShadow(image, 0, 0, width, height); + g.dispose(); + + return image; + } + + /** + * Draws a small rectangular drop shadow (of size {@link #SMALL_SHADOW_SIZE} by {@link + * #SMALL_SHADOW_SIZE} around the given source and returns a new image with both combined + * + * @param source the source image + * + * @return the source image with a drop shadow on the bottom and right + */ + @SuppressWarnings("UnusedDeclaration") + public static BufferedImage createSmallRectangularDropShadow(BufferedImage source) { + int type = source.getType(); + if (type == BufferedImage.TYPE_CUSTOM) { + type = BufferedImage.TYPE_INT_ARGB; + } + + int width = source.getWidth(); + int height = source.getHeight(); + + BufferedImage image; + image = new BufferedImage(width + SMALL_SHADOW_SIZE, height + SMALL_SHADOW_SIZE, type); + + Graphics2D g = image.createGraphics(); + g.drawImage(source, 0, 0, null); + drawSmallRectangleShadow(image, 0, 0, width, height); + g.dispose(); + + return image; + } + + /** + * Draws a drop shadow for the given rectangle into the given context. It will not draw anything + * if the rectangle is smaller than a minimum determined by the assets used to draw the shadow + * graphics. The size of the shadow is {@link #SHADOW_SIZE}. + * + * @param image the image to draw the shadow into + * @param x the left coordinate of the left hand side of the rectangle + * @param y the top coordinate of the top of the rectangle + * @param width the width of the rectangle + * @param height the height of the rectangle + */ + public static void drawRectangleShadow(BufferedImage image, + int x, int y, int width, int height) { + Graphics2D gc = image.createGraphics(); + try { + drawRectangleShadow(gc, x, y, width, height); + } finally { + gc.dispose(); + } + } + + /** + * Draws a small drop shadow for the given rectangle into the given context. It will not draw + * anything if the rectangle is smaller than a minimum determined by the assets used to draw the + * shadow graphics. The size of the shadow is {@link #SMALL_SHADOW_SIZE}. + * + * @param image the image to draw the shadow into + * @param x the left coordinate of the left hand side of the rectangle + * @param y the top coordinate of the top of the rectangle + * @param width the width of the rectangle + * @param height the height of the rectangle + */ + public static void drawSmallRectangleShadow(BufferedImage image, + int x, int y, int width, int height) { + Graphics2D gc = image.createGraphics(); + try { + drawSmallRectangleShadow(gc, x, y, width, height); + } finally { + gc.dispose(); + } + } + + /** + * The width and height of the drop shadow painted by + * {@link #drawRectangleShadow(Graphics2D, int, int, int, int)} + */ + public static final int SHADOW_SIZE = 20; // DO NOT EDIT. This corresponds to bitmap graphics + + /** + * The width and height of the drop shadow painted by + * {@link #drawSmallRectangleShadow(Graphics2D, int, int, int, int)} + */ + public static final int SMALL_SHADOW_SIZE = 10; // DO NOT EDIT. Corresponds to bitmap graphics + + /** + * Draws a drop shadow for the given rectangle into the given context. It will not draw anything + * if the rectangle is smaller than a minimum determined by the assets used to draw the shadow + * graphics. + * + * @param gc the graphics context to draw into + * @param x the left coordinate of the left hand side of the rectangle + * @param y the top coordinate of the top of the rectangle + * @param width the width of the rectangle + * @param height the height of the rectangle + */ + public static void drawRectangleShadow(Graphics2D gc, int x, int y, int width, int height) { + assert ShadowBottomLeft != null; + assert ShadowBottomRight.getWidth(null) == SHADOW_SIZE; + assert ShadowBottomRight.getHeight(null) == SHADOW_SIZE; + + int blWidth = ShadowBottomLeft.getWidth(null); + int trHeight = ShadowTopRight.getHeight(null); + if (width < blWidth) { + return; + } + if (height < trHeight) { + return; + } + + gc.drawImage(ShadowBottomLeft, x - ShadowBottomLeft.getWidth(null), y + height, null); + gc.drawImage(ShadowBottomRight, x + width, y + height, null); + gc.drawImage(ShadowTopRight, x + width, y, null); + gc.drawImage(ShadowTopLeft, x - ShadowTopLeft.getWidth(null), y, null); + gc.drawImage(ShadowBottom, + x, y + height, x + width, y + height + ShadowBottom.getHeight(null), + 0, 0, ShadowBottom.getWidth(null), ShadowBottom.getHeight(null), null); + gc.drawImage(ShadowRight, + x + width, y + ShadowTopRight.getHeight(null), x + width + ShadowRight.getWidth(null), y + height, + 0, 0, ShadowRight.getWidth(null), ShadowRight.getHeight(null), null); + gc.drawImage(ShadowLeft, + x - ShadowLeft.getWidth(null), y + ShadowTopLeft.getHeight(null), x, y + height, + 0, 0, ShadowLeft.getWidth(null), ShadowLeft.getHeight(null), null); + } + + /** + * Draws a small drop shadow for the given rectangle into the given context. It will not draw + * anything if the rectangle is smaller than a minimum determined by the assets used to draw the + * shadow graphics. + * <p/> + * + * @param gc the graphics context to draw into + * @param x the left coordinate of the left hand side of the rectangle + * @param y the top coordinate of the top of the rectangle + * @param width the width of the rectangle + * @param height the height of the rectangle + */ + public static void drawSmallRectangleShadow(Graphics2D gc, int x, int y, int width, + int height) { + assert Shadow2BottomLeft != null; + assert Shadow2TopRight != null; + assert Shadow2BottomRight.getWidth(null) == SMALL_SHADOW_SIZE; + assert Shadow2BottomRight.getHeight(null) == SMALL_SHADOW_SIZE; + + int blWidth = Shadow2BottomLeft.getWidth(null); + int trHeight = Shadow2TopRight.getHeight(null); + if (width < blWidth) { + return; + } + if (height < trHeight) { + return; + } + + gc.drawImage(Shadow2BottomLeft, x - Shadow2BottomLeft.getWidth(null), y + height, null); + gc.drawImage(Shadow2BottomRight, x + width, y + height, null); + gc.drawImage(Shadow2TopRight, x + width, y, null); + gc.drawImage(Shadow2TopLeft, x - Shadow2TopLeft.getWidth(null), y, null); + gc.drawImage(Shadow2Bottom, + x, y + height, x + width, y + height + Shadow2Bottom.getHeight(null), + 0, 0, Shadow2Bottom.getWidth(null), Shadow2Bottom.getHeight(null), null); + gc.drawImage(Shadow2Right, + x + width, y + Shadow2TopRight.getHeight(null), x + width + Shadow2Right.getWidth(null), y + height, + 0, 0, Shadow2Right.getWidth(null), Shadow2Right.getHeight(null), null); + gc.drawImage(Shadow2Left, + x - Shadow2Left.getWidth(null), y + Shadow2TopLeft.getHeight(null), x, y + height, + 0, 0, Shadow2Left.getWidth(null), Shadow2Left.getHeight(null), null); + } + + private static Image loadIcon(String name) { + InputStream inputStream = ShadowPainter.class.getResourceAsStream(name); + if (inputStream == null) { + throw new RuntimeException("Unable to load image for shadow: " + name); + } + try { + return ImageIO.read(inputStream); + } catch (IOException e) { + throw new RuntimeException("Unable to load image for shadow:" + name, e); + } finally { + try { + inputStream.close(); + } catch (IOException e) { + // ignore. + } + } + } + + // Shadow graphics. This was generated by creating a drop shadow in + // Gimp, using the parameters x offset=10, y offset=10, blur radius=10, + // (for the small drop shadows x offset=10, y offset=10, blur radius=10) + // color=black, and opacity=51. These values attempt to make a shadow + // that is legible both for dark and light themes, on top of the + // canvas background (rgb(150,150,150). Darker shadows would tend to + // blend into the foreground for a dark holo screen, and lighter shadows + // would be hard to spot on the canvas background. If you make adjustments, + // make sure to check the shadow with both dark and light themes. + // + // After making the graphics, I cut out the top right, bottom left + // and bottom right corners as 20x20 images, and these are reproduced by + // painting them in the corresponding places in the target graphics context. + // I then grabbed a single horizontal gradient line from the middle of the + // right edge,and a single vertical gradient line from the bottom. These + // are then painted scaled/stretched in the target to fill the gaps between + // the three corner images. + // + // Filenames: bl=bottom left, b=bottom, br=bottom right, r=right, tr=top right + + // Normal Drop Shadow + private static final Image ShadowBottom = loadIcon("/icons/shadow-b.png"); + private static final Image ShadowBottomLeft = loadIcon("/icons/shadow-bl.png"); + private static final Image ShadowBottomRight = loadIcon("/icons/shadow-br.png"); + private static final Image ShadowRight = loadIcon("/icons/shadow-r.png"); + private static final Image ShadowTopRight = loadIcon("/icons/shadow-tr.png"); + private static final Image ShadowTopLeft = loadIcon("/icons/shadow-tl.png"); + private static final Image ShadowLeft = loadIcon("/icons/shadow-l.png"); + + // Small Drop Shadow + private static final Image Shadow2Bottom = loadIcon("/icons/shadow2-b.png"); + private static final Image Shadow2BottomLeft = loadIcon("/icons/shadow2-bl.png"); + private static final Image Shadow2BottomRight = loadIcon("/icons/shadow2-br.png"); + private static final Image Shadow2Right = loadIcon("/icons/shadow2-r.png"); + private static final Image Shadow2TopRight = loadIcon("/icons/shadow2-tr.png"); + private static final Image Shadow2TopLeft = loadIcon("/icons/shadow2-tl.png"); + private static final Image Shadow2Left = loadIcon("/icons/shadow2-l.png"); +} diff --git a/tools/layoutlib/bridge/src/android/view/ViewGroup_Delegate.java b/tools/layoutlib/bridge/src/android/view/ViewGroup_Delegate.java new file mode 100644 index 0000000..a6c00f7 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/ViewGroup_Delegate.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.annotations.NonNull; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.resources.Density; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap_Delegate; +import android.graphics.Canvas; +import android.graphics.Outline; +import android.graphics.Path_Delegate; +import android.graphics.Rect; +import android.graphics.Region.Op; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.animation.Transformation; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; + +/** + * Delegate used to provide new implementation of a select few methods of {@link ViewGroup} + * <p/> + * Through the layoutlib_create tool, the original methods of ViewGroup have been replaced by calls + * to methods of the same name in this delegate class. + */ +public class ViewGroup_Delegate { + + /** + * Overrides the original drawChild call in ViewGroup to draw the shadow. + */ + @LayoutlibDelegate + /*package*/ static boolean drawChild(ViewGroup thisVG, Canvas canvas, View child, + long drawingTime) { + boolean retVal = thisVG.drawChild_Original(canvas, child, drawingTime); + if (child.getZ() > thisVG.getZ()) { + ViewOutlineProvider outlineProvider = child.getOutlineProvider(); + Outline outline = new Outline(); + outlineProvider.getOutline(child, outline); + + if (outline.mPath != null || (outline.mRect != null && !outline.mRect.isEmpty())) { + int restoreTo = transformCanvas(thisVG, canvas, child); + drawShadow(thisVG, canvas, child, outline); + canvas.restoreToCount(restoreTo); + } + } + return retVal; + } + + private static void drawShadow(ViewGroup parent, Canvas canvas, View child, + Outline outline) { + BufferedImage shadow = null; + int x = 0; + if (outline.mRect != null) { + Shadow s = getRectShadow(parent, canvas, child, outline); + shadow = s.mShadow; + x = -s.mShadowWidth; + } else if (outline.mPath != null) { + shadow = getPathShadow(child, outline, canvas); + } + if (shadow == null) { + return; + } + Bitmap bitmap = Bitmap_Delegate.createBitmap(shadow, false, + Density.getEnum(canvas.getDensity())); + Rect clipBounds = canvas.getClipBounds(); + Rect newBounds = new Rect(clipBounds); + newBounds.left = newBounds.left + x; + canvas.clipRect(newBounds, Op.REPLACE); + canvas.drawBitmap(bitmap, x, 0, null); + canvas.clipRect(clipBounds, Op.REPLACE); + } + + private static Shadow getRectShadow(ViewGroup parent, Canvas canvas, View child, + Outline outline) { + BufferedImage shadow; + Rect clipBounds = canvas.getClipBounds(); + if (clipBounds.isEmpty()) { + return null; + } + float height = child.getZ() - parent.getZ(); + // Draw large shadow if difference in z index is more than 10dp + float largeShadowThreshold = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10f, + getMetrics(child)); + boolean largeShadow = height > largeShadowThreshold; + int shadowSize = largeShadow ? ShadowPainter.SHADOW_SIZE : ShadowPainter.SMALL_SHADOW_SIZE; + shadow = new BufferedImage(clipBounds.width() + shadowSize, clipBounds.height(), + BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = shadow.createGraphics(); + Rect rect = outline.mRect; + if (largeShadow) { + ShadowPainter.drawRectangleShadow(graphics, + rect.left + shadowSize, rect.top, rect.width(), rect.height()); + } else { + ShadowPainter.drawSmallRectangleShadow(graphics, + rect.left + shadowSize, rect.top, rect.width(), rect.height()); + } + graphics.dispose(); + return new Shadow(shadow, shadowSize); + } + + @NonNull + private static DisplayMetrics getMetrics(View view) { + Context context = view.getContext(); + while (context instanceof ContextThemeWrapper) { + context = ((ContextThemeWrapper) context).getBaseContext(); + } + if (context instanceof BridgeContext) { + return ((BridgeContext) context).getMetrics(); + } + throw new RuntimeException("View " + view.getClass().getName() + " not created with the " + + "right context"); + } + + private static BufferedImage getPathShadow(View child, Outline outline, Canvas canvas) { + Rect clipBounds = canvas.getClipBounds(); + BufferedImage image = new BufferedImage(clipBounds.width(), clipBounds.height(), + BufferedImage.TYPE_INT_ARGB); + Graphics2D graphics = image.createGraphics(); + graphics.draw(Path_Delegate.getDelegate(outline.mPath.mNativePath).getJavaShape()); + graphics.dispose(); + return ShadowPainter.createDropShadow(image, ((int) child.getZ())); + } + + // Copied from android.view.View#draw(Canvas, ViewGroup, long) and removed code paths + // which were never taken. Ideally, we should hook up the shadow code in the same method so + // that we don't have to transform the canvas twice. + private static int transformCanvas(ViewGroup thisVG, Canvas canvas, View child) { + final int restoreTo = canvas.save(); + final boolean childHasIdentityMatrix = child.hasIdentityMatrix(); + int flags = thisVG.mGroupFlags; + Transformation transformToApply = null; + boolean concatMatrix = false; + if ((flags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) { + final Transformation t = thisVG.getChildTransformation(); + final boolean hasTransform = thisVG.getChildStaticTransformation(child, t); + if (hasTransform) { + final int transformType = t.getTransformationType(); + transformToApply = transformType != Transformation.TYPE_IDENTITY ? t : null; + concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0; + } + } + concatMatrix |= childHasIdentityMatrix; + + child.computeScroll(); + int sx = child.mScrollX; + int sy = child.mScrollY; + + canvas.translate(child.mLeft - sx, child.mTop - sy); + float alpha = child.getAlpha() * child.getTransitionAlpha(); + + if (transformToApply != null || alpha < 1 || !childHasIdentityMatrix) { + if (transformToApply != null || !childHasIdentityMatrix) { + int transX = -sx; + int transY = -sy; + + if (transformToApply != null) { + if (concatMatrix) { + // Undo the scroll translation, apply the transformation matrix, + // then redo the scroll translate to get the correct result. + canvas.translate(-transX, -transY); + canvas.concat(transformToApply.getMatrix()); + canvas.translate(transX, transY); + } + if (!childHasIdentityMatrix) { + canvas.translate(-transX, -transY); + canvas.concat(child.getMatrix()); + canvas.translate(transX, transY); + } + } + + } + } + return restoreTo; + } + + private static class Shadow { + public BufferedImage mShadow; + public int mShadowWidth; + + public Shadow(BufferedImage shadow, int shadowWidth) { + mShadow = shadow; + mShadowWidth = shadowWidth; + } + + } +} diff --git a/tools/layoutlib/bridge/src/android/view/WindowCallback.java b/tools/layoutlib/bridge/src/android/view/WindowCallback.java new file mode 100644 index 0000000..78242a8 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/view/WindowCallback.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.view.ActionMode.Callback; +import android.view.WindowManager.LayoutParams; +import android.view.accessibility.AccessibilityEvent; + +/** + * An empty implementation of {@link Window.Callback} that always returns null/false. + */ +public class WindowCallback implements Window.Callback { + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + return false; + } + + @Override + public boolean dispatchKeyShortcutEvent(KeyEvent event) { + return false; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + return false; + } + + @Override + public boolean dispatchTrackballEvent(MotionEvent event) { + return false; + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + return false; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + return false; + } + + @Override + public View onCreatePanelView(int featureId) { + return null; + } + + @Override + public boolean onCreatePanelMenu(int featureId, Menu menu) { + return false; + } + + @Override + public boolean onPreparePanel(int featureId, View view, Menu menu) { + return false; + } + + @Override + public boolean onMenuOpened(int featureId, Menu menu) { + return false; + } + + @Override + public boolean onMenuItemSelected(int featureId, MenuItem item) { + return false; + } + + @Override + public void onWindowAttributesChanged(LayoutParams attrs) { + + } + + @Override + public void onContentChanged() { + + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + + } + + @Override + public void onAttachedToWindow() { + + } + + @Override + public void onDetachedFromWindow() { + + } + + @Override + public void onPanelClosed(int featureId, Menu menu) { + + } + + @Override + public boolean onSearchRequested() { + return false; + } + + @Override + public ActionMode onWindowStartingActionMode(Callback callback) { + return null; + } + + @Override + public void onActionModeStarted(ActionMode mode) { + + } + + @Override + public void onActionModeFinished(ActionMode mode) { + + } +} diff --git a/tools/layoutlib/bridge/src/android/widget/TimePickerClockDelegate_Delegate.java b/tools/layoutlib/bridge/src/android/widget/TimePickerClockDelegate_Delegate.java new file mode 100644 index 0000000..1bd9830 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/widget/TimePickerClockDelegate_Delegate.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.widget; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.view.KeyEvent; + +/** + * Delegate used to provide new implementation of few methods in {@link TimePickerClockDelegate}. + */ +public class TimePickerClockDelegate_Delegate { + + // Copied from TimePickerClockDelegate. + private static final int AM = 0; + private static final int PM = 1; + + @LayoutlibDelegate + static int getAmOrPmKeyCode(TimePickerClockDelegate tpcd, int amOrPm) { + // We don't care about locales here. + if (amOrPm == AM) { + return KeyEvent.KEYCODE_A; + } else if (amOrPm == PM) { + return KeyEvent.KEYCODE_P; + } else { + assert false : "amOrPm value in TimePickerSpinnerDelegate can only be 0 or 1"; + return -1; + } + } +} diff --git a/tools/layoutlib/bridge/src/android/widget/Toolbar_Accessor.java b/tools/layoutlib/bridge/src/android/widget/Toolbar_Accessor.java new file mode 100644 index 0000000..fdd1779 --- /dev/null +++ b/tools/layoutlib/bridge/src/android/widget/Toolbar_Accessor.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.widget; + +import android.content.Context; + +/** + * To access non public members of classes in {@link Toolbar} + */ +public class Toolbar_Accessor { + public static ActionMenuPresenter getActionMenuPresenter(Toolbar toolbar) { + return toolbar.getOuterActionMenuPresenter(); + } + + public static Context getPopupContext(Toolbar toolbar) { + return toolbar.getPopupContext(); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java index 3d0e1e8..4d2c2fc 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java @@ -19,8 +19,10 @@ 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.annotations.NonNull; import com.android.ide.common.rendering.api.Capability; import com.android.ide.common.rendering.api.DrawableParams; +import com.android.ide.common.rendering.api.Features; import com.android.ide.common.rendering.api.LayoutLog; import com.android.ide.common.rendering.api.RenderSession; import com.android.ide.common.rendering.api.Result; @@ -35,10 +37,11 @@ import com.android.tools.layoutlib.create.MethodAdapter; import com.android.tools.layoutlib.create.OverrideMethod; import com.android.util.Pair; import com.ibm.icu.util.ULocale; +import libcore.io.MemoryMappedFile_Delegate; import android.content.res.BridgeAssetManager; import android.graphics.Bitmap; -import android.graphics.Typeface_Accessor; +import android.graphics.FontFamily_Delegate; import android.graphics.Typeface_Delegate; import android.os.Looper; import android.os.Looper_Accessor; @@ -178,7 +181,7 @@ public final class Bridge extends com.android.ide.common.rendering.api.Bridge { */ private static LayoutLog sCurrentLog = sDefaultLog; - private EnumSet<Capability> mCapabilities; + private static final int LAST_SUPPORTED_FEATURE = Features.PREFERENCES_RENDERING; @Override public int getApiLevel() { @@ -186,8 +189,16 @@ public final class Bridge extends com.android.ide.common.rendering.api.Bridge { } @Override + @Deprecated public EnumSet<Capability> getCapabilities() { - return mCapabilities; + // The Capability class is deprecated and frozen. All Capabilities enumerated there are + // supported by this version of LayoutLibrary. So, it's safe to use EnumSet.allOf() + return EnumSet.allOf(Capability.class); + } + + @Override + public boolean supports(int feature) { + return feature <= LAST_SUPPORTED_FEATURE; } @Override @@ -198,26 +209,6 @@ public final class Bridge extends com.android.ide.common.rendering.api.Bridge { 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, - Capability.RTL, - Capability.ACTION_BAR, - Capability.SIMULATE_PLATFORM); - - BridgeAssetManager.initSystem(); // When DEBUG_LAYOUT is set and is not 0 or false, setup a default listener @@ -250,7 +241,8 @@ public final class Bridge extends com.android.ide.common.rendering.api.Bridge { } // load the fonts. - Typeface_Delegate.setFontLocation(fontLocation.getAbsolutePath()); + FontFamily_Delegate.setFontLocation(fontLocation.getAbsolutePath()); + MemoryMappedFile_Delegate.setDataDir(fontLocation.getAbsoluteFile().getParentFile()); // 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. @@ -303,7 +295,7 @@ public final class Bridge extends com.android.ide.common.rendering.api.Bridge { BridgeAssetManager.clearSystem(); // dispose of the default typeface. - Typeface_Accessor.resetDefaults(); + Typeface_Delegate.resetDefaults(); return true; } @@ -459,7 +451,7 @@ public final class Bridge extends com.android.ide.common.rendering.api.Bridge { public static void setLog(LayoutLog log) { // check only the thread currently owning the lock can do this. - if (sLock.isHeldByCurrentThread() == false) { + if (!sLock.isHeldByCurrentThread()) { throw new IllegalStateException("scene must be acquired first. see #acquire(long)"); } @@ -489,7 +481,6 @@ public final class Bridge extends com.android.ide.common.rendering.api.Bridge { /** * Returns the name of a framework resource whose value is an int array. - * @param array */ public static String resolveResourceId(int[] array) { sIntArrayWrapper.set(array); @@ -502,6 +493,7 @@ public final class Bridge extends com.android.ide.common.rendering.api.Bridge { * @param name the name of the resource. * @return an {@link Integer} containing the resource id, or null if no resource were found. */ + @NonNull public static Integer getResourceId(ResourceType type, String name) { Map<String, Integer> map = sRevRMap.get(type); Integer value = null; @@ -509,11 +501,8 @@ public final class Bridge extends com.android.ide.common.rendering.api.Bridge { value = map.get(name); } - if (value == null) { - value = sDynamicIds.getId(type, name); - } + return value == null ? sDynamicIds.getId(type, name) : value; - return value; } /** 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 index 89288bf..e4cbb2f 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentProvider.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContentProvider.java @@ -90,7 +90,7 @@ public final class BridgeContentProvider implements IContentProvider { @Override public ParcelFileDescriptor openFile( - String callingPackage, Uri arg0, String arg1, ICancellationSignal signal) + String callingPackage, Uri arg0, String arg1, ICancellationSignal signal, IBinder token) throws RemoteException, FileNotFoundException { // TODO Auto-generated method stub return null; 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 index 04a52ea..3953624 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -16,6 +16,7 @@ package com.android.layoutlib.bridge.android; +import android.os.IBinder; import com.android.annotations.Nullable; import com.android.ide.common.rendering.api.ILayoutPullParser; import com.android.ide.common.rendering.api.IProjectCallback; @@ -52,7 +53,6 @@ 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; @@ -74,6 +74,7 @@ import android.view.DisplayAdjustments; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import android.view.textservice.TextServicesManager; import java.io.File; @@ -93,8 +94,15 @@ import java.util.Map; */ public final class BridgeContext extends Context { - private Resources mSystemResources; + /** The map adds cookies to each view so that IDE can link xml tags to views. */ private final HashMap<View, Object> mViewKeyMap = new HashMap<View, Object>(); + /** + * In some cases, when inflating an xml, some objects are created. Then later, the objects are + * converted to views. This map stores the mapping from objects to cookies which can then be + * used to populate the mViewKeyMap. + */ + private final HashMap<Object, Object> mViewKeyHelpMap = new HashMap<Object, Object>(); + private Resources mSystemResources; private final Object mProjectKey; private final DisplayMetrics mMetrics; private final RenderResources mRenderResources; @@ -115,19 +123,19 @@ public final class BridgeContext extends Context { private int mDynamicIdGenerator = 0x02030000; // Base id for R.style in custom namespace // cache for TypedArray generated from IStyleResourceValue object - private Map<int[], Map<Integer, TypedArray>> mTypedArrayCache; + private Map<int[], Map<Integer, BridgeTypedArray>> mTypedArrayCache; private BridgeInflater mBridgeInflater; private BridgeContentResolver mContentResolver; private final Stack<BridgeXmlBlockParser> mParserStack = new Stack<BridgeXmlBlockParser>(); + private SharedPreferences mSharedPreferences; - /** + /** * @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. */ @@ -191,6 +199,14 @@ public final class BridgeContext extends Context { return mViewKeyMap.get(view); } + public void addCookie(Object o, Object cookie) { + mViewKeyHelpMap.put(o, cookie); + } + + public Object getCookie(Object o) { + return mViewKeyHelpMap.get(o); + } + public Object getProjectKey() { return mProjectKey; } @@ -331,7 +347,7 @@ public final class BridgeContext extends Context { boolean attachToRoot, boolean skipCallbackParser) { boolean isPlatformLayout = resource.isFramework(); - if (isPlatformLayout == false && skipCallbackParser == false) { + if (!isPlatformLayout && !skipCallbackParser) { // check if the project callback can provide us with a custom parser. ILayoutPullParser parser = getParser(resource); @@ -462,12 +478,16 @@ public final class BridgeContext extends Context { return mDisplayManager; } + if (ACCESSIBILITY_SERVICE.equals(service)) { + return AccessibilityManager.getInstance(this); + } + throw new UnsupportedOperationException("Unsupported Service: " + service); } @Override - public final TypedArray obtainStyledAttributes(int[] attrs) { + public final BridgeTypedArray obtainStyledAttributes(int[] attrs) { // No style is specified here, so create the typed array based on the default theme // and the styles already applied to it. A null value of style indicates that the default // theme should be used. @@ -475,19 +495,30 @@ public final class BridgeContext extends Context { } @Override - public final TypedArray obtainStyledAttributes(int resid, int[] attrs) + public final BridgeTypedArray obtainStyledAttributes(int resid, int[] attrs) throws Resources.NotFoundException { + StyleResourceValue style = null; // get the StyleResourceValue based on the resId; - StyleResourceValue style = getStyleByDynamicId(resid); + if (resid != 0) { + style = getStyleByDynamicId(resid); + + if (style == null) { + // In some cases, style may not be a dynamic id, so we do a full search. + ResourceReference ref = resolveId(resid); + if (ref != null) { + style = mRenderResources.getStyle(ref.getName(), ref.isFramework()); + } + } - if (style == null) { - throw new Resources.NotFoundException(); + if (style == null) { + throw new Resources.NotFoundException(); + } } if (mTypedArrayCache == null) { - mTypedArrayCache = new HashMap<int[], Map<Integer,TypedArray>>(); + mTypedArrayCache = new HashMap<int[], Map<Integer,BridgeTypedArray>>(); - Map<Integer, TypedArray> map = new HashMap<Integer, TypedArray>(); + Map<Integer, BridgeTypedArray> map = new HashMap<Integer, BridgeTypedArray>(); mTypedArrayCache.put(attrs, map); BridgeTypedArray ta = createStyleBasedTypedArray(style, attrs); @@ -497,14 +528,14 @@ public final class BridgeContext extends Context { } // get the 2nd map - Map<Integer, TypedArray> map = mTypedArrayCache.get(attrs); + Map<Integer, BridgeTypedArray> map = mTypedArrayCache.get(attrs); if (map == null) { - map = new HashMap<Integer, TypedArray>(); + map = new HashMap<Integer, BridgeTypedArray>(); mTypedArrayCache.put(attrs, map); } // get the array from the 2nd map - TypedArray ta = map.get(resid); + BridgeTypedArray ta = map.get(resid); if (ta == null) { ta = createStyleBasedTypedArray(style, attrs); @@ -515,12 +546,12 @@ public final class BridgeContext extends Context { } @Override - public final TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs) { + public final BridgeTypedArray obtainStyledAttributes(AttributeSet set, int[] attrs) { return obtainStyledAttributes(set, attrs, 0, 0); } @Override - public TypedArray obtainStyledAttributes(AttributeSet set, int[] attrs, + public BridgeTypedArray obtainStyledAttributes(AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) { Map<String, String> defaultPropMap = null; @@ -663,7 +694,7 @@ public final class BridgeContext extends Context { } String attrName = attribute.getFirst(); - boolean frameworkAttr = attribute.getSecond().booleanValue(); + boolean frameworkAttr = attribute.getSecond(); String value = null; if (set != null) { value = set.getAttributeValue( @@ -672,7 +703,7 @@ public final class BridgeContext extends Context { // 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) { + if (!frameworkAttr && value == null) { value = set.getAttributeValue(BridgeConstants.NS_APP_RES_AUTO, attrName); } } @@ -789,13 +820,13 @@ public final class BridgeContext extends Context { 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]); + for (int attr : attrs) { + Pair<ResourceType, String> resolvedResource = Bridge.resolveResourceId(attr); boolean isFramework = false; if (resolvedResource != null) { isFramework = true; } else { - resolvedResource = mProjectCallback.resolveResourceId(attrs[i]); + resolvedResource = mProjectCallback.resolveResourceId(attr); } if (resolvedResource != null) { @@ -841,7 +872,7 @@ public final class BridgeContext extends Context { if (id == null) { // generate a new id - id = Integer.valueOf(++mDynamicIdGenerator); + id = ++mDynamicIdGenerator; // and add it to the maps. mDynamicIdToStyleMap.put(id, resValue); @@ -860,19 +891,24 @@ public final class BridgeContext extends Context { } public int getFrameworkResourceValue(ResourceType resType, String resName, int defValue) { - Integer value = Bridge.getResourceId(resType, resName); - if (value != null) { - return value.intValue(); + if (getRenderResources().getFrameworkResource(resType, resName) != null) { + // Bridge.getResourceId creates a new resource id if an existing one isn't found. So, + // we check for the existence of the resource before calling it. + return Bridge.getResourceId(resType, resName); } 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(); + // getResourceId creates a new resource id if an existing resource id isn't found. So, we + // check for the existence of the resource before calling it. + if (getRenderResources().getProjectResource(resType, resName) != null) { + if (mProjectCallback != null) { + Integer value = mProjectCallback.getResourceId(resType, resName); + if (value != null) { + return value; + } } } @@ -918,12 +954,24 @@ public final class BridgeContext extends Context { } @Override + public int checkPermission(String arg0, int arg1, int arg2, IBinder arg3) { + // pass + return 0; + } + + @Override public int checkUriPermission(Uri arg0, int arg1, int arg2, int arg3) { // pass return 0; } @Override + public int checkUriPermission(Uri arg0, int arg1, int arg2, int arg3, IBinder arg4) { + // pass + return 0; + } + + @Override public int checkUriPermission(Uri arg0, String arg1, String arg2, int arg3, int arg4, int arg5) { // pass @@ -1152,8 +1200,10 @@ public final class BridgeContext extends Context { @Override public SharedPreferences getSharedPreferences(String arg0, int arg1) { - // pass - return null; + if (mSharedPreferences == null) { + mSharedPreferences = new BridgeSharedPreferences(); + } + return mSharedPreferences; } @Override @@ -1455,9 +1505,6 @@ public final class BridgeContext extends Context { return null; } - /** - * @hide - */ @Override public int getUserId() { return 0; // not used 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 index 22265a3..39ebdfc 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java @@ -116,11 +116,6 @@ public class BridgePowerManager implements IPowerManager { } @Override - public void setMaximumScreenOffTimeoutFromDeviceAdmin(int arg0) throws RemoteException { - // pass for now. - } - - @Override public void setStayOnSetting(int arg0) throws RemoteException { // pass for now. } @@ -145,4 +140,9 @@ public class BridgePowerManager implements IPowerManager { public void wakeUp(long time) throws RemoteException { // pass for now. } + + @Override + public void boostScreenBrightness(long time) throws RemoteException { + // pass for now. + } } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeSharedPreferences.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeSharedPreferences.java new file mode 100644 index 0000000..132ff2f --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeSharedPreferences.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.SharedPreferences; + +import java.util.Map; +import java.util.Set; + +/** + * An empty shared preferences implementation which doesn't store anything. It always returns + * null, 0 or false. + */ +public class BridgeSharedPreferences implements SharedPreferences { + private Editor mEditor; + + @Override + public Map<String, ?> getAll() { + return null; + } + + @Override + public String getString(String key, String defValue) { + return null; + } + + @Override + public Set<String> getStringSet(String key, Set<String> defValues) { + return null; + } + + @Override + public int getInt(String key, int defValue) { + return 0; + } + + @Override + public long getLong(String key, long defValue) { + return 0; + } + + @Override + public float getFloat(String key, float defValue) { + return 0; + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + return false; + } + + @Override + public boolean contains(String key) { + return false; + } + + @Override + public Editor edit() { + if (mEditor != null) { + return mEditor; + } + mEditor = new Editor() { + @Override + public Editor putString(String key, String value) { + return null; + } + + @Override + public Editor putStringSet(String key, Set<String> values) { + return null; + } + + @Override + public Editor putInt(String key, int value) { + return null; + } + + @Override + public Editor putLong(String key, long value) { + return null; + } + + @Override + public Editor putFloat(String key, float value) { + return null; + } + + @Override + public Editor putBoolean(String key, boolean value) { + return null; + } + + @Override + public Editor remove(String key) { + return null; + } + + @Override + public Editor clear() { + return null; + } + + @Override + public boolean commit() { + return false; + } + + @Override + public void apply() { + } + }; + return mEditor; + } + + @Override + public void registerOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + } + + @Override + public void unregisterOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + } +} 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 index 997b199..4c4454d 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java @@ -95,6 +95,10 @@ public final class BridgeWindow implements IWindow { } @Override + public void dispatchWindowShown() { + } + + @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 index 0ed6ab1..0f51d00 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java @@ -38,7 +38,7 @@ import android.view.WindowManager.LayoutParams; public final class BridgeWindowSession implements IWindowSession { @Override - public int add(IWindow arg0, int seq, LayoutParams arg1, int arg2, Rect arg3, + public int add(IWindow arg0, int seq, LayoutParams arg1, int arg2, Rect arg3, Rect arg4, InputChannel outInputchannel) throws RemoteException { // pass for now. @@ -47,7 +47,7 @@ public final class BridgeWindowSession implements IWindowSession { @Override public int addToDisplay(IWindow arg0, int seq, LayoutParams arg1, int arg2, int displayId, - Rect arg3, InputChannel outInputchannel) + Rect arg3, Rect arg4, InputChannel outInputchannel) throws RemoteException { // pass for now. return 0; @@ -55,7 +55,7 @@ public final class BridgeWindowSession implements IWindowSession { @Override public int addWithoutInputChannel(IWindow arg0, int seq, LayoutParams arg1, int arg2, - Rect arg3) + Rect arg3, Rect arg4) throws RemoteException { // pass for now. return 0; @@ -63,7 +63,7 @@ public final class BridgeWindowSession implements IWindowSession { @Override public int addToDisplayWithoutInputChannel(IWindow arg0, int seq, LayoutParams arg1, int arg2, - int displayId, Rect arg3) + int displayId, Rect arg3, Rect arg4) throws RemoteException { // pass for now. return 0; diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/SessionParamsFlags.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/SessionParamsFlags.java new file mode 100644 index 0000000..e00ea6a --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/SessionParamsFlags.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.SessionParams; + +/** + * This contains all known keys for the {@link SessionParams#getFlag(SessionParams.Key)}. + * <p/> + * The IDE has its own copy of this class which may be newer or older than this one. + * <p/> + * Constants should never be modified or removed from this class. + */ +public final class SessionParamsFlags { + + public static final SessionParams.Key<String> FLAG_KEY_ROOT_TAG = + new SessionParams.Key<String>("rootTag", String.class); + + // Disallow instances. + private SessionParamsFlags() {} +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/ActionBarLayout.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/ActionBarLayout.java index d95c815..2ff8d37 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/ActionBarLayout.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/ActionBarLayout.java @@ -18,210 +18,116 @@ package com.android.layoutlib.bridge.bars; import com.android.annotations.NonNull; import com.android.annotations.Nullable; -import com.android.ide.common.rendering.api.ActionBarCallback; -import com.android.ide.common.rendering.api.ActionBarCallback.HomeButtonStyle; import com.android.ide.common.rendering.api.RenderResources; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.rendering.api.SessionParams; import com.android.internal.R; -import com.android.internal.app.WindowDecorActionBar; import com.android.internal.view.menu.MenuBuilder; import com.android.internal.view.menu.MenuItemImpl; -import com.android.internal.widget.ActionBarAccessor; -import com.android.internal.widget.ActionBarContainer; -import com.android.internal.widget.ActionBarView; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.impl.ResourceHelper; -import com.android.resources.ResourceType; -import android.app.ActionBar; -import android.app.ActionBar.Tab; -import android.app.ActionBar.TabListener; -import android.app.FragmentTransaction; import android.content.Context; import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; import android.util.DisplayMetrics; import android.util.TypedValue; -import android.view.Gravity; import android.view.LayoutInflater; -import android.view.MenuInflater; import android.view.View; +import android.view.View.MeasureSpec; import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.ActionMenuPresenter; import android.widget.FrameLayout; -import android.widget.LinearLayout; import android.widget.ListAdapter; import android.widget.ListView; import android.widget.RelativeLayout; import java.util.ArrayList; -/** - * A layout representing the action bar. - */ -public class ActionBarLayout extends LinearLayout { +public class ActionBarLayout { - // Store another reference to the context so that we don't have to cast it repeatedly. - @NonNull private final BridgeContext mBridgeContext; - @NonNull private final Context mThemedContext; - - @NonNull private final ActionBar mActionBar; - - // Data for Action Bar. - @Nullable private final String mIcon; - @Nullable private final String mTitle; - @Nullable private final String mSubTitle; - private final boolean mSplit; - private final boolean mShowHomeAsUp; - private final int mNavMode; - - // Helper fields. - @NonNull private final MenuBuilder mMenuBuilder; - private final int mPopupMaxWidth; - @NonNull private final RenderResources res; - @Nullable private final ActionBarView mActionBarView; - @Nullable private FrameLayout mContentRoot; - @NonNull private final ActionBarCallback mCallback; + private static final String LAYOUT_ATTR_NAME = "windowActionBarFullscreenDecorLayout"; - // A fake parent for measuring views. - @Nullable private ViewGroup mMeasureParent; + // The Action Bar + @NonNull + private CustomActionBarWrapper mActionBar; - public ActionBarLayout(@NonNull BridgeContext context, @NonNull SessionParams params) { + // Store another reference to the context so that we don't have to cast it repeatedly. + @NonNull + private final BridgeContext mBridgeContext; - super(context); - setOrientation(LinearLayout.HORIZONTAL); - setGravity(Gravity.CENTER_VERTICAL); + @NonNull + private FrameLayout mContentRoot; - // Inflate action bar layout. - LayoutInflater.from(context).inflate(R.layout.screen_action_bar, this, - true /*attachToRoot*/); - mActionBar = new WindowDecorActionBar(this); + // A fake parent for measuring views. + @Nullable + private ViewGroup mMeasureParent; - // Set contexts. - mBridgeContext = context; - mThemedContext = mActionBar.getThemedContext(); - - // Set data for action bar. - mCallback = params.getProjectCallback().getActionBarCallback(); - mIcon = params.getAppIcon(); - mTitle = params.getAppLabel(); - // Split Action Bar when the screen size is narrow and the application requests split action - // bar when narrow. - mSplit = context.getResources().getBoolean(R.bool.split_action_bar_is_narrow) && - mCallback.getSplitActionBarWhenNarrow(); - mNavMode = mCallback.getNavigationMode(); - // TODO: Support Navigation Drawer Indicator. - mShowHomeAsUp = mCallback.getHomeButtonStyle() == HomeButtonStyle.SHOW_HOME_AS_UP; - mSubTitle = mCallback.getSubTitle(); - - - // Set helper fields. - mMenuBuilder = new MenuBuilder(mThemedContext); - res = mBridgeContext.getRenderResources(); - mPopupMaxWidth = Math.max(mBridgeContext.getMetrics().widthPixels / 2, - mThemedContext.getResources().getDimensionPixelSize( - R.dimen.config_prefDialogWidth)); - mActionBarView = (ActionBarView) findViewById(R.id.action_bar); - mContentRoot = (FrameLayout) findViewById(android.R.id.content); - - setupActionBar(); - } + // A Layout that contains the inflated action bar. The menu popup is added to this layout. + @NonNull + private final RelativeLayout mEnclosingLayout; /** - * Sets up the action bar by filling the appropriate data. + * Inflate the action bar and attach it to {@code parentView} */ - private void setupActionBar() { - // Add title and sub title. - ResourceValue titleValue = res.findResValue(mTitle, false /*isFramework*/); - if (titleValue != null && titleValue.getValue() != null) { - mActionBar.setTitle(titleValue.getValue()); - } else { - mActionBar.setTitle(mTitle); - } - if (mSubTitle != null) { - mActionBar.setSubtitle(mSubTitle); - } + public ActionBarLayout(@NonNull BridgeContext context, @NonNull SessionParams params, + @NonNull ViewGroup parentView) { - // Add show home as up icon. - if (mShowHomeAsUp) { - mActionBar.setDisplayOptions(0xFF, ActionBar.DISPLAY_HOME_AS_UP); - } + mBridgeContext = context; - // Set the navigation mode. - mActionBar.setNavigationMode(mNavMode); - if (mNavMode == ActionBar.NAVIGATION_MODE_TABS) { - setupTabs(3); + ResourceValue layoutName = context.getRenderResources() + .findItemInTheme(LAYOUT_ATTR_NAME, true); + if (layoutName != null) { + // We may need to resolve the reference obtained. + layoutName = context.getRenderResources().findResValue(layoutName.getValue(), + layoutName.isFramework()); } - - if (mActionBarView != null) { - // If the action bar style doesn't specify an icon, set the icon obtained from the session - // params. - if (!mActionBarView.hasIcon() && mIcon != null) { - Drawable iconDrawable = getDrawable(mIcon, false /*isFramework*/); - if (iconDrawable != null) { - mActionBar.setIcon(iconDrawable); - } + int layoutId = 0; + String error = null; + if (layoutName == null) { + error = "Unable to find action bar layout (" + LAYOUT_ATTR_NAME + + ") in the current theme."; + } else { + layoutId = context.getFrameworkResourceValue(layoutName.getResourceType(), + layoutName.getName(), 0); + if (layoutId == 0) { + error = String.format("Unable to resolve attribute \"%s\" of type \"%s\"", + layoutName.getName(), layoutName.getResourceType()); } + } + if (layoutId == 0) { + throw new RuntimeException(error); + } + // Create a RelativeLayout to hold the action bar. The layout is needed so that we may + // add the menu popup to it. + mEnclosingLayout = new RelativeLayout(mBridgeContext); + setMatchParent(mEnclosingLayout); + parentView.addView(mEnclosingLayout); - // Set action bar to be split, if needed. - ActionBarContainer splitView = (ActionBarContainer) findViewById(R.id.split_action_bar); - mActionBarView.setSplitView(splitView); - mActionBarView.setSplitToolbar(mSplit); + // Inflate action bar layout. + View decorContent = LayoutInflater.from(context).inflate(layoutId, mEnclosingLayout, true); - inflateMenus(); - } - } + mActionBar = CustomActionBarWrapper.getActionBarWrapper(context, params, decorContent); - /** - * Gets the menus to add to the action bar from the callback, resolves them, inflates them and - * adds them to the action bar. - */ - private void inflateMenus() { - if (mActionBarView == null) { - return; - } - final MenuInflater inflater = new MenuInflater(mThemedContext); - for (String name : mCallback.getMenuIdNames()) { - if (mBridgeContext.getRenderResources().getProjectResource(ResourceType.MENU, name) - != null) { - int id = mBridgeContext.getProjectResourceValue(ResourceType.MENU, name, -1); - if (id > -1) { - inflater.inflate(id, mMenuBuilder); - } - } - } - mActionBarView.setMenu(mMenuBuilder, null /*callback*/); - } + FrameLayout contentRoot = (FrameLayout) mEnclosingLayout.findViewById(android.R.id.content); - // TODO: Use an adapter, like List View to set up tabs. - private void setupTabs(int num) { - for (int i = 1; i <= num; i++) { - Tab tab = mActionBar.newTab().setText("Tab" + i).setTabListener(new TabListener() { - @Override - public void onTabUnselected(Tab t, FragmentTransaction ft) { - // pass - } - @Override - public void onTabSelected(Tab t, FragmentTransaction ft) { - // pass - } - @Override - public void onTabReselected(Tab t, FragmentTransaction ft) { - // pass - } - }); - mActionBar.addTab(tab); + // If something went wrong and we were not able to initialize the content root, + // just add a frame layout inside this and return. + if (contentRoot == null) { + contentRoot = new FrameLayout(context); + setMatchParent(contentRoot); + mEnclosingLayout.addView(contentRoot); + mContentRoot = contentRoot; + } else { + mContentRoot = contentRoot; + mActionBar.setupActionBar(); + mActionBar.inflateMenus(); } } - @Nullable - private Drawable getDrawable(@NonNull String name, boolean isFramework) { - ResourceValue value = res.findResValue(name, isFramework); - value = res.resolveResValue(value); - if (value != null) { - return ResourceHelper.getDrawable(value, mBridgeContext); - } - return null; + private void setMatchParent(View view) { + view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT)); } /** @@ -229,73 +135,53 @@ public class ActionBarLayout extends LinearLayout { * the content frame which shall serve as the new content root. */ public void createMenuPopup() { - assert mContentRoot != null && findViewById(android.R.id.content) == mContentRoot + assert mEnclosingLayout.getChildCount() == 1 : "Action Bar Menus have already been created."; if (!isOverflowPopupNeeded()) { return; } - // Create a layout to hold the menus and the user's content. - RelativeLayout layout = new RelativeLayout(mThemedContext); - layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT)); - mContentRoot.addView(layout); - // Create a layout for the user's content. - FrameLayout contentRoot = new FrameLayout(mBridgeContext); - contentRoot.setLayoutParams(new LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - // Add contentRoot and menus to the layout. - layout.addView(contentRoot); - layout.addView(createMenuView()); - // ContentRoot is now the view we just created. - mContentRoot = contentRoot; - } - - /** - * Returns a {@link LinearLayout} containing the menu list view to be embedded in a - * {@link RelativeLayout} - */ - @NonNull - private View createMenuView() { DisplayMetrics metrics = mBridgeContext.getMetrics(); - OverflowMenuAdapter adapter = new OverflowMenuAdapter(mMenuBuilder, mThemedContext); + MenuBuilder menu = mActionBar.getMenuBuilder(); + OverflowMenuAdapter adapter = new OverflowMenuAdapter(menu, mActionBar.getPopupContext()); - LinearLayout layout = new LinearLayout(mThemedContext); + ListView listView = new ListView(mActionBar.getPopupContext(), null, + R.attr.dropDownListViewStyle); RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams( measureContentWidth(adapter), LayoutParams.WRAP_CONTENT); layoutParams.addRule(RelativeLayout.ALIGN_PARENT_END); - if (mSplit) { + if (mActionBar.isSplit()) { layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); - // TODO: Find correct value instead of hardcoded 10dp. - layoutParams.bottomMargin = getPixelValue("-10dp", metrics); + layoutParams.bottomMargin = getActionBarHeight() + mActionBar.getMenuPopupMargin(); } else { - layoutParams.topMargin = getPixelValue("-10dp", metrics); + layoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); + layoutParams.topMargin = getActionBarHeight() + mActionBar.getMenuPopupMargin(); } - layout.setLayoutParams(layoutParams); - final TypedArray a = mThemedContext.obtainStyledAttributes(null, + layoutParams.setMarginEnd(getPixelValue("5dp", metrics)); + listView.setLayoutParams(layoutParams); + listView.setAdapter(adapter); + final TypedArray a = mActionBar.getPopupContext().obtainStyledAttributes(null, R.styleable.PopupWindow, R.attr.popupMenuStyle, 0); - layout.setBackground(a.getDrawable(R.styleable.PopupWindow_popupBackground)); - layout.setDividerDrawable(a.getDrawable(R.attr.actionBarDivider)); + listView.setBackground(a.getDrawable(R.styleable.PopupWindow_popupBackground)); + listView.setDivider(a.getDrawable(R.attr.actionBarDivider)); a.recycle(); - layout.setOrientation(LinearLayout.VERTICAL); - layout.setDividerPadding(getPixelValue("12dp", metrics)); - layout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE); - - ListView listView = new ListView(mThemedContext, null, R.attr.dropDownListViewStyle); - listView.setAdapter(adapter); - layout.addView(listView); - return layout; + listView.setElevation(mActionBar.getMenuPopupElevation()); + mEnclosingLayout.addView(listView); } private boolean isOverflowPopupNeeded() { - boolean needed = mCallback.isOverflowPopupNeeded(); + boolean needed = mActionBar.isOverflowPopupNeeded(); if (!needed) { return false; } // Copied from android.widget.ActionMenuPresenter.updateMenuView() - ArrayList<MenuItemImpl> menus = mMenuBuilder.getNonActionItems(); - if (ActionBarAccessor.getActionMenuPresenter(mActionBarView).isOverflowReserved() && + ArrayList<MenuItemImpl> menus = mActionBar.getMenuBuilder().getNonActionItems(); + ActionMenuPresenter presenter = mActionBar.getActionMenuPresenter(); + if (presenter == null) { + throw new RuntimeException("Failed to create a Presenter for Action Bar Menus."); + } + if (presenter.isOverflowReserved() && menus != null) { final int count = menus.size(); if (count == 1) { @@ -307,7 +193,7 @@ public class ActionBarLayout extends LinearLayout { return needed; } - @Nullable + @NonNull public FrameLayout getContentRoot() { return mContentRoot; } @@ -319,6 +205,7 @@ public class ActionBarLayout extends LinearLayout { View itemView = null; int itemType = 0; + Context context = mActionBar.getPopupContext(); final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); final int count = adapter.getCount(); @@ -330,15 +217,17 @@ public class ActionBarLayout extends LinearLayout { } if (mMeasureParent == null) { - mMeasureParent = new FrameLayout(mThemedContext); + mMeasureParent = new FrameLayout(context); } itemView = adapter.getView(i, itemView, mMeasureParent); itemView.measure(widthMeasureSpec, heightMeasureSpec); final int itemWidth = itemView.getMeasuredWidth(); - if (itemWidth >= mPopupMaxWidth) { - return mPopupMaxWidth; + int popupMaxWidth = Math.max(mBridgeContext.getMetrics().widthPixels / 2, + context.getResources().getDimensionPixelSize(R.dimen.config_prefDialogWidth)); + if (itemWidth >= popupMaxWidth) { + return popupMaxWidth; } else if (itemWidth > maxWidth) { maxWidth = itemWidth; } @@ -347,9 +236,30 @@ public class ActionBarLayout extends LinearLayout { return maxWidth; } - private int getPixelValue(@NonNull String value, @NonNull DisplayMetrics metrics) { + static int getPixelValue(@NonNull String value, @NonNull DisplayMetrics metrics) { TypedValue typedValue = ResourceHelper.getValue(null, value, false /*requireUnit*/); return (int) typedValue.getDimension(metrics); } + // TODO: This is duplicated from RenderSessionImpl. + private int getActionBarHeight() { + RenderResources resources = mBridgeContext.getRenderResources(); + DisplayMetrics metrics = mBridgeContext.getMetrics(); + ResourceValue value = resources.findItemInTheme("actionBarSize", true); + + // resolve it + value = resources.resolveResValue(value); + + if (value != null) { + // get the numerical value, if available + TypedValue typedValue = ResourceHelper.getValue("actionBarSize", value.getValue(), + true); + if (typedValue != null) { + // compute the pixel value based on the display metrics + return (int) typedValue.getDimension(metrics); + + } + } + return 0; + } } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomActionBarWrapper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomActionBarWrapper.java new file mode 100644 index 0000000..6db722e --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomActionBarWrapper.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.rendering.api.ActionBarCallback; +import com.android.ide.common.rendering.api.ActionBarCallback.HomeButtonStyle; +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.SessionParams; +import com.android.internal.R; +import com.android.internal.app.ToolbarActionBar; +import com.android.internal.app.WindowDecorActionBar; +import com.android.internal.view.menu.MenuBuilder; +import com.android.internal.widget.ActionBarAccessor; +import com.android.internal.widget.ActionBarView; +import com.android.internal.widget.DecorToolbar; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.impl.ResourceHelper; + +import android.app.ActionBar; +import android.app.ActionBar.Tab; +import android.app.ActionBar.TabListener; +import android.app.FragmentTransaction; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowCallback; +import android.widget.ActionMenuPresenter; +import android.widget.Toolbar; +import android.widget.Toolbar_Accessor; + +import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX; +import static com.android.resources.ResourceType.MENU; + +/** + * A common API to access {@link ToolbarActionBar} and {@link WindowDecorActionBar}. + */ +public abstract class CustomActionBarWrapper { + + @NonNull protected ActionBar mActionBar; + @NonNull protected SessionParams mParams; + @NonNull protected ActionBarCallback mCallback; + @NonNull protected BridgeContext mContext; + + /** + * Returns a wrapper around different implementations of the Action Bar to provide a common API. + * + * @param decorContent the top level view returned by inflating + * ?attr/windowActionBarFullscreenDecorLayout + */ + @NonNull + public static CustomActionBarWrapper getActionBarWrapper(@NonNull BridgeContext context, + @NonNull SessionParams params, @NonNull View decorContent) { + View view = decorContent.findViewById(R.id.action_bar); + if (view instanceof Toolbar) { + return new ToolbarWrapper(context, params, ((Toolbar) view)); + } else if (view instanceof ActionBarView) { + return new WindowActionBarWrapper(context, params, decorContent, + ((ActionBarView) view)); + } else { + throw new IllegalStateException("Can't make an action bar out of " + + view.getClass().getSimpleName()); + } + } + + CustomActionBarWrapper(@NonNull BridgeContext context, @NonNull SessionParams params, + @NonNull ActionBar actionBar) { + mActionBar = actionBar; + mParams = params; + mCallback = params.getProjectCallback().getActionBarCallback(); + mContext = context; + } + + protected void setupActionBar() { + // Do the things that are common to all implementations. + RenderResources res = mContext.getRenderResources(); + + String title = mParams.getAppLabel(); + ResourceValue titleValue = res.findResValue(title, false); + if (titleValue != null && titleValue.getValue() != null) { + mActionBar.setTitle(titleValue.getValue()); + } else { + mActionBar.setTitle(title); + } + + String subTitle = mCallback.getSubTitle(); + if (subTitle != null) { + mActionBar.setSubtitle(subTitle); + } + + // Add show home as up icon. + if (mCallback.getHomeButtonStyle() == HomeButtonStyle.SHOW_HOME_AS_UP) { + mActionBar.setDisplayOptions(0xFF, ActionBar.DISPLAY_HOME_AS_UP); + } + } + + protected boolean isSplit() { + return getDecorToolbar().isSplit(); + } + + protected boolean isOverflowPopupNeeded() { + return mCallback.isOverflowPopupNeeded(); + } + + /** + * Gets the menus to add to the action bar from the callback, resolves them, inflates them and + * adds them to the action bar. + */ + protected void inflateMenus() { + MenuInflater inflater = new MenuInflater(getActionMenuContext()); + MenuBuilder menuBuilder = getMenuBuilder(); + for (String name : mCallback.getMenuIdNames()) { + int id; + if (name.startsWith(ANDROID_NS_NAME_PREFIX)) { + // Framework menu. + name = name.substring(ANDROID_NS_NAME_PREFIX.length()); + id = mContext.getFrameworkResourceValue(MENU, name, -1); + } else { + // Project menu. + id = mContext.getProjectResourceValue(MENU, name, -1); + } + if (id > -1) { + inflater.inflate(id, menuBuilder); + } + } + } + + /** + * The context used for the ActionBar and the menus in the ActionBarView. + */ + @NonNull + protected Context getActionMenuContext() { + return mActionBar.getThemedContext(); + } + + /** + * The context used to inflate the popup menu. + */ + @NonNull + abstract Context getPopupContext(); + + /** + * The Menu in which to inflate the user's menus. + */ + @NonNull + abstract MenuBuilder getMenuBuilder(); + + @Nullable + abstract ActionMenuPresenter getActionMenuPresenter(); + + /** + * Framework's wrapper over two ActionBar implementations. + */ + @NonNull + abstract DecorToolbar getDecorToolbar(); + + abstract int getMenuPopupElevation(); + + /** + * Margin between the menu popup and the action bar. + */ + abstract int getMenuPopupMargin(); + + // ---- The implementations ---- + + /** + * Material theme uses {@link Toolbar} as the action bar. This wrapper provides access to + * Toolbar using a common API. + */ + private static class ToolbarWrapper extends CustomActionBarWrapper { + + @NonNull + private final Toolbar mToolbar; // This is the view. + + ToolbarWrapper(@NonNull BridgeContext context, @NonNull SessionParams params, + @NonNull Toolbar toolbar) { + super(context, params, new ToolbarActionBar(toolbar, "", new WindowCallback()) + ); + mToolbar = toolbar; + } + + @Override + protected void inflateMenus() { + super.inflateMenus(); + // Inflating the menus doesn't initialize the ActionMenuPresenter. Setting a fake menu + // and then setting it back does the trick. + MenuBuilder menu = getMenuBuilder(); + DecorToolbar decorToolbar = getDecorToolbar(); + decorToolbar.setMenu(new MenuBuilder(getActionMenuContext()), null); + decorToolbar.setMenu(menu, null); + } + + @NonNull + @Override + Context getPopupContext() { + return Toolbar_Accessor.getPopupContext(mToolbar); + } + + @NonNull + @Override + MenuBuilder getMenuBuilder() { + return (MenuBuilder) mToolbar.getMenu(); + } + + @Nullable + @Override + ActionMenuPresenter getActionMenuPresenter() { + return Toolbar_Accessor.getActionMenuPresenter(mToolbar); + } + + @NonNull + @Override + DecorToolbar getDecorToolbar() { + return mToolbar.getWrapper(); + } + + @Override + int getMenuPopupElevation() { + return 10; + } + + @Override + int getMenuPopupMargin() { + return 0; + } + } + + /** + * Holo theme uses {@link WindowDecorActionBar} as the action bar. This wrapper provides + * access to it using a common API. + */ + private static class WindowActionBarWrapper extends CustomActionBarWrapper { + + @NonNull + private final WindowDecorActionBar mActionBar; + @NonNull + private final ActionBarView mActionBarView; + @NonNull + private final View mDecorContentRoot; + private MenuBuilder mMenuBuilder; + + public WindowActionBarWrapper(@NonNull BridgeContext context, @NonNull SessionParams params, + @NonNull View decorContentRoot, @NonNull ActionBarView actionBarView) { + super(context, params, new WindowDecorActionBar(decorContentRoot)); + mActionBarView = actionBarView; + mActionBar = ((WindowDecorActionBar) super.mActionBar); + mDecorContentRoot = decorContentRoot; + } + + @Override + protected void setupActionBar() { + super.setupActionBar(); + + // Set the navigation mode. + int navMode = mCallback.getNavigationMode(); + mActionBar.setNavigationMode(navMode); + //noinspection deprecation + if (navMode == ActionBar.NAVIGATION_MODE_TABS) { + setupTabs(3); + } + + String icon = mParams.getAppIcon(); + // If the action bar style doesn't specify an icon, set the icon obtained from the + // session params. + if (!mActionBar.hasIcon() && icon != null) { + Drawable iconDrawable = getDrawable(icon, false); + if (iconDrawable != null) { + mActionBar.setIcon(iconDrawable); + } + } + + // Set action bar to be split, if needed. + ViewGroup splitView = (ViewGroup) mDecorContentRoot.findViewById(R.id.split_action_bar); + if (splitView != null) { + mActionBarView.setSplitView(splitView); + Resources res = mContext.getResources(); + boolean split = res.getBoolean(R.bool.split_action_bar_is_narrow) + && mCallback.getSplitActionBarWhenNarrow(); + mActionBarView.setSplitToolbar(split); + } + } + + @Override + protected void inflateMenus() { + super.inflateMenus(); + // The super implementation doesn't set the menu on the view. Set it here. + mActionBarView.setMenu(getMenuBuilder(), null); + } + + @NonNull + @Override + Context getPopupContext() { + return getActionMenuContext(); + } + + @NonNull + @Override + MenuBuilder getMenuBuilder() { + if (mMenuBuilder == null) { + mMenuBuilder = new MenuBuilder(getActionMenuContext()); + } + return mMenuBuilder; + } + + @Nullable + @Override + ActionMenuPresenter getActionMenuPresenter() { + return ActionBarAccessor.getActionMenuPresenter(mActionBarView); + } + + @NonNull + @Override + ActionBarView getDecorToolbar() { + return mActionBarView; + } + + @Override + int getMenuPopupElevation() { + return 0; + } + + @Override + int getMenuPopupMargin() { + return -ActionBarLayout.getPixelValue("10dp", mContext.getMetrics()); + } + + // TODO: Use an adapter, like List View to set up tabs. + @SuppressWarnings("deprecation") // For Tab + private void setupTabs(int num) { + for (int i = 1; i <= num; i++) { + Tab tab = mActionBar.newTab().setText("Tab" + i).setTabListener(new TabListener() { + @Override + public void onTabUnselected(Tab t, FragmentTransaction ft) { + // pass + } + @Override + public void onTabSelected(Tab t, FragmentTransaction ft) { + // pass + } + @Override + public void onTabReselected(Tab t, FragmentTransaction ft) { + // pass + } + }); + mActionBar.addTab(tab); + } + } + + @Nullable + private Drawable getDrawable(@NonNull String name, boolean isFramework) { + RenderResources res = mContext.getRenderResources(); + ResourceValue value = res.findResValue(name, isFramework); + value = res.resolveResValue(value); + if (value != null) { + return ResourceHelper.getDrawable(value, mContext); + } + return null; + } + + } +} 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 index b677131..669e6b5 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java @@ -57,63 +57,59 @@ public class RenderDrawable extends RenderAction<DrawableParams> { 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(); - } + // 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); + // 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); + // 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); + // 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); + // 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); + // now do the layout. + content.layout(0, 0, w, h); - // preDraw setup - AttachInfo_Accessor.dispatchOnPreDraw(content); + // preDraw setup + AttachInfo_Accessor.dispatchOnPreDraw(content); - // draw into a new image - BufferedImage image = getImage(w, h); + // 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 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()); + // create a Canvas around the Android bitmap + Canvas canvas = new Canvas(bitmap); + canvas.setDensity(hardwareConfig.getDensity().getDpiValue()); - // and draw - content.draw(canvas); + // and draw + content.draw(canvas); - return Status.SUCCESS.createResult(image); - } catch (IOException e) { - return ERROR_UNKNOWN.createResult(e.getMessage(), e); - } + return Status.SUCCESS.createResult(image); } protected BufferedImage getImage(int w, int h) { 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 index b8dce70..daf82fc 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java @@ -49,6 +49,7 @@ 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.android.SessionParamsFlags; import com.android.layoutlib.bridge.bars.Config; import com.android.layoutlib.bridge.bars.NavigationBar; import com.android.layoutlib.bridge.bars.StatusBar; @@ -73,6 +74,7 @@ import android.graphics.Bitmap; import android.graphics.Bitmap_Delegate; import android.graphics.Canvas; import android.graphics.drawable.Drawable; +import android.preference.Preference_Delegate; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.AttachInfo_Accessor; @@ -87,7 +89,6 @@ import android.view.ViewGroup.LayoutParams; import android.view.ViewGroup.MarginLayoutParams; import android.view.ViewParent; import android.view.WindowManagerGlobal_Delegate; -import android.view.ViewParent; import android.widget.AbsListView; import android.widget.AbsSpinner; import android.widget.ActionMenuView; @@ -353,8 +354,7 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { // if the theme says no title/action bar, then the size will be 0 if (mActionBarSize > 0) { - ActionBarLayout actionBar = createActionBar(context, params); - backgroundLayout.addView(actionBar); + ActionBarLayout actionBar = createActionBar(context, params, backgroundLayout); actionBar.createMenuPopup(); mContentRoot = actionBar.getContentRoot(); } else if (mTitleBarSize > 0) { @@ -398,7 +398,15 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { // it can instantiate the custom Fragment. Fragment_Delegate.setProjectCallback(params.getProjectCallback()); - View view = mInflater.inflate(mBlockParser, mContentRoot); + String rootTag = params.getFlag(SessionParamsFlags.FLAG_KEY_ROOT_TAG); + boolean isPreference = "PreferenceScreen".equals(rootTag); + View view; + if (isPreference) { + view = Preference_Delegate.inflatePreference(getContext(), mBlockParser, + mContentRoot); + } else { + view = mInflater.inflate(mBlockParser, mContentRoot); + } // done with the parser, pop it. context.popParser(); @@ -409,7 +417,7 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { AttachInfo_Accessor.setAttachInfo(mViewRoot); // post-inflate process. For now this supports TabHost/TabWidget - postInflateProcess(view, params.getProjectCallback()); + postInflateProcess(view, params.getProjectCallback(), isPreference ? view : null); // get the background drawable if (mWindowBackground != null) { @@ -1211,12 +1219,16 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { * based on the content of the {@link FrameLayout}. * @param view the root view to process. * @param projectCallback callback to the project. + * @param skip the view and it's children are not processed. */ @SuppressWarnings("deprecation") // For the use of Pair - private void postInflateProcess(View view, IProjectCallback projectCallback) + private void postInflateProcess(View view, IProjectCallback projectCallback, View skip) throws PostInflateException { + if (view == skip) { + return; + } if (view instanceof TabHost) { - setupTabHost((TabHost)view, projectCallback); + setupTabHost((TabHost) view, projectCallback); } else if (view instanceof QuickContactBadge) { QuickContactBadge badge = (QuickContactBadge) view; badge.setImageToDefault(); @@ -1249,7 +1261,7 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { boolean skipCallbackParser = false; int count = binding.getHeaderCount(); - for (int i = 0 ; i < count ; i++) { + for (int i = 0; i < count; i++) { Pair<View, Boolean> pair = context.inflateView( binding.getHeaderAt(i), list, false /*attachToRoot*/, skipCallbackParser); @@ -1261,7 +1273,7 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { } count = binding.getFooterCount(); - for (int i = 0 ; i < count ; i++) { + for (int i = 0; i < count; i++) { Pair<View, Boolean> pair = context.inflateView( binding.getFooterAt(i), list, false /*attachToRoot*/, skipCallbackParser); @@ -1290,11 +1302,11 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { } } } else if (view instanceof ViewGroup) { - ViewGroup group = (ViewGroup)view; + ViewGroup group = (ViewGroup) view; final int count = group.getChildCount(); - for (int c = 0 ; c < count ; c++) { + for (int c = 0; c < count; c++) { View child = group.getChildAt(c); - postInflateProcess(child, projectCallback); + postInflateProcess(child, projectCallback, skip); } } } @@ -1362,6 +1374,7 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { for (int i = 0 ; i < count ; i++) { View child = content.getChildAt(i); String tabSpec = String.format("tab_spec%d", i+1); + @SuppressWarnings("ConstantConditions") // child cannot be null. int id = child.getId(); @SuppressWarnings("deprecation") Pair<ResourceType, String> resource = projectCallback.resolveResourceId(id); @@ -1624,11 +1637,9 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { /** * Creates the action bar. Also queries the project callback for missing information. */ - private ActionBarLayout createActionBar(BridgeContext context, SessionParams params) { - ActionBarLayout actionBar = new ActionBarLayout(context, params); - actionBar.setLayoutParams(new LinearLayout.LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - return actionBar; + private ActionBarLayout createActionBar(BridgeContext context, SessionParams params, + ViewGroup parentView) { + return new ActionBarLayout(context, params, parentView); } public BufferedImage getImage() { 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 index 22f8e1c..677c744 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/ResourceHelper.java @@ -32,6 +32,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.content.res.ColorStateList; +import android.content.res.Resources.Theme; import android.graphics.Bitmap; import android.graphics.Bitmap_Delegate; import android.graphics.NinePatch_Delegate; @@ -166,6 +167,17 @@ public final class ResourceHelper { * @param context the current context */ public static Drawable getDrawable(ResourceValue value, BridgeContext context) { + return getDrawable(value, context, 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 + * @param theme the theme to be used to inflate the drawable. + */ + public static Drawable getDrawable(ResourceValue value, BridgeContext context, Theme theme) { if (value == null) { return null; } @@ -209,7 +221,7 @@ public final class ResourceHelper { BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( parser, context, value.isFramework()); try { - return Drawable.createFromXml(context.getResources(), blockParser); + return Drawable.createFromXml(context.getResources(), blockParser, theme); } finally { blockParser.ensurePopped(); } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/libcore/io/BridgeBufferIterator.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/libcore/io/BridgeBufferIterator.java new file mode 100644 index 0000000..7e361a1 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/libcore/io/BridgeBufferIterator.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.libcore.io; + +import java.nio.ByteBuffer; + +import libcore.io.BufferIterator; + +/** + * Provides an implementation of {@link BufferIterator} over a {@link ByteBuffer}. + */ +public class BridgeBufferIterator extends BufferIterator { + + private final long mSize; + private final ByteBuffer mByteBuffer; + + public BridgeBufferIterator(long size, ByteBuffer buffer) { + mSize = size; + mByteBuffer = buffer; + } + + @Override + public void seek(int offset) { + assert offset <= mSize; + mByteBuffer.position(offset); + } + + @Override + public void skip(int byteCount) { + int newPosition = mByteBuffer.position() + byteCount; + assert newPosition <= mSize; + mByteBuffer.position(newPosition); + } + + @Override + public void readByteArray(byte[] dst, int dstOffset, int byteCount) { + assert dst.length >= dstOffset + byteCount; + mByteBuffer.get(dst, dstOffset, byteCount); + } + + @Override + public byte readByte() { + return mByteBuffer.get(); + } + + @Override + public int readInt() { + return mByteBuffer.getInt(); + } + + @Override + public void readIntArray(int[] dst, int dstOffset, int intCount) { + while (--intCount >= 0) { + dst[dstOffset++] = mByteBuffer.getInt(); + } + } + + @Override + public short readShort() { + return mByteBuffer.getShort(); + } +} 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 index a1fae95..979aa33 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/DynamicIdMap.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/DynamicIdMap.java @@ -16,6 +16,7 @@ package com.android.layoutlib.bridge.util; +import com.android.annotations.NonNull; import com.android.resources.ResourceType; import com.android.util.Pair; @@ -48,6 +49,7 @@ public class DynamicIdMap { * @param name the name of the resource * @return an integer. */ + @NonNull public Integer getId(ResourceType type, String name) { return getId(Pair.of(type, name)); } @@ -59,10 +61,11 @@ public class DynamicIdMap { * @param resource the type/name of the resource * @return an integer. */ + @NonNull public Integer getId(Pair<ResourceType, String> resource) { Integer value = mDynamicIds.get(resource); if (value == null) { - value = Integer.valueOf(++mDynamicSeed); + value = ++mDynamicSeed; mDynamicIds.put(resource, value); mRevDynamicIds.put(value, resource); } diff --git a/tools/layoutlib/bridge/src/libcore/io/MemoryMappedFile_Delegate.java b/tools/layoutlib/bridge/src/libcore/io/MemoryMappedFile_Delegate.java new file mode 100644 index 0000000..723d5c4 --- /dev/null +++ b/tools/layoutlib/bridge/src/libcore/io/MemoryMappedFile_Delegate.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.io; + +import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.layoutlib.bridge.libcore.io.BridgeBufferIterator; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import android.system.ErrnoException; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel.MapMode; +import java.util.HashMap; +import java.util.Map; + +/** + * Delegate used to provide alternate implementation of select methods of {@link MemoryMappedFile}. + */ +public class MemoryMappedFile_Delegate { + + private static final DelegateManager<MemoryMappedFile_Delegate> sManager = new + DelegateManager<MemoryMappedFile_Delegate>(MemoryMappedFile_Delegate.class); + + private static final Map<MemoryMappedFile, Long> sMemoryMappedFileMap = + new HashMap<MemoryMappedFile, Long>(); + + private final MappedByteBuffer mMappedByteBuffer; + private final long mSize; + + /** Path on the target device where the data file is available. */ + private static final String TARGET_PATH = System.getenv("ANDROID_ROOT") + "/usr/share/zoneinfo"; + /** Path on the host (inside the SDK) where the data files are available. */ + private static File sRootPath; + + @LayoutlibDelegate + static MemoryMappedFile mmapRO(String path) throws ErrnoException { + if (!path.startsWith(TARGET_PATH)) { + throw new ErrnoException("Custom timezone data files are not supported.", 1); + } + if (sRootPath == null) { + throw new ErrnoException("Bridge has not been initialized properly.", 1); + } + path = path.substring(TARGET_PATH.length()); + try { + File f = new File(sRootPath, path); + if (!f.exists()) { + throw new ErrnoException("File not found: " + f.getPath(), 1); + } + RandomAccessFile file = new RandomAccessFile(f, "r"); + try { + long size = file.length(); + MemoryMappedFile_Delegate newDelegate = new MemoryMappedFile_Delegate(file); + long filePointer = file.getFilePointer(); + MemoryMappedFile mmFile = new MemoryMappedFile(filePointer, size); + long delegateIndex = sManager.addNewDelegate(newDelegate); + sMemoryMappedFileMap.put(mmFile, delegateIndex); + return mmFile; + } finally { + file.close(); + } + } catch (IOException e) { + throw new ErrnoException("mmapRO", 1, e); + } + } + + @LayoutlibDelegate + static void close(MemoryMappedFile thisFile) throws ErrnoException { + Long index = sMemoryMappedFileMap.get(thisFile); + if (index != null) { + sMemoryMappedFileMap.remove(thisFile); + sManager.removeJavaReferenceFor(index); + } + } + + @LayoutlibDelegate + static BufferIterator bigEndianIterator(MemoryMappedFile file) { + MemoryMappedFile_Delegate delegate = getDelegate(file); + return new BridgeBufferIterator(delegate.mSize, delegate.mMappedByteBuffer.duplicate()); + } + + // TODO: implement littleEndianIterator() + + public MemoryMappedFile_Delegate(RandomAccessFile file) throws IOException { + mSize = file.length(); + // It's weird that map() takes size as long, but returns MappedByteBuffer which uses an int + // to store the marker to the position. + mMappedByteBuffer = file.getChannel().map(MapMode.READ_ONLY, 0, mSize); + assert mMappedByteBuffer.order() == ByteOrder.BIG_ENDIAN; + } + + public static void setDataDir(File path) { + sRootPath = path; + } + + private static MemoryMappedFile_Delegate getDelegate(MemoryMappedFile file) { + Long index = sMemoryMappedFileMap.get(file); + return index == null ? null : sManager.getDelegate(index); + } + +} diff --git a/tools/layoutlib/bridge/src/libcore/util/ZoneInfo_WallTime_Delegate.java b/tools/layoutlib/bridge/src/libcore/util/ZoneInfo_WallTime_Delegate.java new file mode 100644 index 0000000..f29c5c0 --- /dev/null +++ b/tools/layoutlib/bridge/src/libcore/util/ZoneInfo_WallTime_Delegate.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.util; + +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; + +import java.util.GregorianCalendar; + +/** + * Delegate used to provide alternate implementation of select methods in {@link ZoneInfo.WallTime} + */ +public class ZoneInfo_WallTime_Delegate { + + @LayoutlibDelegate + static GregorianCalendar createGregorianCalendar() { + return new GregorianCalendar(); + } +} diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png Binary files differnew file mode 100644 index 0000000..943cdf1 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/activity.png diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/layout.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/layout.xml index 2704c07..b8ec5661 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/layout.xml +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/layout.xml @@ -8,4 +8,10 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Some text"/> + <DatePicker + android:layout_width="100dp" + android:layout_height="100dp"/> + <CalendarView + android:layout_width="100dp" + android:layout_height="100dp"/> </LinearLayout>
\ No newline at end of file diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java new file mode 100644 index 0000000..e13ad72 --- /dev/null +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/ImageUtils.java @@ -0,0 +1,336 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.intensive; + +import com.android.annotations.NonNull; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; + +import static java.awt.RenderingHints.*; +import static java.awt.image.BufferedImage.TYPE_INT_ARGB; +import static java.io.File.separatorChar; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +// Adapted by taking the relevant pieces of code from the following classes: +// +// com.android.tools.idea.rendering.ImageUtils, +// com.android.tools.idea.tests.gui.framework.fixture.layout.ImageFixture and +// com.android.tools.idea.rendering.RenderTestBase +/** + * Utilities related to image processing. + */ +public class ImageUtils { + /** + * Normally, this test will fail when there is a missing thumbnail. However, when + * you create creating a new test, it's useful to be able to turn this off such that + * you can generate all the missing thumbnails in one go, rather than having to run + * the test repeatedly to get to each new render assertion generating its thumbnail. + */ + private static final boolean FAIL_ON_MISSING_THUMBNAIL = true; + + private static final int THUMBNAIL_SIZE = 250; + + private static final double MAX_PERCENT_DIFFERENCE = 0.1; + + public static void requireSimilar(@NonNull String relativePath, @NonNull BufferedImage image) + throws IOException { + int maxDimension = Math.max(image.getWidth(), image.getHeight()); + double scale = THUMBNAIL_SIZE / (double)maxDimension; + BufferedImage thumbnail = scale(image, scale, scale); + + InputStream is = ImageUtils.class.getResourceAsStream(relativePath); + if (is == null) { + String message = "Unable to load golden thumbnail: " + relativePath + "\n"; + message = saveImageAndAppendMessage(thumbnail, message, relativePath); + if (FAIL_ON_MISSING_THUMBNAIL) { + fail(message); + } else { + System.out.println(message); + } + } + else { + BufferedImage goldenImage = ImageIO.read(is); + assertImageSimilar(relativePath, goldenImage, thumbnail, MAX_PERCENT_DIFFERENCE); + } + } + + public static void assertImageSimilar(String relativePath, BufferedImage goldenImage, + BufferedImage image, double maxPercentDifferent) throws IOException { + assertEquals("Only TYPE_INT_ARGB image types are supported", TYPE_INT_ARGB, image.getType()); + + if (goldenImage.getType() != TYPE_INT_ARGB) { + BufferedImage temp = new BufferedImage(goldenImage.getWidth(), goldenImage.getHeight(), + TYPE_INT_ARGB); + temp.getGraphics().drawImage(goldenImage, 0, 0, null); + goldenImage = temp; + } + assertEquals(TYPE_INT_ARGB, goldenImage.getType()); + + int imageWidth = Math.min(goldenImage.getWidth(), image.getWidth()); + int imageHeight = Math.min(goldenImage.getHeight(), image.getHeight()); + + // Blur the images to account for the scenarios where there are pixel + // differences + // in where a sharp edge occurs + // goldenImage = blur(goldenImage, 6); + // image = blur(image, 6); + + int width = 3 * imageWidth; + @SuppressWarnings("UnnecessaryLocalVariable") + int height = imageHeight; // makes code more readable + BufferedImage deltaImage = new BufferedImage(width, height, TYPE_INT_ARGB); + Graphics g = deltaImage.getGraphics(); + + // Compute delta map + long delta = 0; + for (int y = 0; y < imageHeight; y++) { + for (int x = 0; x < imageWidth; x++) { + int goldenRgb = goldenImage.getRGB(x, y); + int rgb = image.getRGB(x, y); + if (goldenRgb == rgb) { + deltaImage.setRGB(imageWidth + x, y, 0x00808080); + continue; + } + + // If the pixels have no opacity, don't delta colors at all + if (((goldenRgb & 0xFF000000) == 0) && (rgb & 0xFF000000) == 0) { + deltaImage.setRGB(imageWidth + x, y, 0x00808080); + continue; + } + + int deltaR = ((rgb & 0xFF0000) >>> 16) - ((goldenRgb & 0xFF0000) >>> 16); + int newR = 128 + deltaR & 0xFF; + int deltaG = ((rgb & 0x00FF00) >>> 8) - ((goldenRgb & 0x00FF00) >>> 8); + int newG = 128 + deltaG & 0xFF; + int deltaB = (rgb & 0x0000FF) - (goldenRgb & 0x0000FF); + int newB = 128 + deltaB & 0xFF; + + int avgAlpha = ((((goldenRgb & 0xFF000000) >>> 24) + + ((rgb & 0xFF000000) >>> 24)) / 2) << 24; + + int newRGB = avgAlpha | newR << 16 | newG << 8 | newB; + deltaImage.setRGB(imageWidth + x, y, newRGB); + + delta += Math.abs(deltaR); + delta += Math.abs(deltaG); + delta += Math.abs(deltaB); + } + } + + // 3 different colors, 256 color levels + long total = imageHeight * imageWidth * 3L * 256L; + float percentDifference = (float) (delta * 100 / (double) total); + + String error = null; + String imageName = getName(relativePath); + if (percentDifference > maxPercentDifferent) { + error = String.format("Images differ (by %.1f%%)", percentDifference); + } else if (Math.abs(goldenImage.getWidth() - image.getWidth()) >= 2) { + error = "Widths differ too much for " + imageName + ": " + + goldenImage.getWidth() + "x" + goldenImage.getHeight() + + "vs" + image.getWidth() + "x" + image.getHeight(); + } else if (Math.abs(goldenImage.getHeight() - image.getHeight()) >= 2) { + error = "Heights differ too much for " + imageName + ": " + + goldenImage.getWidth() + "x" + goldenImage.getHeight() + + "vs" + image.getWidth() + "x" + image.getHeight(); + } + + assertEquals(TYPE_INT_ARGB, image.getType()); + if (error != null) { + // Expected on the left + // Golden on the right + g.drawImage(goldenImage, 0, 0, null); + g.drawImage(image, 2 * imageWidth, 0, null); + + // Labels + if (imageWidth > 80) { + g.setColor(Color.RED); + g.drawString("Expected", 10, 20); + g.drawString("Actual", 2 * imageWidth + 10, 20); + } + + File output = new File(getTempDir(), "delta-" + imageName); + if (output.exists()) { + boolean deleted = output.delete(); + assertTrue(deleted); + } + ImageIO.write(deltaImage, "PNG", output); + error += " - see details in " + output.getPath() + "\n"; + error = saveImageAndAppendMessage(image, error, relativePath); + System.out.println(error); + fail(error); + } + + g.dispose(); + } + + /** + * Resize the given image + * + * @param source the image to be scaled + * @param xScale x scale + * @param yScale y scale + * @return the scaled image + */ + @NonNull + public static BufferedImage scale(@NonNull BufferedImage source, double xScale, double yScale) { + + int sourceWidth = source.getWidth(); + int sourceHeight = source.getHeight(); + int destWidth = Math.max(1, (int) (xScale * sourceWidth)); + int destHeight = Math.max(1, (int) (yScale * sourceHeight)); + int imageType = source.getType(); + if (imageType == BufferedImage.TYPE_CUSTOM) { + imageType = BufferedImage.TYPE_INT_ARGB; + } + if (xScale > 0.5 && yScale > 0.5) { + BufferedImage scaled = + new BufferedImage(destWidth, destHeight, imageType); + Graphics2D g2 = scaled.createGraphics(); + g2.setComposite(AlphaComposite.Src); + g2.setColor(new Color(0, true)); + g2.fillRect(0, 0, destWidth, destHeight); + if (xScale == 1 && yScale == 1) { + g2.drawImage(source, 0, 0, null); + } else { + setRenderingHints(g2); + g2.drawImage(source, 0, 0, destWidth, destHeight, 0, 0, sourceWidth, sourceHeight, + null); + } + g2.dispose(); + return scaled; + } else { + // When creating a thumbnail, using the above code doesn't work very well; + // you get some visible artifacts, especially for text. Instead use the + // technique of repeatedly scaling the image into half; this will cause + // proper averaging of neighboring pixels, and will typically (for the kinds + // of screen sizes used by this utility method in the layout editor) take + // about 3-4 iterations to get the result since we are logarithmically reducing + // the size. Besides, each successive pass in operating on much fewer pixels + // (a reduction of 4 in each pass). + // + // However, we may not be resizing to a size that can be reached exactly by + // successively diving in half. Therefore, once we're within a factor of 2 of + // the final size, we can do a resize to the exact target size. + // However, we can get even better results if we perform this final resize + // up front. Let's say we're going from width 1000 to a destination width of 85. + // The first approach would cause a resize from 1000 to 500 to 250 to 125, and + // then a resize from 125 to 85. That last resize can distort/blur a lot. + // Instead, we can start with the destination width, 85, and double it + // successfully until we're close to the initial size: 85, then 170, + // then 340, and finally 680. (The next one, 1360, is larger than 1000). + // So, now we *start* the thumbnail operation by resizing from width 1000 to + // width 680, which will preserve a lot of visual details such as text. + // Then we can successively resize the image in half, 680 to 340 to 170 to 85. + // We end up with the expected final size, but we've been doing an exact + // divide-in-half resizing operation at the end so there is less distortion. + + int iterations = 0; // Number of halving operations to perform after the initial resize + int nearestWidth = destWidth; // Width closest to source width that = 2^x, x is integer + int nearestHeight = destHeight; + while (nearestWidth < sourceWidth / 2) { + nearestWidth *= 2; + nearestHeight *= 2; + iterations++; + } + + BufferedImage scaled = new BufferedImage(nearestWidth, nearestHeight, imageType); + + Graphics2D g2 = scaled.createGraphics(); + setRenderingHints(g2); + g2.drawImage(source, 0, 0, nearestWidth, nearestHeight, 0, 0, sourceWidth, sourceHeight, + null); + g2.dispose(); + + sourceWidth = nearestWidth; + sourceHeight = nearestHeight; + source = scaled; + + for (int iteration = iterations - 1; iteration >= 0; iteration--) { + int halfWidth = sourceWidth / 2; + int halfHeight = sourceHeight / 2; + scaled = new BufferedImage(halfWidth, halfHeight, imageType); + g2 = scaled.createGraphics(); + setRenderingHints(g2); + g2.drawImage(source, 0, 0, halfWidth, halfHeight, 0, 0, sourceWidth, sourceHeight, + null); + g2.dispose(); + + sourceWidth = halfWidth; + sourceHeight = halfHeight; + source = scaled; + iterations--; + } + return scaled; + } + } + + private static void setRenderingHints(@NonNull Graphics2D g2) { + g2.setRenderingHint(KEY_INTERPOLATION,VALUE_INTERPOLATION_BILINEAR); + g2.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY); + g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); + } + + /** + * Temp directory where to write the thumbnails and deltas. + */ + @NonNull + private static File getTempDir() { + if (System.getProperty("os.name").equals("Mac OS X")) { + return new File("/tmp"); //$NON-NLS-1$ + } + + return new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$ + } + + /** + * Saves the generated thumbnail image and appends the info message to an initial message + */ + @NonNull + private static String saveImageAndAppendMessage(@NonNull BufferedImage image, + @NonNull String initialMessage, @NonNull String relativePath) throws IOException { + File output = new File(getTempDir(), getName(relativePath)); + if (output.exists()) { + boolean deleted = output.delete(); + assertTrue(deleted); + } + ImageIO.write(image, "PNG", output); + initialMessage += "Thumbnail for current rendering stored at " + output.getPath(); +// initialMessage += "\nRun the following command to accept the changes:\n"; +// initialMessage += String.format("mv %1$s %2$s", output.getPath(), +// ImageUtils.class.getResource(relativePath).getPath()); + // The above has been commented out, since the destination path returned is in out dir + // and it makes the tests pass without the code being actually checked in. + return initialMessage; + } + + private static String getName(@NonNull String relativePath) { + return relativePath.substring(relativePath.lastIndexOf(separatorChar) + 1); + } +} diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java index a2588a6..a86fcdd 100644 --- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java @@ -39,6 +39,7 @@ import org.junit.Test; import java.io.File; import java.io.FileFilter; +import java.io.IOException; import java.net.URL; import java.util.Arrays; import java.util.Comparator; @@ -75,7 +76,10 @@ public class Main { private static final String PLATFORM_DIR; private static final String TEST_RES_DIR; - private static final String APP_TEST_RES = "/testApp/MyApplication/src/main/res"; + /** Location of the app to test inside {@link #TEST_RES_DIR}*/ + private static final String APP_TEST_DIR = "/testApp/MyApplication"; + /** Location of the app's res dir inside {@link #TEST_RES_DIR}*/ + private static final String APP_TEST_RES = APP_TEST_DIR + "/src/main/res"; private LayoutLog mLayoutLibLog; private FrameworkResources mFrameworkRepo; @@ -161,11 +165,27 @@ public class Main { if (!out.isDirectory()) { return null; } - File sdkDir = new File(out, "sdk" + File.separator + "sdk"); + File sdkDir = new File(out, "sdk"); if (!sdkDir.isDirectory()) { - // The directory we thought that should contain the sdk is not a directory. return null; } + File[] sdkDirs = sdkDir.listFiles(new FileFilter() { + @Override + public boolean accept(File path) { + // We need to search for $TARGET_PRODUCT (usually, sdk_phone_armv7) + return path.isDirectory() && path.getName().startsWith("sdk"); + } + }); + for (File dir : sdkDirs) { + String platformDir = getPlatformDirFromHostOutSdkSdk(dir); + if (platformDir != null) { + return platformDir; + } + } + return null; + } + + private static String getPlatformDirFromHostOutSdkSdk(File sdkDir) { File[] possibleSdks = sdkDir.listFiles(new FileFilter() { @Override public boolean accept(File path) { @@ -280,6 +300,12 @@ public class Main { getLogger().error(session.getResult().getException(), session.getResult().getErrorMessage()); } + try { + String goldenImagePath = APP_TEST_DIR + "/golden/activity.png"; + ImageUtils.requireSimilar(goldenImagePath, session.getImage()); + } catch (IOException e) { + getLogger().error(e, e.getMessage()); + } } /** 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 index 83fac85..8f50c5d 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java @@ -20,7 +20,9 @@ import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import com.android.tools.layoutlib.java.AutoCloseable; import com.android.tools.layoutlib.java.Charsets; import com.android.tools.layoutlib.java.IntegralToString; +import com.android.tools.layoutlib.java.LinkedHashMap_Delegate; import com.android.tools.layoutlib.java.Objects; +import com.android.tools.layoutlib.java.System_Delegate; import com.android.tools.layoutlib.java.UnsafeByteSequence; import java.util.Arrays; @@ -131,25 +133,28 @@ public final class CreateInfo implements ICreateInfo { IntegralToString.class, UnsafeByteSequence.class, Charsets.class, + System_Delegate.class, + LinkedHashMap_Delegate.class, }; /** * The list of methods to rewrite as delegates. */ public final static String[] DELEGATE_METHODS = new String[] { + "android.animation.AnimatorInflater#loadAnimator", // TODO: remove when Path.approximate() is supported. "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.Resources$Theme#resolveAttributes", "android.content.res.AssetManager#newTheme", "android.content.res.AssetManager#deleteTheme", - "android.content.res.AssetManager#applyThemeStyle", "android.content.res.TypedArray#getValueAt", "android.content.res.TypedArray#obtain", "android.graphics.BitmapFactory#finishDecode", "android.graphics.Typeface#getSystemFontConfigLocation", "android.os.Handler#sendMessageAtTime", "android.os.HandlerThread#run", + "android.preference.Preference#getView", "android.text.format.DateFormat#is24HourFormat", "android.util.Xml#newPullParser", "android.view.Choreographer#getRefreshRate", @@ -162,10 +167,20 @@ public final class CreateInfo implements ICreateInfo { "android.view.WindowManagerGlobal#getWindowManagerService", "android.view.inputmethod.InputMethodManager#getInstance", "android.view.MenuInflater#registerMenu", + "android.view.RenderNode#nCreate", + "android.view.RenderNode#nDestroyRenderNode", + "android.view.RenderNode#nSetElevation", + "android.view.RenderNode#nGetElevation", + "android.view.ViewGroup#drawChild", + "android.widget.TimePickerClockDelegate#getAmOrPmKeyCode", "com.android.internal.view.menu.MenuBuilder#createNewMenuItem", "com.android.internal.util.XmlUtils#convertValueToInt", "com.android.internal.textservice.ITextServicesManager$Stub#asInterface", - "dalvik.system.VMRuntime#newUnpaddedArray" + "dalvik.system.VMRuntime#newUnpaddedArray", + "libcore.io.MemoryMappedFile#mmapRO", + "libcore.io.MemoryMappedFile#close", + "libcore.io.MemoryMappedFile#bigEndianIterator", + "libcore.util.ZoneInfo$WallTime#createGregorianCalendar", }; /** @@ -215,7 +230,6 @@ public final class CreateInfo implements ICreateInfo { "android.os.SystemProperties", "android.text.AndroidBidi", "android.text.StaticLayout", - "android.text.format.Time", "android.view.Display", "libcore.icu.DateIntervalFormat", "libcore.icu.ICU", @@ -255,10 +269,12 @@ public final class CreateInfo implements ICreateInfo { "java.nio.charset.Charsets", "com.android.tools.layoutlib.java.Charsets", "java.lang.IntegralToString", "com.android.tools.layoutlib.java.IntegralToString", "java.lang.UnsafeByteSequence", "com.android.tools.layoutlib.java.UnsafeByteSequence", + "java.nio.charset.StandardCharsets", "com.android.tools.layoutlib.java.Charsets", }; private final static String[] EXCLUDED_CLASSES = new String[] { + "android.preference.PreferenceActivity", "org.kxml2.io.KXmlParser" }; 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 index 3d89c68..ae4a57d 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java @@ -112,6 +112,7 @@ public class DelegateClassAdapter extends ClassVisitor { // The implementation of this 'delegate' method is done in layoutlib_bridge. int accessDelegate = access; + access = access & ~Opcodes.ACC_PRIVATE; // If private, make it package protected. MethodVisitor mwOriginal = super.visitMethod(access, name + ORIGINAL_SUFFIX, desc, signature, exceptions); 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 index cd3c39e..fa570c8 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java @@ -108,6 +108,7 @@ public class Main { "android.graphics.drawable.*", "android.content.*", "android.content.res.*", + "android.preference.*", "org.apache.harmony.xml.*", "com.android.internal.R**", "android.pim.*", // for datepicker diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java index 9c6fbac..384d8ca 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java @@ -16,6 +16,9 @@ package com.android.tools.layoutlib.create; +import com.android.tools.layoutlib.java.LinkedHashMap_Delegate; +import com.android.tools.layoutlib.java.System_Delegate; + import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; @@ -24,8 +27,10 @@ import org.objectweb.asm.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; /** @@ -42,36 +47,38 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { "([CI[CII)V", "([BI[BII)V", "([SI[SII)V", "([II[III)V", "([JI[JII)V", "([FI[FII)V", "([DI[DII)V", "([ZI[ZII)V")); - private static final List<MethodReplacer> METHOD_REPLACERS = new ArrayList<MethodReplacer>(2); + private static final List<MethodReplacer> METHOD_REPLACERS = new ArrayList<MethodReplacer>(5); private static final String ANDROID_LOCALE_CLASS = "com/android/layoutlib/bridge/android/AndroidLocale"; - private static final String JAVA_LOCALE_CLASS = "java/util/Locale"; + private static final String JAVA_LOCALE_CLASS = Type.getInternalName(java.util.Locale.class); private static final Type STRING = Type.getType(String.class); + private static final String JAVA_LANG_SYSTEM = Type.getInternalName(System.class); + // Static initialization block to initialize METHOD_REPLACERS. static { // Case 1: java.lang.System.arraycopy() METHOD_REPLACERS.add(new MethodReplacer() { @Override public boolean isNeeded(String owner, String name, String desc) { - return "java/lang/System".equals(owner) && "arraycopy".equals(name) && + return JAVA_LANG_SYSTEM.equals(owner) && "arraycopy".equals(name) && ARRAYCOPY_DESCRIPTORS.contains(desc); } @Override - public void replace(int[] opcode, String[] methodInformation) { - assert methodInformation.length == 3 && isNeeded(methodInformation[0], methodInformation[1], methodInformation[2]) - && opcode.length == 1; - methodInformation[2] = "(Ljava/lang/Object;ILjava/lang/Object;II)V"; + public void replace(MethodInformation mi) { + assert isNeeded(mi.owner, mi.name, mi.desc); + mi.desc = "(Ljava/lang/Object;ILjava/lang/Object;II)V"; } }); // Case 2: java.util.Locale.toLanguageTag() and java.util.Locale.getScript() METHOD_REPLACERS.add(new MethodReplacer() { - String LOCALE_TO_STRING = Type.getMethodDescriptor(STRING, Type.getType(Locale.class)); + private final String LOCALE_TO_STRING = + Type.getMethodDescriptor(STRING, Type.getType(Locale.class)); @Override public boolean isNeeded(String owner, String name, String desc) { @@ -80,12 +87,11 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { } @Override - public void replace(int[] opcode, String[] methodInformation) { - assert methodInformation.length == 3 && isNeeded(methodInformation[0], methodInformation[1], methodInformation[2]) - && opcode.length == 1; - opcode[0] = Opcodes.INVOKESTATIC; - methodInformation[0] = ANDROID_LOCALE_CLASS; - methodInformation[2] = LOCALE_TO_STRING; + public void replace(MethodInformation mi) { + assert isNeeded(mi.owner, mi.name, mi.desc); + mi.opcode = Opcodes.INVOKESTATIC; + mi.owner = ANDROID_LOCALE_CLASS; + mi.desc = LOCALE_TO_STRING; } }); @@ -104,10 +110,51 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { } @Override - public void replace(int[] opcode, String[] methodInformation) { - assert methodInformation.length == 3 && isNeeded(methodInformation[0], methodInformation[1], methodInformation[2]) - && opcode.length == 1; - methodInformation[0] = ANDROID_LOCALE_CLASS; + public void replace(MethodInformation mi) { + assert isNeeded(mi.owner, mi.name, mi.desc); + mi.owner = ANDROID_LOCALE_CLASS; + } + }); + + // Case 4: java.lang.System.log?() + METHOD_REPLACERS.add(new MethodReplacer() { + @Override + public boolean isNeeded(String owner, String name, String desc) { + return JAVA_LANG_SYSTEM.equals(owner) && name.length() == 4 + && name.startsWith("log"); + } + + @Override + public void replace(MethodInformation mi) { + assert isNeeded(mi.owner, mi.name, mi.desc); + assert mi.desc.equals("(Ljava/lang/String;Ljava/lang/Throwable;)V") + || mi.desc.equals("(Ljava/lang/String;)V"); + mi.name = "log"; + mi.owner = Type.getInternalName(System_Delegate.class); + } + }); + + // Case 5: java.util.LinkedHashMap.eldest() + METHOD_REPLACERS.add(new MethodReplacer() { + + private final String VOID_TO_MAP_ENTRY = + Type.getMethodDescriptor(Type.getType(Map.Entry.class)); + private final String LINKED_HASH_MAP = Type.getInternalName(LinkedHashMap.class); + + @Override + public boolean isNeeded(String owner, String name, String desc) { + return LINKED_HASH_MAP.equals(owner) && + "eldest".equals(name) && + VOID_TO_MAP_ENTRY.equals(desc); + } + + @Override + public void replace(MethodInformation mi) { + assert isNeeded(mi.owner, mi.name, mi.desc); + mi.opcode = Opcodes.INVOKESTATIC; + mi.owner = Type.getInternalName(LinkedHashMap_Delegate.class); + mi.desc = Type.getMethodDescriptor( + Type.getType(Map.Entry.class), Type.getType(LinkedHashMap.class)); } }); } @@ -141,13 +188,12 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { public void visitMethodInsn(int opcode, String owner, String name, String desc) { for (MethodReplacer replacer : METHOD_REPLACERS) { if (replacer.isNeeded(owner, name, desc)) { - String[] methodInformation = {owner, name, desc}; - int[] opcodeOut = {opcode}; - replacer.replace(opcodeOut, methodInformation); - opcode = opcodeOut[0]; - owner = methodInformation[0]; - name = methodInformation[1]; - desc = methodInformation[2]; + MethodInformation mi = new MethodInformation(opcode, owner, name, desc); + replacer.replace(mi); + opcode = mi.opcode; + owner = mi.owner; + name = mi.name; + desc = mi.desc; break; } } @@ -155,19 +201,28 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { } } + private static class MethodInformation { + public int opcode; + public String owner; + public String name; + public String desc; + + public MethodInformation(int opcode, String owner, String name, String desc) { + this.opcode = opcode; + this.owner = owner; + this.name = name; + this.desc = desc; + } + } + private interface MethodReplacer { public boolean isNeeded(String owner, String name, String desc); /** - * This method must update the arrays with the new values of the method attributes - + * Updates the MethodInformation with the new values of the method attributes - * opcode, owner, name and desc. - * @param opcode This array should contain the original value of the opcode. The value is - * modified by the method if needed. The size of the array must be 1. * - * @param methodInformation This array should contain the original values of the method - * attributes - owner, name and desc in that order. The values - * may be modified as needed. The size of the array must be 3. */ - public void replace(int[] opcode, String[] methodInformation); + public void replace(MethodInformation mi); } } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/java/LinkedHashMap_Delegate.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/java/LinkedHashMap_Delegate.java new file mode 100644 index 0000000..59cc75f --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/java/LinkedHashMap_Delegate.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.java; + +import com.android.tools.layoutlib.create.ReplaceMethodCallsAdapter; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Provides alternate implementation to java.util.LinkedHashMap#eldest(), which is present as a + * non-public method in the Android VM, but not present on the host VM. This is injected in the + * layoutlib using {@link ReplaceMethodCallsAdapter}. + */ +public class LinkedHashMap_Delegate { + public static <K,V> Map.Entry<K,V> eldest(LinkedHashMap<K,V> map) { + Iterator<Entry<K, V>> iterator = map.entrySet().iterator(); + return iterator.hasNext() ? iterator.next() : null; + } +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/java/System_Delegate.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/java/System_Delegate.java new file mode 100644 index 0000000..613c8d9 --- /dev/null +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/java/System_Delegate.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.java; + +import com.android.tools.layoutlib.create.ReplaceMethodCallsAdapter; + +/** + * Provides dummy implementation of methods that don't exist on the host VM. + * + * @see ReplaceMethodCallsAdapter + */ +public class System_Delegate { + public static void log(String message) { + // ignore. + } + + public static void log(String message, Throwable th) { + // ignore. + } +} diff --git a/tools/split-select/Abi.cpp b/tools/split-select/Abi.cpp new file mode 100644 index 0000000..180dd8f --- /dev/null +++ b/tools/split-select/Abi.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Abi.h" + +using namespace android; + +namespace split { +namespace abi { + +static Vector<Variant> buildVariants(Variant v1, Variant v2) { + Vector<Variant> v; + v.add(v1); + v.add(v2); + return v; +} + +static Vector<Variant> buildVariants(Variant v1, Variant v2, Variant v3) { + Vector<Variant> v; + v.add(v1); + v.add(v2); + v.add(v3); + return v; +} + +static const Vector<Variant> sNoneVariants; +static const Vector<Variant> sArmVariants = buildVariants(Variant_armeabi, Variant_armeabi_v7a, Variant_arm64_v8a); +static const Vector<Variant> sIntelVariants = buildVariants(Variant_x86, Variant_x86_64); +static const Vector<Variant> sMipsVariants = buildVariants(Variant_mips, Variant_mips64); + +Family getFamily(Variant variant) { + switch (variant) { + case Variant_none: + return Family_none; + case Variant_armeabi: + case Variant_armeabi_v7a: + case Variant_arm64_v8a: + return Family_arm; + case Variant_x86: + case Variant_x86_64: + return Family_intel; + case Variant_mips: + case Variant_mips64: + return Family_mips; + } + return Family_none; +} + +const Vector<Variant>& getVariants(Family family) { + switch (family) { + case Family_none: + return sNoneVariants; + case Family_arm: + return sArmVariants; + case Family_intel: + return sIntelVariants; + case Family_mips: + return sMipsVariants; + } + return sNoneVariants; +} + +const char* toString(Variant variant) { + switch (variant) { + case Variant_none: + return ""; + case Variant_armeabi: + return "armeabi"; + case Variant_armeabi_v7a: + return "armeabi-v7a"; + case Variant_arm64_v8a: + return "arm64-v8a"; + case Variant_x86: + return "x86"; + case Variant_x86_64: + return "x86_64"; + case Variant_mips: + return "mips"; + case Variant_mips64: + return "mips64"; + } + return ""; +} + +} // namespace abi +} // namespace split diff --git a/tools/split-select/Abi.h b/tools/split-select/Abi.h new file mode 100644 index 0000000..85b4d62 --- /dev/null +++ b/tools/split-select/Abi.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 H_ANDROID_SPLIT_ABI +#define H_ANDROID_SPLIT_ABI + +#include <utils/Vector.h> + +namespace split { +namespace abi { + +enum Variant { + Variant_none = 0, + Variant_armeabi, + Variant_armeabi_v7a, + Variant_arm64_v8a, + Variant_x86, + Variant_x86_64, + Variant_mips, + Variant_mips64, +}; + +enum Family { + Family_none, + Family_arm, + Family_intel, + Family_mips, +}; + +Family getFamily(Variant variant); +const android::Vector<Variant>& getVariants(Family family); +const char* toString(Variant variant); + +} // namespace abi +} // namespace split + +#endif // H_ANDROID_SPLIT_ABI diff --git a/tools/split-select/Android.mk b/tools/split-select/Android.mk new file mode 100644 index 0000000..968d22b --- /dev/null +++ b/tools/split-select/Android.mk @@ -0,0 +1,115 @@ +# +# Copyright (C) 2014 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# This tool is prebuilt if we're doing an app-only build. +ifeq ($(TARGET_BUILD_APPS)$(filter true,$(TARGET_BUILD_PDK)),) + +# ========================================================== +# Setup some common variables for the different build +# targets here. +# ========================================================== +LOCAL_PATH:= $(call my-dir) + +main := Main.cpp +sources := \ + Abi.cpp \ + Grouper.cpp \ + Rule.cpp \ + RuleGenerator.cpp \ + SplitDescription.cpp + +testSources := \ + Grouper_test.cpp \ + Rule_test.cpp \ + RuleGenerator_test.cpp \ + TestRules.cpp + +cIncludes := \ + external/zlib \ + frameworks/base/tools + +hostLdLibs := +hostStaticLibs := \ + libaapt \ + libandroidfw \ + libpng \ + liblog \ + libutils \ + libcutils \ + libexpat \ + libziparchive-host + +cFlags := -Wall -Werror + +ifeq ($(HOST_OS),linux) + hostLdLibs += -lrt -ldl -lpthread +endif + +# Statically link libz for MinGW (Win SDK under Linux), +# and dynamically link for all others. +ifneq ($(strip $(USE_MINGW)),) + hostStaticLibs += libz +else + hostLdLibs += -lz +endif + + +# ========================================================== +# Build the host static library: libsplit-select +# ========================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := libsplit-select + +LOCAL_SRC_FILES := $(sources) + +LOCAL_C_INCLUDES += $(cIncludes) +LOCAL_CFLAGS += $(cFlags) -D_DARWIN_UNLIMITED_STREAMS + +include $(BUILD_HOST_STATIC_LIBRARY) + + +# ========================================================== +# Build the host tests: libsplit-select_tests +# ========================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := libsplit-select_tests +LOCAL_MODULE_TAGS := tests + +LOCAL_SRC_FILES := $(testSources) + +LOCAL_C_INCLUDES += $(cIncludes) +LOCAL_STATIC_LIBRARIES += libsplit-select $(hostStaticLibs) +LOCAL_LDLIBS += $(hostLdLibs) +LOCAL_CFLAGS += $(cFlags) + +include $(BUILD_HOST_NATIVE_TEST) + +# ========================================================== +# Build the host executable: split-select +# ========================================================== +include $(CLEAR_VARS) +LOCAL_MODULE := split-select + +LOCAL_SRC_FILES := $(main) + +LOCAL_C_INCLUDES += $(cIncludes) +LOCAL_STATIC_LIBRARIES += libsplit-select $(hostStaticLibs) +LOCAL_LDLIBS += $(hostLdLibs) +LOCAL_CFLAGS += $(cFlags) + +include $(BUILD_HOST_EXECUTABLE) + +endif # No TARGET_BUILD_APPS or TARGET_BUILD_PDK diff --git a/tools/split-select/Grouper.cpp b/tools/split-select/Grouper.cpp new file mode 100644 index 0000000..22685cd --- /dev/null +++ b/tools/split-select/Grouper.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Grouper.h" + +#include "aapt/AaptUtil.h" +#include "SplitDescription.h" + +#include <utils/KeyedVector.h> +#include <utils/Vector.h> + +using namespace android; +using AaptUtil::appendValue; + +namespace split { + +Vector<SortedVector<SplitDescription> > +groupByMutualExclusivity(const Vector<SplitDescription>& splits) { + Vector<SortedVector<SplitDescription> > groups; + + // Find mutually exclusive splits and group them. + KeyedVector<SplitDescription, SortedVector<SplitDescription> > densityGroups; + KeyedVector<SplitDescription, SortedVector<SplitDescription> > abiGroups; + KeyedVector<SplitDescription, SortedVector<SplitDescription> > localeGroups; + const size_t splitCount = splits.size(); + for (size_t i = 0; i < splitCount; i++) { + const SplitDescription& split = splits[i]; + if (split.config.density != 0) { + SplitDescription key(split); + key.config.density = 0; + key.config.sdkVersion = 0; // Ignore density so we can support anydpi. + appendValue(densityGroups, key, split); + } else if (split.abi != abi::Variant_none) { + SplitDescription key(split); + key.abi = abi::Variant_none; + appendValue(abiGroups, key, split); + } else if (split.config.locale != 0) { + SplitDescription key(split); + key.config.clearLocale(); + appendValue(localeGroups, key, split); + } else { + groups.add(); + groups.editTop().add(split); + } + } + + const size_t densityCount = densityGroups.size(); + for (size_t i = 0; i < densityCount; i++) { + groups.add(densityGroups[i]); + } + + const size_t abiCount = abiGroups.size(); + for (size_t i = 0; i < abiCount; i++) { + groups.add(abiGroups[i]); + } + + const size_t localeCount = localeGroups.size(); + for (size_t i = 0; i < localeCount; i++) { + groups.add(localeGroups[i]); + } + return groups; +} + +} // namespace split diff --git a/tools/layoutlib/bridge/src/android/graphics/Typeface_Accessor.java b/tools/split-select/Grouper.h index adad2ac..5cb0b5b 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Typeface_Accessor.java +++ b/tools/split-select/Grouper.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011 The Android Open Source Project + * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,19 @@ * limitations under the License. */ -package android.graphics; +#ifndef H_ANDROID_SPLIT_GROUPER +#define H_ANDROID_SPLIT_GROUPER -/** - * Class allowing access to package-protected methods/fields. - */ -public class Typeface_Accessor { +#include "SplitDescription.h" + +#include <utils/SortedVector.h> +#include <utils/Vector.h> + +namespace split { + +android::Vector<android::SortedVector<SplitDescription> > +groupByMutualExclusivity(const android::Vector<SplitDescription>& splits); + +} // namespace split - public static void resetDefaults() { - Typeface.sDefaults = null; - } -} +#endif // H_ANDROID_SPLIT_GROUPER diff --git a/tools/split-select/Grouper_test.cpp b/tools/split-select/Grouper_test.cpp new file mode 100644 index 0000000..a5f9c5a --- /dev/null +++ b/tools/split-select/Grouper_test.cpp @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Grouper.h" + +#include "SplitDescription.h" + +#include <gtest/gtest.h> +#include <utils/String8.h> +#include <utils/Vector.h> + +using namespace android; + +namespace split { + +class GrouperTest : public ::testing::Test { +protected: + virtual void SetUp() { + Vector<SplitDescription> splits; + addSplit(splits, "en-rUS-sw600dp-hdpi"); + addSplit(splits, "fr-rFR-sw600dp-hdpi"); + addSplit(splits, "fr-rFR-sw600dp-xhdpi"); + addSplit(splits, ":armeabi"); + addSplit(splits, "en-rUS-sw300dp-xhdpi"); + addSplit(splits, "large"); + addSplit(splits, "pl-rPL"); + addSplit(splits, "xlarge"); + addSplit(splits, "en-rUS-sw600dp-xhdpi"); + addSplit(splits, "en-rUS-sw300dp-hdpi"); + addSplit(splits, "xxhdpi"); + addSplit(splits, "hdpi"); + addSplit(splits, "de-rDE"); + addSplit(splits, "xhdpi"); + addSplit(splits, ":x86"); + addSplit(splits, "anydpi"); + addSplit(splits, "v7"); + addSplit(splits, "v8"); + addSplit(splits, "sw600dp"); + addSplit(splits, "sw300dp"); + mGroups = groupByMutualExclusivity(splits); + } + + void addSplit(Vector<SplitDescription>& splits, const char* str); + void expectHasGroupWithSplits(const char* a); + void expectHasGroupWithSplits(const char* a, const char* b); + void expectHasGroupWithSplits(const char* a, const char* b, const char* c); + void expectHasGroupWithSplits(const char* a, const char* b, const char* c, const char* d); + void expectHasGroupWithSplits(const Vector<const char*>& expectedStrs); + + Vector<SortedVector<SplitDescription> > mGroups; +}; + +TEST_F(GrouperTest, shouldHaveCorrectNumberOfGroups) { + EXPECT_EQ(12u, mGroups.size()); +} + +TEST_F(GrouperTest, shouldGroupDensities) { + expectHasGroupWithSplits("en-rUS-sw300dp-hdpi", "en-rUS-sw300dp-xhdpi"); + expectHasGroupWithSplits("en-rUS-sw600dp-hdpi", "en-rUS-sw600dp-xhdpi"); + expectHasGroupWithSplits("fr-rFR-sw600dp-hdpi", "fr-rFR-sw600dp-xhdpi"); + expectHasGroupWithSplits("hdpi", "xhdpi", "xxhdpi", "anydpi"); +} + +TEST_F(GrouperTest, shouldGroupAbi) { + expectHasGroupWithSplits(":armeabi", ":x86"); +} + +TEST_F(GrouperTest, shouldGroupLocale) { + expectHasGroupWithSplits("pl-rPL", "de-rDE"); +} + +TEST_F(GrouperTest, shouldGroupEachSplitIntoItsOwnGroup) { + expectHasGroupWithSplits("large"); + expectHasGroupWithSplits("xlarge"); + expectHasGroupWithSplits("v7"); + expectHasGroupWithSplits("v8"); + expectHasGroupWithSplits("sw600dp"); + expectHasGroupWithSplits("sw300dp"); +} + +// +// Helper methods +// + +void GrouperTest::expectHasGroupWithSplits(const char* a) { + Vector<const char*> expected; + expected.add(a); + expectHasGroupWithSplits(expected); +} + +void GrouperTest::expectHasGroupWithSplits(const char* a, const char* b) { + Vector<const char*> expected; + expected.add(a); + expected.add(b); + expectHasGroupWithSplits(expected); +} + +void GrouperTest::expectHasGroupWithSplits(const char* a, const char* b, const char* c) { + Vector<const char*> expected; + expected.add(a); + expected.add(b); + expected.add(c); + expectHasGroupWithSplits(expected); +} + +void GrouperTest::expectHasGroupWithSplits(const char* a, const char* b, const char* c, const char* d) { + Vector<const char*> expected; + expected.add(a); + expected.add(b); + expected.add(c); + expected.add(d); + expectHasGroupWithSplits(expected); +} + +void GrouperTest::expectHasGroupWithSplits(const Vector<const char*>& expectedStrs) { + Vector<SplitDescription> splits; + const size_t expectedStrCount = expectedStrs.size(); + for (size_t i = 0; i < expectedStrCount; i++) { + splits.add(); + if (!SplitDescription::parse(String8(expectedStrs[i]), &splits.editTop())) { + ADD_FAILURE() << "Failed to parse SplitDescription " << expectedStrs[i]; + return; + } + } + const size_t splitCount = splits.size(); + + const size_t groupCount = mGroups.size(); + for (size_t i = 0; i < groupCount; i++) { + const SortedVector<SplitDescription>& group = mGroups[i]; + if (group.size() != splitCount) { + continue; + } + + size_t found = 0; + for (size_t j = 0; j < splitCount; j++) { + if (group.indexOf(splits[j]) >= 0) { + found++; + } + } + + if (found == splitCount) { + return; + } + } + + String8 errorMessage("Failed to find expected group ["); + for (size_t i = 0; i < splitCount; i++) { + if (i != 0) { + errorMessage.append(", "); + } + errorMessage.append(splits[i].toString()); + } + errorMessage.append("].\nActual:\n"); + + for (size_t i = 0; i < groupCount; i++) { + errorMessage.appendFormat("Group %d:\n", int(i + 1)); + const SortedVector<SplitDescription>& group = mGroups[i]; + for (size_t j = 0; j < group.size(); j++) { + errorMessage.append(" "); + errorMessage.append(group[j].toString()); + errorMessage.append("\n"); + } + } + ADD_FAILURE() << errorMessage.string(); +} + +void GrouperTest::addSplit(Vector<SplitDescription>& splits, const char* str) { + splits.add(); + EXPECT_TRUE(SplitDescription::parse(String8(str), &splits.editTop())); +} + +} // namespace split diff --git a/tools/split-select/Main.cpp b/tools/split-select/Main.cpp new file mode 100644 index 0000000..434494e --- /dev/null +++ b/tools/split-select/Main.cpp @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 <algorithm> +#include <cstdio> + +#include "aapt/AaptUtil.h" + +#include "Grouper.h" +#include "Rule.h" +#include "RuleGenerator.h" +#include "SplitDescription.h" + +#include <androidfw/AssetManager.h> +#include <androidfw/ResourceTypes.h> +#include <utils/KeyedVector.h> +#include <utils/Vector.h> + +using namespace android; + +namespace split { + +static void usage() { + fprintf(stderr, + "split-select --help\n" + "split-select --target <config> --split <path/to/apk> [--split <path/to/apk> [...]]\n" + "split-select --generate --split <path/to/apk> [--split <path/to/apk> [...]]\n" + "\n" + " --help Displays more information about this program.\n" + " --target <config> Performs the Split APK selection on the given configuration.\n" + " --generate Generates the logic for selecting the Split APK, in JSON format.\n" + " --split <path/to/apk> Includes a Split APK in the selection process.\n" + "\n" + " Where <config> is an extended AAPT resource qualifier of the form\n" + " 'resource-qualifiers:extended-qualifiers', where 'resource-qualifiers' is an AAPT resource\n" + " qualifier (ex: en-rUS-sw600dp-xhdpi), and 'extended-qualifiers' is an ordered list of one\n" + " qualifier (or none) from each category:\n" + " Architecture: armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, mips\n"); +} + +static void help() { + usage(); + fprintf(stderr, "\n" + " Generates the logic for selecting a Split APK given some target Android device configuration.\n" + " Using the flag --generate will emit a JSON encoded tree of rules that must be satisfied in order\n" + " to install the given Split APK. Using the flag --target along with the device configuration\n" + " will emit the set of Split APKs to install, following the same logic that would have been emitted\n" + " via JSON.\n"); +} + +class SplitSelector { +public: + SplitSelector(); + SplitSelector(const Vector<SplitDescription>& splits); + + Vector<SplitDescription> getBestSplits(const SplitDescription& target) const; + + template <typename RuleGenerator> + KeyedVector<SplitDescription, sp<Rule> > getRules() const; + +private: + Vector<SortedVector<SplitDescription> > mGroups; +}; + +SplitSelector::SplitSelector() { +} + +SplitSelector::SplitSelector(const Vector<SplitDescription>& splits) + : mGroups(groupByMutualExclusivity(splits)) { +} + +static void selectBestFromGroup(const SortedVector<SplitDescription>& splits, + const SplitDescription& target, Vector<SplitDescription>& splitsOut) { + SplitDescription bestSplit; + bool isSet = false; + const size_t splitCount = splits.size(); + for (size_t j = 0; j < splitCount; j++) { + const SplitDescription& thisSplit = splits[j]; + if (!thisSplit.match(target)) { + continue; + } + + if (!isSet || thisSplit.isBetterThan(bestSplit, target)) { + isSet = true; + bestSplit = thisSplit; + } + } + + if (isSet) { + splitsOut.add(bestSplit); + } +} + +Vector<SplitDescription> SplitSelector::getBestSplits(const SplitDescription& target) const { + Vector<SplitDescription> bestSplits; + const size_t groupCount = mGroups.size(); + for (size_t i = 0; i < groupCount; i++) { + selectBestFromGroup(mGroups[i], target, bestSplits); + } + return bestSplits; +} + +template <typename RuleGenerator> +KeyedVector<SplitDescription, sp<Rule> > SplitSelector::getRules() const { + KeyedVector<SplitDescription, sp<Rule> > rules; + + const size_t groupCount = mGroups.size(); + for (size_t i = 0; i < groupCount; i++) { + const SortedVector<SplitDescription>& splits = mGroups[i]; + const size_t splitCount = splits.size(); + for (size_t j = 0; j < splitCount; j++) { + sp<Rule> rule = Rule::simplify(RuleGenerator::generate(splits, j)); + if (rule != NULL) { + rules.add(splits[j], rule); + } + } + } + return rules; +} + +Vector<SplitDescription> select(const SplitDescription& target, const Vector<SplitDescription>& splits) { + const SplitSelector selector(splits); + return selector.getBestSplits(target); +} + +void generate(const KeyedVector<String8, Vector<SplitDescription> >& splits) { + Vector<SplitDescription> allSplits; + const size_t apkSplitCount = splits.size(); + for (size_t i = 0; i < apkSplitCount; i++) { + allSplits.appendVector(splits[i]); + } + const SplitSelector selector(allSplits); + KeyedVector<SplitDescription, sp<Rule> > rules(selector.getRules<RuleGenerator>()); + + fprintf(stdout, "[\n"); + for (size_t i = 0; i < apkSplitCount; i++) { + sp<Rule> masterRule = new Rule(); + masterRule->op = Rule::OR_SUBRULES; + const Vector<SplitDescription>& splitDescriptions = splits[i]; + const size_t splitDescriptionCount = splitDescriptions.size(); + for (size_t j = 0; j < splitDescriptionCount; j++) { + masterRule->subrules.add(rules.valueFor(splitDescriptions[j])); + } + masterRule = Rule::simplify(masterRule); + fprintf(stdout, " {\n \"path\": \"%s\",\n \"rules\": %s\n }%s\n", + splits.keyAt(i).string(), + masterRule->toJson(2).string(), + i < apkSplitCount - 1 ? "," : ""); + } + fprintf(stdout, "]\n"); +} + +static void removeRuntimeQualifiers(ConfigDescription* outConfig) { + outConfig->imsi = 0; + outConfig->orientation = ResTable_config::ORIENTATION_ANY; + outConfig->screenWidth = ResTable_config::SCREENWIDTH_ANY; + outConfig->screenHeight = ResTable_config::SCREENHEIGHT_ANY; + outConfig->uiMode &= ResTable_config::UI_MODE_NIGHT_ANY; +} + +static Vector<SplitDescription> extractSplitDescriptionsFromApk(const String8& path) { + AssetManager assetManager; + Vector<SplitDescription> splits; + int32_t cookie = 0; + if (!assetManager.addAssetPath(path, &cookie)) { + return splits; + } + + const ResTable& res = assetManager.getResources(false); + if (res.getError() == NO_ERROR) { + Vector<ResTable_config> configs; + res.getConfigurations(&configs); + const size_t configCount = configs.size(); + for (size_t i = 0; i < configCount; i++) { + splits.add(); + splits.editTop().config = configs[i]; + } + } + + AssetDir* dir = assetManager.openNonAssetDir(cookie, "lib"); + if (dir != NULL) { + const size_t fileCount = dir->getFileCount(); + for (size_t i = 0; i < fileCount; i++) { + splits.add(); + Vector<String8> parts = AaptUtil::splitAndLowerCase(dir->getFileName(i), '-'); + if (parseAbi(parts, 0, &splits.editTop()) < 0) { + fprintf(stderr, "Malformed library %s\n", dir->getFileName(i).string()); + splits.pop(); + } + } + delete dir; + } + return splits; +} + +static int main(int argc, char** argv) { + // Skip over the first argument. + argc--; + argv++; + + bool generateFlag = false; + String8 targetConfigStr; + Vector<String8> splitApkPaths; + while (argc > 0) { + const String8 arg(*argv); + if (arg == "--target") { + argc--; + argv++; + if (argc < 1) { + fprintf(stderr, "Missing parameter for --split.\n"); + usage(); + return 1; + } + targetConfigStr.setTo(*argv); + } else if (arg == "--split") { + argc--; + argv++; + if (argc < 1) { + fprintf(stderr, "Missing parameter for --split.\n"); + usage(); + return 1; + } + splitApkPaths.add(String8(*argv)); + } else if (arg == "--generate") { + generateFlag = true; + } else if (arg == "--help") { + help(); + return 0; + } else { + fprintf(stderr, "Unknown argument '%s'\n", arg.string()); + usage(); + return 1; + } + argc--; + argv++; + } + + if (!generateFlag && targetConfigStr == "") { + usage(); + return 1; + } + + if (splitApkPaths.size() == 0) { + usage(); + return 1; + } + + SplitDescription targetSplit; + if (!generateFlag) { + if (!SplitDescription::parse(targetConfigStr, &targetSplit)) { + fprintf(stderr, "Invalid --target config: '%s'\n", + targetConfigStr.string()); + usage(); + return 1; + } + + // We don't want to match on things that will change at run-time + // (orientation, w/h, etc.). + removeRuntimeQualifiers(&targetSplit.config); + } + + KeyedVector<String8, Vector<SplitDescription> > apkPathSplitMap; + KeyedVector<SplitDescription, String8> splitApkPathMap; + Vector<SplitDescription> splitConfigs; + const size_t splitCount = splitApkPaths.size(); + for (size_t i = 0; i < splitCount; i++) { + Vector<SplitDescription> splits = extractSplitDescriptionsFromApk(splitApkPaths[i]); + if (splits.isEmpty()) { + fprintf(stderr, "Invalid --split path: '%s'. No splits found.\n", + splitApkPaths[i].string()); + usage(); + return 1; + } + apkPathSplitMap.replaceValueFor(splitApkPaths[i], splits); + const size_t apkSplitDescriptionCount = splits.size(); + for (size_t j = 0; j < apkSplitDescriptionCount; j++) { + splitApkPathMap.replaceValueFor(splits[j], splitApkPaths[i]); + } + splitConfigs.appendVector(splits); + } + + if (!generateFlag) { + Vector<SplitDescription> matchingConfigs = select(targetSplit, splitConfigs); + const size_t matchingConfigCount = matchingConfigs.size(); + SortedVector<String8> matchingSplitPaths; + for (size_t i = 0; i < matchingConfigCount; i++) { + matchingSplitPaths.add(splitApkPathMap.valueFor(matchingConfigs[i])); + } + + const size_t matchingSplitApkPathCount = matchingSplitPaths.size(); + for (size_t i = 0; i < matchingSplitApkPathCount; i++) { + fprintf(stderr, "%s\n", matchingSplitPaths[i].string()); + } + } else { + generate(apkPathSplitMap); + } + return 0; +} + +} // namespace split + +int main(int argc, char** argv) { + return split::main(argc, argv); +} diff --git a/tools/split-select/Rule.cpp b/tools/split-select/Rule.cpp new file mode 100644 index 0000000..48d21ff --- /dev/null +++ b/tools/split-select/Rule.cpp @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Rule.h" + +#include <utils/String8.h> + +using namespace android; + +namespace split { + +inline static void indentStr(String8& str, int indent) { + while (indent > 0) { + str.append(" "); + indent--; + } +} + +Rule::Rule(const Rule& rhs) + : RefBase() + , op(rhs.op) + , key(rhs.key) + , negate(rhs.negate) + , stringArgs(rhs.stringArgs) + , longArgs(rhs.longArgs) + , subrules(rhs.subrules) { +} + +String8 Rule::toJson(int indent) const { + String8 str; + indentStr(str, indent); + str.append("{\n"); + indent++; + indentStr(str, indent); + str.append("\"op\": \""); + switch (op) { + case ALWAYS_TRUE: + str.append("ALWAYS_TRUE"); + break; + case GREATER_THAN: + str.append("GREATER_THAN"); + break; + case LESS_THAN: + str.append("LESS_THAN"); + break; + case EQUALS: + str.append("EQUALS"); + break; + case AND_SUBRULES: + str.append("AND_SUBRULES"); + break; + case OR_SUBRULES: + str.append("OR_SUBRULES"); + break; + case CONTAINS_ANY: + str.append("CONTAINS_ANY"); + break; + default: + str.appendFormat("%d", op); + break; + } + str.append("\""); + + if (negate) { + str.append(",\n"); + indentStr(str, indent); + str.append("\"negate\": true"); + } + + bool includeKey = true; + switch (op) { + case AND_SUBRULES: + case OR_SUBRULES: + includeKey = false; + break; + default: + break; + } + + if (includeKey) { + str.append(",\n"); + indentStr(str, indent); + str.append("\"property\": \""); + switch (key) { + case NONE: + str.append("NONE"); + break; + case SDK_VERSION: + str.append("SDK_VERSION"); + break; + case SCREEN_DENSITY: + str.append("SCREEN_DENSITY"); + break; + case NATIVE_PLATFORM: + str.append("NATIVE_PLATFORM"); + break; + case LANGUAGE: + str.append("LANGUAGE"); + break; + default: + str.appendFormat("%d", key); + break; + } + str.append("\""); + } + + if (op == AND_SUBRULES || op == OR_SUBRULES) { + str.append(",\n"); + indentStr(str, indent); + str.append("\"subrules\": [\n"); + const size_t subruleCount = subrules.size(); + for (size_t i = 0; i < subruleCount; i++) { + str.append(subrules[i]->toJson(indent + 1)); + if (i != subruleCount - 1) { + str.append(","); + } + str.append("\n"); + } + indentStr(str, indent); + str.append("]"); + } else { + switch (key) { + case SDK_VERSION: + case SCREEN_DENSITY: { + str.append(",\n"); + indentStr(str, indent); + str.append("\"args\": ["); + const size_t argCount = longArgs.size(); + for (size_t i = 0; i < argCount; i++) { + if (i != 0) { + str.append(", "); + } + str.appendFormat("%d", longArgs[i]); + } + str.append("]"); + break; + } + case LANGUAGE: + case NATIVE_PLATFORM: { + str.append(",\n"); + indentStr(str, indent); + str.append("\"args\": ["); + const size_t argCount = stringArgs.size(); + for (size_t i = 0; i < argCount; i++) { + if (i != 0) { + str.append(", "); + } + str.append(stringArgs[i]); + } + str.append("]"); + break; + } + default: + break; + } + } + str.append("\n"); + indent--; + indentStr(str, indent); + str.append("}"); + return str; +} + +sp<Rule> Rule::simplify(sp<Rule> rule) { + if (rule->op != AND_SUBRULES && rule->op != OR_SUBRULES) { + return rule; + } + + Vector<sp<Rule> > newSubrules; + newSubrules.setCapacity(rule->subrules.size()); + const size_t subruleCount = rule->subrules.size(); + for (size_t i = 0; i < subruleCount; i++) { + sp<Rule> simplifiedRule = simplify(rule->subrules.editItemAt(i)); + if (simplifiedRule != NULL) { + if (simplifiedRule->op == rule->op) { + newSubrules.appendVector(simplifiedRule->subrules); + } else { + newSubrules.add(simplifiedRule); + } + } + } + + const size_t newSubruleCount = newSubrules.size(); + if (newSubruleCount == 0) { + return NULL; + } else if (subruleCount == 1) { + return newSubrules.editTop(); + } + rule->subrules = newSubrules; + return rule; +} + +} // namespace split diff --git a/tools/split-select/Rule.h b/tools/split-select/Rule.h new file mode 100644 index 0000000..08a2075 --- /dev/null +++ b/tools/split-select/Rule.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 H_ANDROID_SPLIT_RULE +#define H_ANDROID_SPLIT_RULE + +#include "SplitDescription.h" + +#include <utils/RefBase.h> +#include <utils/StrongPointer.h> +#include <utils/String8.h> +#include <utils/Vector.h> + +namespace split { + +struct Rule : public virtual android::RefBase { + inline Rule(); + Rule(const Rule& rhs); + + enum Operator { + LESS_THAN = 1, + GREATER_THAN, + EQUALS, + CONTAINS_ANY, + CONTAINS_ALL, + IS_TRUE, + IS_FALSE, + AND_SUBRULES, + OR_SUBRULES, + ALWAYS_TRUE, + }; + + Operator op; + + enum Key { + NONE = 0, + SDK_VERSION, + SCREEN_DENSITY, + LANGUAGE, + NATIVE_PLATFORM, + TOUCH_SCREEN, + SCREEN_SIZE, + SCREEN_LAYOUT, + }; + + Key key; + bool negate; + + android::Vector<android::String8> stringArgs; + android::Vector<int> longArgs; + android::Vector<double> doubleArgs; + android::Vector<android::sp<Rule> > subrules; + + android::String8 toJson(int indent=0) const; + + static android::sp<Rule> simplify(android::sp<Rule> rule); +}; + +Rule::Rule() +: op(ALWAYS_TRUE) +, key(NONE) +, negate(false) {} + +} // namespace split + +#endif // H_ANDROID_SPLIT_RULE diff --git a/tools/split-select/RuleGenerator.cpp b/tools/split-select/RuleGenerator.cpp new file mode 100644 index 0000000..83c9795 --- /dev/null +++ b/tools/split-select/RuleGenerator.cpp @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "RuleGenerator.h" +#include "aapt/SdkConstants.h" + +#include <algorithm> +#include <cmath> +#include <vector> +#include <androidfw/ResourceTypes.h> + +using namespace android; + +namespace split { + +// Calculate the point at which the density selection changes between l and h. +static inline int findMid(int l, int h) { + double root = sqrt((h*h) + (8*l*h)); + return (double(-h) + root) / 2.0; +} + +sp<Rule> RuleGenerator::generateDensity(const Vector<int>& allDensities, size_t index) { + if (allDensities[index] != ResTable_config::DENSITY_ANY) { + sp<Rule> densityRule = new Rule(); + densityRule->op = Rule::AND_SUBRULES; + + const bool hasAnyDensity = std::find(allDensities.begin(), + allDensities.end(), (int) ResTable_config::DENSITY_ANY) != allDensities.end(); + + if (hasAnyDensity) { + sp<Rule> version = new Rule(); + version->op = Rule::LESS_THAN; + version->key = Rule::SDK_VERSION; + version->longArgs.add((long) SDK_LOLLIPOP); + densityRule->subrules.add(version); + } + + if (index > 0) { + sp<Rule> gt = new Rule(); + gt->op = Rule::GREATER_THAN; + gt->key = Rule::SCREEN_DENSITY; + gt->longArgs.add(findMid(allDensities[index - 1], allDensities[index]) - 1); + densityRule->subrules.add(gt); + } + + if (index + 1 < allDensities.size() && allDensities[index + 1] != ResTable_config::DENSITY_ANY) { + sp<Rule> lt = new Rule(); + lt->op = Rule::LESS_THAN; + lt->key = Rule::SCREEN_DENSITY; + lt->longArgs.add(findMid(allDensities[index], allDensities[index + 1])); + densityRule->subrules.add(lt); + } + return densityRule; + } else { + // SDK_VERSION is handled elsewhere, so we always pick DENSITY_ANY if it's + // available. + sp<Rule> always = new Rule(); + always->op = Rule::ALWAYS_TRUE; + return always; + } +} + +sp<Rule> RuleGenerator::generateAbi(const Vector<abi::Variant>& splitAbis, size_t index) { + const abi::Variant thisAbi = splitAbis[index]; + const Vector<abi::Variant>& familyVariants = abi::getVariants(abi::getFamily(thisAbi)); + + Vector<abi::Variant>::const_iterator start = + std::find(familyVariants.begin(), familyVariants.end(), thisAbi); + + Vector<abi::Variant>::const_iterator end = familyVariants.end(); + if (index + 1 < splitAbis.size()) { + end = std::find(start, familyVariants.end(), splitAbis[index + 1]); + } + + sp<Rule> abiRule = new Rule(); + abiRule->op = Rule::CONTAINS_ANY; + abiRule->key = Rule::NATIVE_PLATFORM; + while (start != end) { + abiRule->stringArgs.add(String8(abi::toString(*start))); + ++start; + } + return abiRule; +} + +sp<Rule> RuleGenerator::generate(const SortedVector<SplitDescription>& group, size_t index) { + sp<Rule> rootRule = new Rule(); + rootRule->op = Rule::AND_SUBRULES; + + if (group[index].config.locale != 0) { + sp<Rule> locale = new Rule(); + locale->op = Rule::EQUALS; + locale->key = Rule::LANGUAGE; + char str[RESTABLE_MAX_LOCALE_LEN]; + group[index].config.getBcp47Locale(str); + locale->stringArgs.add(String8(str)); + rootRule->subrules.add(locale); + } + + if (group[index].config.sdkVersion != 0) { + sp<Rule> sdk = new Rule(); + sdk->op = Rule::GREATER_THAN; + sdk->key = Rule::SDK_VERSION; + sdk->longArgs.add(group[index].config.sdkVersion - 1); + rootRule->subrules.add(sdk); + } + + if (group[index].config.density != 0) { + size_t densityIndex = 0; + Vector<int> allDensities; + allDensities.add(group[index].config.density); + + const size_t groupSize = group.size(); + for (size_t i = 0; i < groupSize; i++) { + if (group[i].config.density != group[index].config.density) { + // This group differs by density. + allDensities.clear(); + for (size_t j = 0; j < groupSize; j++) { + allDensities.add(group[j].config.density); + } + densityIndex = index; + break; + } + } + rootRule->subrules.add(generateDensity(allDensities, densityIndex)); + } + + if (group[index].abi != abi::Variant_none) { + size_t abiIndex = 0; + Vector<abi::Variant> allVariants; + allVariants.add(group[index].abi); + + const size_t groupSize = group.size(); + for (size_t i = 0; i < groupSize; i++) { + if (group[i].abi != group[index].abi) { + // This group differs by ABI. + allVariants.clear(); + for (size_t j = 0; j < groupSize; j++) { + allVariants.add(group[j].abi); + } + abiIndex = index; + break; + } + } + rootRule->subrules.add(generateAbi(allVariants, abiIndex)); + } + + return rootRule; +} + +} // namespace split diff --git a/tools/split-select/RuleGenerator.h b/tools/split-select/RuleGenerator.h new file mode 100644 index 0000000..619acd9 --- /dev/null +++ b/tools/split-select/RuleGenerator.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 H_ANDROID_SPLIT_RULE_GENERATOR +#define H_ANDROID_SPLIT_RULE_GENERATOR + +#include "Abi.h" +#include "Rule.h" +#include "SplitDescription.h" + +#include <utils/SortedVector.h> +#include <utils/Vector.h> + +namespace split { + +struct RuleGenerator { + // Generate rules for a Split given the group of mutually exclusive splits it belongs to + static android::sp<Rule> generate(const android::SortedVector<SplitDescription>& group, size_t index); + + static android::sp<Rule> generateAbi(const android::Vector<abi::Variant>& allVariants, size_t index); + static android::sp<Rule> generateDensity(const android::Vector<int>& allDensities, size_t index); +}; + +} // namespace split + +#endif // H_ANDROID_SPLIT_RULE_GENERATOR diff --git a/tools/split-select/RuleGenerator_test.cpp b/tools/split-select/RuleGenerator_test.cpp new file mode 100644 index 0000000..470cadc --- /dev/null +++ b/tools/split-select/RuleGenerator_test.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "RuleGenerator.h" + +#include "aapt/SdkConstants.h" +#include "TestRules.h" + +#include <gtest/gtest.h> +#include <utils/Vector.h> + +using namespace android; +using namespace split::test; + +namespace split { + +TEST(RuleGeneratorTest, testAbiRules) { + Vector<abi::Variant> abis; + const ssize_t armeabiIndex = abis.add(abi::Variant_armeabi); + const ssize_t armeabi_v7aIndex = abis.add(abi::Variant_armeabi_v7a); + const ssize_t x86Index = abis.add(abi::Variant_x86); + + EXPECT_RULES_EQ(RuleGenerator::generateAbi(abis, armeabiIndex), + ContainsAnyRule(Rule::NATIVE_PLATFORM, "armeabi") + ); + + EXPECT_RULES_EQ(RuleGenerator::generateAbi(abis, armeabi_v7aIndex), + ContainsAnyRule(Rule::NATIVE_PLATFORM, "armeabi-v7a", "arm64-v8a") + ); + + EXPECT_RULES_EQ(RuleGenerator::generateAbi(abis, x86Index), + ContainsAnyRule(Rule::NATIVE_PLATFORM, "x86", "x86_64") + ); +} + +TEST(RuleGeneratorTest, densityConstantsAreSane) { + EXPECT_LT(263, (int) ConfigDescription::DENSITY_XHIGH); + EXPECT_GT(262, (int) ConfigDescription::DENSITY_HIGH); + EXPECT_LT(363, (int) ConfigDescription::DENSITY_XXHIGH); + EXPECT_GT(362, (int) ConfigDescription::DENSITY_XHIGH); +} + +TEST(RuleGeneratorTest, testDensityRules) { + Vector<int> densities; + const ssize_t highIndex = densities.add(ConfigDescription::DENSITY_HIGH); + const ssize_t xhighIndex = densities.add(ConfigDescription::DENSITY_XHIGH); + const ssize_t xxhighIndex = densities.add(ConfigDescription::DENSITY_XXHIGH); + + EXPECT_RULES_EQ(RuleGenerator::generateDensity(densities, highIndex), + AndRule() + .add(LtRule(Rule::SCREEN_DENSITY, 263)) + ); + + EXPECT_RULES_EQ(RuleGenerator::generateDensity(densities, xhighIndex), + AndRule() + .add(GtRule(Rule::SCREEN_DENSITY, 262)) + .add(LtRule(Rule::SCREEN_DENSITY, 363)) + ); + + EXPECT_RULES_EQ(RuleGenerator::generateDensity(densities, xxhighIndex), + AndRule() + .add(GtRule(Rule::SCREEN_DENSITY, 362)) + ); +} + +TEST(RuleGeneratorTest, testDensityRulesWithAnyDpi) { + Vector<int> densities; + const ssize_t highIndex = densities.add(ConfigDescription::DENSITY_HIGH); + const ssize_t xhighIndex = densities.add(ConfigDescription::DENSITY_XHIGH); + const ssize_t xxhighIndex = densities.add(ConfigDescription::DENSITY_XXHIGH); + const ssize_t anyIndex = densities.add(ConfigDescription::DENSITY_ANY); + + EXPECT_RULES_EQ(RuleGenerator::generateDensity(densities, highIndex), + AndRule() + .add(LtRule(Rule::SDK_VERSION, SDK_LOLLIPOP)) + .add(LtRule(Rule::SCREEN_DENSITY, 263)) + ); + + EXPECT_RULES_EQ(RuleGenerator::generateDensity(densities, xhighIndex), + AndRule() + .add(LtRule(Rule::SDK_VERSION, SDK_LOLLIPOP)) + .add(GtRule(Rule::SCREEN_DENSITY, 262)) + .add(LtRule(Rule::SCREEN_DENSITY, 363)) + ); + + EXPECT_RULES_EQ(RuleGenerator::generateDensity(densities, xxhighIndex), + AndRule() + .add(LtRule(Rule::SDK_VERSION, SDK_LOLLIPOP)) + .add(GtRule(Rule::SCREEN_DENSITY, 362)) + ); + + // We expect AlwaysTrue because anydpi always has attached v21 to the configuration + // and the rest of the rule generation code generates the sdk version checks. + EXPECT_RULES_EQ(RuleGenerator::generateDensity(densities, anyIndex), AlwaysTrue()); +} + +} // namespace split diff --git a/tools/split-select/Rule_test.cpp b/tools/split-select/Rule_test.cpp new file mode 100644 index 0000000..c6cff0d --- /dev/null +++ b/tools/split-select/Rule_test.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "Rule.h" + +#include "SplitDescription.h" +#include "TestRules.h" + +#include <algorithm> +#include <gtest/gtest.h> +#include <string> +#include <utils/String8.h> + +using namespace android; +using namespace split::test; + +namespace split { + +TEST(RuleTest, generatesValidJson) { + Rule rule(AndRule() + .add(EqRule(Rule::SDK_VERSION, 7)) + .add(OrRule() + .add(GtRule(Rule::SCREEN_DENSITY, 10)) + .add(LtRule(Rule::SCREEN_DENSITY, 5)) + ) + ); + + // Expected + std::string expected( + "{" + " \"op\": \"AND_SUBRULES\"," + " \"subrules\": [" + " {" + " \"op\": \"EQUALS\"," + " \"property\": \"SDK_VERSION\"," + " \"args\": [7]" + " }," + " {" + " \"op\": \"OR_SUBRULES\"," + " \"subrules\": [" + " {" + " \"op\": \"GREATER_THAN\"," + " \"property\": \"SCREEN_DENSITY\"," + " \"args\": [10]" + " }," + " {" + " \"op\": \"LESS_THAN\"," + " \"property\": \"SCREEN_DENSITY\"," + " \"args\": [5]" + " }" + " ]" + " }" + " ]" + "}"); + expected.erase(std::remove_if(expected.begin(), expected.end(), ::isspace), expected.end()); + + // Result + std::string result(rule.toJson().string()); + result.erase(std::remove_if(result.begin(), result.end(), ::isspace), result.end()); + + ASSERT_EQ(expected, result); +} + +TEST(RuleTest, simplifiesSingleSubruleRules) { + sp<Rule> rule = new Rule(AndRule() + .add(EqRule(Rule::SDK_VERSION, 7)) + ); + + EXPECT_RULES_EQ(Rule::simplify(rule), EqRule(Rule::SDK_VERSION, 7)); +} + +TEST(RuleTest, simplifiesNestedSameOpSubrules) { + sp<Rule> rule = new Rule(AndRule() + .add(AndRule() + .add(EqRule(Rule::SDK_VERSION, 7)) + ) + .add(EqRule(Rule::SDK_VERSION, 8)) + ); + + EXPECT_RULES_EQ(Rule::simplify(rule), + AndRule() + .add(EqRule(Rule::SDK_VERSION, 7)) + .add(EqRule(Rule::SDK_VERSION, 8)) + ); +} + +} // namespace split diff --git a/tools/split-select/SplitDescription.cpp b/tools/split-select/SplitDescription.cpp new file mode 100644 index 0000000..99bc23d --- /dev/null +++ b/tools/split-select/SplitDescription.cpp @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "SplitDescription.h" + +#include "aapt/AaptConfig.h" +#include "aapt/AaptUtil.h" + +#include <utils/String8.h> +#include <utils/Vector.h> + +using namespace android; + +namespace split { + +SplitDescription::SplitDescription() +: abi(abi::Variant_none) { +} + +int SplitDescription::compare(const SplitDescription& rhs) const { + int cmp; + cmp = (int)abi - (int)rhs.abi; + if (cmp != 0) return cmp; + return config.compareLogical(rhs.config); +} + +bool SplitDescription::isBetterThan(const SplitDescription& o, const SplitDescription& target) const { + if (abi != abi::Variant_none || o.abi != abi::Variant_none) { + abi::Family family = abi::getFamily(abi); + abi::Family oFamily = abi::getFamily(o.abi); + if (family != oFamily) { + return family != abi::Family_none; + } + + if (int(target.abi) - int(abi) < int(target.abi) - int(o.abi)) { + return true; + } + } + return config.isBetterThan(o.config, &target.config); +} + +bool SplitDescription::match(const SplitDescription& o) const { + if (abi != abi::Variant_none) { + abi::Family family = abi::getFamily(abi); + abi::Family oFamily = abi::getFamily(o.abi); + if (family != oFamily) { + return false; + } + + if (int(abi) > int(o.abi)) { + return false; + } + } + return config.match(o.config); +} + +String8 SplitDescription::toString() const { + String8 extension; + if (abi != abi::Variant_none) { + if (extension.isEmpty()) { + extension.append(":"); + } else { + extension.append("-"); + } + extension.append(abi::toString(abi)); + } + String8 str(config.toString()); + str.append(extension); + return str; +} + +ssize_t parseAbi(const Vector<String8>& parts, const ssize_t index, + SplitDescription* outSplit) { + const ssize_t N = parts.size(); + abi::Variant abi = abi::Variant_none; + ssize_t endIndex = index; + if (parts[endIndex] == "arm64") { + endIndex++; + if (endIndex < N) { + if (parts[endIndex] == "v8a") { + endIndex++; + abi = abi::Variant_arm64_v8a; + } + } + } else if (parts[endIndex] == "armeabi") { + endIndex++; + abi = abi::Variant_armeabi; + if (endIndex < N) { + if (parts[endIndex] == "v7a") { + endIndex++; + abi = abi::Variant_armeabi_v7a; + } + } + } else if (parts[endIndex] == "x86") { + endIndex++; + abi = abi::Variant_x86; + } else if (parts[endIndex] == "x86_64") { + endIndex++; + abi = abi::Variant_x86_64; + } else if (parts[endIndex] == "mips") { + endIndex++; + abi = abi::Variant_mips; + } else if (parts[endIndex] == "mips64") { + endIndex++; + abi = abi::Variant_mips64; + } + + if (abi == abi::Variant_none && endIndex != index) { + return -1; + } + + if (outSplit != NULL) { + outSplit->abi = abi; + } + return endIndex; +} + +bool SplitDescription::parse(const String8& str, SplitDescription* outSplit) { + ssize_t index = str.find(":"); + + String8 configStr; + String8 extensionStr; + if (index >= 0) { + configStr.setTo(str.string(), index); + extensionStr.setTo(str.string() + index + 1); + } else { + configStr.setTo(str); + } + + SplitDescription split; + if (!AaptConfig::parse(configStr, &split.config)) { + return false; + } + + Vector<String8> parts = AaptUtil::splitAndLowerCase(extensionStr, '-'); + const ssize_t N = parts.size(); + index = 0; + + if (extensionStr.length() == 0) { + goto success; + } + + index = parseAbi(parts, index, &split); + if (index < 0) { + return false; + } else { + if (index == N) { + goto success; + } + } + + // Unrecognized + return false; + +success: + if (outSplit != NULL) { + *outSplit = split; + } + return true; +} + +} // namespace split diff --git a/tools/split-select/SplitDescription.h b/tools/split-select/SplitDescription.h new file mode 100644 index 0000000..b13c9ee --- /dev/null +++ b/tools/split-select/SplitDescription.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 H_ANDROID_SPLIT_SPLIT_DESCRIPTION +#define H_ANDROID_SPLIT_SPLIT_DESCRIPTION + +#include "aapt/ConfigDescription.h" +#include "Abi.h" + +#include <utils/String8.h> +#include <utils/Vector.h> + +namespace split { + +struct SplitDescription { + SplitDescription(); + + ConfigDescription config; + abi::Variant abi; + + int compare(const SplitDescription& rhs) const; + inline bool operator<(const SplitDescription& rhs) const; + inline bool operator==(const SplitDescription& rhs) const; + inline bool operator!=(const SplitDescription& rhs) const; + + bool match(const SplitDescription& o) const; + bool isBetterThan(const SplitDescription& o, const SplitDescription& target) const; + + android::String8 toString() const; + + static bool parse(const android::String8& str, SplitDescription* outSplit); +}; + +ssize_t parseAbi(const android::Vector<android::String8>& parts, const ssize_t index, + SplitDescription* outSplit); + +bool SplitDescription::operator<(const SplitDescription& rhs) const { + return compare(rhs) < 0; +} + +bool SplitDescription::operator==(const SplitDescription& rhs) const { + return compare(rhs) == 0; +} + +bool SplitDescription::operator!=(const SplitDescription& rhs) const { + return compare(rhs) != 0; +} + +} // namespace split + +#endif // H_ANDROID_SPLIT_SPLIT_DESCRIPTION diff --git a/tools/split-select/TestRules.cpp b/tools/split-select/TestRules.cpp new file mode 100644 index 0000000..86ccd6a --- /dev/null +++ b/tools/split-select/TestRules.cpp @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 "TestRules.h" + +#include <utils/String8.h> + +using android::String8; +using android::sp; + +namespace split { +namespace test { + +const Rule EqRule(Rule::Key key, long value) { + Rule rule; + rule.op = Rule::EQUALS; + rule.key = key; + rule.longArgs.add(value); + return rule; +} + +const Rule GtRule(Rule::Key key, long value) { + Rule rule; + rule.op = Rule::GREATER_THAN; + rule.key = key; + rule.longArgs.add(value); + return rule; +} + +const Rule LtRule(Rule::Key key, long value) { + Rule rule; + rule.op = Rule::LESS_THAN; + rule.key = key; + rule.longArgs.add(value); + return rule; +} + +const Rule ContainsAnyRule(Rule::Key key, const char* str1) { + Rule rule; + rule.op = Rule::CONTAINS_ANY; + rule.key = key; + rule.stringArgs.add(String8(str1)); + return rule; +} + +const Rule ContainsAnyRule(Rule::Key key, const char* str1, const char* str2) { + Rule rule; + rule.op = Rule::CONTAINS_ANY; + rule.key = key; + rule.stringArgs.add(String8(str1)); + rule.stringArgs.add(String8(str2)); + return rule; +} + +const Rule AlwaysTrue() { + Rule rule; + rule.op = Rule::ALWAYS_TRUE; + return rule; +} + +::testing::AssertionResult RulePredFormat( + const char*, const char*, + const sp<Rule>& actual, const Rule& expected) { + const String8 expectedStr(expected.toJson()); + const String8 actualStr(actual != NULL ? actual->toJson() : String8()); + + if (expectedStr != actualStr) { + return ::testing::AssertionFailure() + << "Expected: " << expectedStr.string() << "\n" + << " Actual: " << actualStr.string(); + } + return ::testing::AssertionSuccess(); +} + + +} // namespace test +} // namespace split diff --git a/tools/split-select/TestRules.h b/tools/split-select/TestRules.h new file mode 100644 index 0000000..50b7ad1 --- /dev/null +++ b/tools/split-select/TestRules.h @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 H_AAPT_SPLIT_TEST_RULES +#define H_AAPT_SPLIT_TEST_RULES + +#include "Rule.h" + +#include <gtest/gtest.h> + +namespace split { +namespace test { + +struct AndRule : public Rule { + AndRule() { + op = Rule::AND_SUBRULES; + } + + AndRule& add(const Rule& rhs) { + subrules.add(new Rule(rhs)); + return *this; + } +}; + +struct OrRule : public Rule { + OrRule() { + op = Rule::OR_SUBRULES; + } + + OrRule& add(const Rule& rhs) { + subrules.add(new Rule(rhs)); + return *this; + } +}; + +const Rule EqRule(Rule::Key key, long value); +const Rule LtRule(Rule::Key key, long value); +const Rule GtRule(Rule::Key key, long value); +const Rule ContainsAnyRule(Rule::Key key, const char* str1); +const Rule ContainsAnyRule(Rule::Key key, const char* str1, const char* str2); +const Rule AlwaysTrue(); + +::testing::AssertionResult RulePredFormat( + const char* actualExpr, const char* expectedExpr, + const android::sp<Rule>& actual, const Rule& expected); + +#define EXPECT_RULES_EQ(actual, expected) \ + EXPECT_PRED_FORMAT2(::split::test::RulePredFormat, actual, expected) + +} // namespace test +} // namespace split + +#endif // H_AAPT_SPLIT_TEST_RULES |