| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 1 | /* | 
|  | 2 | * Copyright (C) 2016 The Android Open Source Project | 
|  | 3 | * | 
|  | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | 5 | * you may not use this file except in compliance with the License. | 
|  | 6 | * You may obtain a copy of the License at | 
|  | 7 | * | 
|  | 8 | *      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 9 | * | 
|  | 10 | * Unless required by applicable law or agreed to in writing, software | 
|  | 11 | * distributed under the License is distributed on an "AS IS" BASIS, | 
|  | 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | 13 | * See the License for the specific language governing permissions and | 
|  | 14 | * limitations under the License. | 
|  | 15 | */ | 
|  | 16 |  | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 17 | #include "split/TableSplitter.h" | 
|  | 18 |  | 
| Adam Lesinski | 803c7c8 | 2016-04-06 16:09:43 -0700 | [diff] [blame] | 19 | #include <algorithm> | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 20 | #include <map> | 
|  | 21 | #include <set> | 
| Pierre Lecesne | 672384b | 2017-02-06 10:29:02 +0000 | [diff] [blame] | 22 | #include <unordered_set> | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 23 | #include <unordered_map> | 
|  | 24 | #include <vector> | 
| Adam Lesinski | d5083f6 | 2017-01-16 15:07:21 -0800 | [diff] [blame] | 25 |  | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 26 | #include "android-base/logging.h" | 
| Mårten Kongstad | 24c9aa6 | 2018-06-20 08:46:41 +0200 | [diff] [blame] | 27 | #include "androidfw/ConfigDescription.h" | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 28 |  | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 29 | #include "ResourceTable.h" | 
| Fabien Sanglard | 2d34e76 | 2019-02-21 15:13:29 -0800 | [diff] [blame] | 30 | #include "trace/TraceBuffer.h" | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 31 | #include "util/Util.h" | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 32 |  | 
| Mårten Kongstad | 24c9aa6 | 2018-06-20 08:46:41 +0200 | [diff] [blame] | 33 | using ::android::ConfigDescription; | 
|  | 34 |  | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 35 | namespace aapt { | 
|  | 36 |  | 
|  | 37 | using ConfigClaimedMap = std::unordered_map<ResourceConfigValue*, bool>; | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 38 | using ConfigDensityGroups = std::map<ConfigDescription, std::vector<ResourceConfigValue*>>; | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 39 |  | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 40 | static ConfigDescription CopyWithoutDensity(const ConfigDescription& config) { | 
|  | 41 | ConfigDescription without_density = config; | 
|  | 42 | without_density.density = 0; | 
|  | 43 | return without_density; | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 44 | } | 
|  | 45 |  | 
|  | 46 | /** | 
|  | 47 | * Selects values that match exactly the constraints given. | 
|  | 48 | */ | 
|  | 49 | class SplitValueSelector { | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 50 | public: | 
|  | 51 | explicit SplitValueSelector(const SplitConstraints& constraints) { | 
|  | 52 | for (const ConfigDescription& config : constraints.configs) { | 
|  | 53 | if (config.density == 0) { | 
|  | 54 | density_independent_configs_.insert(config); | 
|  | 55 | } else { | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 56 | density_dependent_config_to_density_map_[CopyWithoutDensity(config)] = config.density; | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 57 | } | 
|  | 58 | } | 
|  | 59 | } | 
|  | 60 |  | 
|  | 61 | std::vector<ResourceConfigValue*> SelectValues( | 
|  | 62 | const ConfigDensityGroups& density_groups, | 
|  | 63 | ConfigClaimedMap* claimed_values) { | 
|  | 64 | std::vector<ResourceConfigValue*> selected; | 
|  | 65 |  | 
|  | 66 | // Select the regular values. | 
|  | 67 | for (auto& entry : *claimed_values) { | 
|  | 68 | // Check if the entry has a density. | 
|  | 69 | ResourceConfigValue* config_value = entry.first; | 
|  | 70 | if (config_value->config.density == 0 && !entry.second) { | 
|  | 71 | // This is still available. | 
|  | 72 | if (density_independent_configs_.find(config_value->config) != | 
|  | 73 | density_independent_configs_.end()) { | 
|  | 74 | selected.push_back(config_value); | 
|  | 75 |  | 
|  | 76 | // Mark the entry as taken. | 
|  | 77 | entry.second = true; | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 78 | } | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 79 | } | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 80 | } | 
|  | 81 |  | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 82 | // Now examine the densities | 
|  | 83 | for (auto& entry : density_groups) { | 
|  | 84 | // We do not care if the value is claimed, since density values can be | 
|  | 85 | // in multiple splits. | 
|  | 86 | const ConfigDescription& config = entry.first; | 
|  | 87 | const std::vector<ResourceConfigValue*>& related_values = entry.second; | 
|  | 88 | auto density_value_iter = | 
|  | 89 | density_dependent_config_to_density_map_.find(config); | 
|  | 90 | if (density_value_iter != | 
|  | 91 | density_dependent_config_to_density_map_.end()) { | 
|  | 92 | // Select the best one! | 
|  | 93 | ConfigDescription target_density = config; | 
|  | 94 | target_density.density = density_value_iter->second; | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 95 |  | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 96 | ResourceConfigValue* best_value = nullptr; | 
|  | 97 | for (ResourceConfigValue* this_value : related_values) { | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 98 | if (!best_value || this_value->config.isBetterThan(best_value->config, &target_density)) { | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 99 | best_value = this_value; | 
|  | 100 | } | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 101 | } | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 102 | CHECK(best_value != nullptr); | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 103 |  | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 104 | // When we select one of these, they are all claimed such that the base | 
|  | 105 | // doesn't include any anymore. | 
|  | 106 | (*claimed_values)[best_value] = true; | 
|  | 107 | selected.push_back(best_value); | 
|  | 108 | } | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 109 | } | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 110 | return selected; | 
|  | 111 | } | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 112 |  | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 113 | private: | 
|  | 114 | DISALLOW_COPY_AND_ASSIGN(SplitValueSelector); | 
|  | 115 |  | 
|  | 116 | std::set<ConfigDescription> density_independent_configs_; | 
|  | 117 | std::map<ConfigDescription, uint16_t> | 
|  | 118 | density_dependent_config_to_density_map_; | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 119 | }; | 
|  | 120 |  | 
|  | 121 | /** | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 122 | * Marking non-preferred densities as claimed will make sure the base doesn't include them, leaving | 
|  | 123 | * only the preferred density behind. | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 124 | */ | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 125 | static void MarkNonPreferredDensitiesAsClaimed( | 
| Pierre Lecesne | 672384b | 2017-02-06 10:29:02 +0000 | [diff] [blame] | 126 | const std::vector<uint16_t>& preferred_densities, const ConfigDensityGroups& density_groups, | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 127 | ConfigClaimedMap* config_claimed_map) { | 
|  | 128 | for (auto& entry : density_groups) { | 
|  | 129 | const ConfigDescription& config = entry.first; | 
|  | 130 | const std::vector<ResourceConfigValue*>& related_values = entry.second; | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 131 |  | 
| Pierre Lecesne | 672384b | 2017-02-06 10:29:02 +0000 | [diff] [blame] | 132 | // There can be multiple best values if there are multiple preferred densities. | 
|  | 133 | std::unordered_set<ResourceConfigValue*> best_values; | 
|  | 134 |  | 
|  | 135 | // For each preferred density, find the value that is the best. | 
|  | 136 | for (uint16_t preferred_density : preferred_densities) { | 
|  | 137 | ConfigDescription target_density = config; | 
|  | 138 | target_density.density = preferred_density; | 
|  | 139 | ResourceConfigValue* best_value = nullptr; | 
|  | 140 | for (ResourceConfigValue* this_value : related_values) { | 
|  | 141 | if (!best_value || this_value->config.isBetterThan(best_value->config, &target_density)) { | 
|  | 142 | best_value = this_value; | 
|  | 143 | } | 
|  | 144 | } | 
|  | 145 | CHECK(best_value != nullptr); | 
|  | 146 | best_values.insert(best_value); | 
|  | 147 | } | 
|  | 148 |  | 
|  | 149 | // Claim all the values that aren't the best so that they will be removed from the base. | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 150 | for (ResourceConfigValue* this_value : related_values) { | 
| Pierre Lecesne | 672384b | 2017-02-06 10:29:02 +0000 | [diff] [blame] | 151 | if (best_values.find(this_value) == best_values.end()) { | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 152 | (*config_claimed_map)[this_value] = true; | 
|  | 153 | } | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 154 | } | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 155 | } | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 156 | } | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 157 | bool TableSplitter::VerifySplitConstraints(IAaptContext* context) { | 
| Fabien Sanglard | 2d34e76 | 2019-02-21 15:13:29 -0800 | [diff] [blame] | 158 | TRACE_CALL(); | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 159 | bool error = false; | 
|  | 160 | for (size_t i = 0; i < split_constraints_.size(); i++) { | 
| Todd Kennedy | 9fbdf89 | 2018-08-28 16:31:15 -0700 | [diff] [blame] | 161 | if (split_constraints_[i].configs.size() == 0) { | 
|  | 162 | // For now, treat this as a warning. We may consider aborting processing. | 
|  | 163 | context->GetDiagnostics()->Warn(DiagMessage() | 
|  | 164 | << "no configurations for constraint '" | 
|  | 165 | << split_constraints_[i].name << "'"); | 
|  | 166 | } | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 167 | for (size_t j = i + 1; j < split_constraints_.size(); j++) { | 
|  | 168 | for (const ConfigDescription& config : split_constraints_[i].configs) { | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 169 | if (split_constraints_[j].configs.find(config) != split_constraints_[j].configs.end()) { | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 170 | context->GetDiagnostics()->Error(DiagMessage() | 
|  | 171 | << "config '" << config | 
|  | 172 | << "' appears in multiple splits, " | 
|  | 173 | << "target split ambiguous"); | 
|  | 174 | error = true; | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 175 | } | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 176 | } | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 177 | } | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 178 | } | 
|  | 179 | return !error; | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 180 | } | 
|  | 181 |  | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 182 | void TableSplitter::SplitTable(ResourceTable* original_table) { | 
|  | 183 | const size_t split_count = split_constraints_.size(); | 
|  | 184 | for (auto& pkg : original_table->packages) { | 
|  | 185 | // Initialize all packages for splits. | 
|  | 186 | for (size_t idx = 0; idx < split_count; idx++) { | 
|  | 187 | ResourceTable* split_table = splits_[idx].get(); | 
| Ryan Mitchell | 9634efb | 2021-03-19 14:53:17 -0700 | [diff] [blame] | 188 | split_table->FindOrCreatePackage(pkg->name); | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 189 | } | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 190 |  | 
|  | 191 | for (auto& type : pkg->types) { | 
|  | 192 | if (type->type == ResourceType::kMipmap) { | 
|  | 193 | // Always keep mipmaps. | 
|  | 194 | continue; | 
|  | 195 | } | 
|  | 196 |  | 
|  | 197 | for (auto& entry : type->entries) { | 
|  | 198 | if (options_.config_filter) { | 
|  | 199 | // First eliminate any resource that we definitely don't want. | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 200 | for (std::unique_ptr<ResourceConfigValue>& config_value : entry->values) { | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 201 | if (!options_.config_filter->Match(config_value->config)) { | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 202 | // null out the entry. We will clean up and remove nulls at the end for performance | 
|  | 203 | // reasons. | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 204 | config_value.reset(); | 
|  | 205 | } | 
|  | 206 | } | 
|  | 207 | } | 
|  | 208 |  | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 209 | // Organize the values into two separate buckets. Those that are density-dependent and those | 
|  | 210 | // that are density-independent. One density technically matches all density, it's just that | 
|  | 211 | // some densities match better. So we need to be aware of the full set of densities to make | 
|  | 212 | // this decision. | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 213 | ConfigDensityGroups density_groups; | 
|  | 214 | ConfigClaimedMap config_claimed_map; | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 215 | for (const std::unique_ptr<ResourceConfigValue>& config_value : entry->values) { | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 216 | if (config_value) { | 
|  | 217 | config_claimed_map[config_value.get()] = false; | 
|  | 218 |  | 
|  | 219 | if (config_value->config.density != 0) { | 
|  | 220 | // Create a bucket for this density-dependent config. | 
|  | 221 | density_groups[CopyWithoutDensity(config_value->config)] | 
|  | 222 | .push_back(config_value.get()); | 
|  | 223 | } | 
|  | 224 | } | 
|  | 225 | } | 
|  | 226 |  | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 227 | // First we check all the splits. If it doesn't match one of the splits, we leave it in the | 
|  | 228 | // base. | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 229 | for (size_t idx = 0; idx < split_count; idx++) { | 
|  | 230 | const SplitConstraints& split_constraint = split_constraints_[idx]; | 
|  | 231 | ResourceTable* split_table = splits_[idx].get(); | 
| Ryan Mitchell | efcdb95 | 2021-04-14 17:31:37 -0700 | [diff] [blame] | 232 | CloningValueTransformer cloner(&split_table->string_pool); | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 233 |  | 
|  | 234 | // Select the values we want from this entry for this split. | 
|  | 235 | SplitValueSelector selector(split_constraint); | 
|  | 236 | std::vector<ResourceConfigValue*> selected_values = | 
|  | 237 | selector.SelectValues(density_groups, &config_claimed_map); | 
|  | 238 |  | 
|  | 239 | // No need to do any work if we selected nothing. | 
|  | 240 | if (!selected_values.empty()) { | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 241 | // Create the same resource structure in the split. We do this lazily because we might | 
|  | 242 | // not have actual values for each type/entry. | 
|  | 243 | ResourceTablePackage* split_pkg = split_table->FindPackage(pkg->name); | 
|  | 244 | ResourceTableType* split_type = split_pkg->FindOrCreateType(type->type); | 
| Ryan Mitchell | 9634efb | 2021-03-19 14:53:17 -0700 | [diff] [blame] | 245 | split_type->visibility_level = type->visibility_level; | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 246 |  | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 247 | ResourceEntry* split_entry = split_type->FindOrCreateEntry(entry->name); | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 248 | if (!split_entry->id) { | 
|  | 249 | split_entry->id = entry->id; | 
| Adam Lesinski | 71be705 | 2017-12-12 16:48:07 -0800 | [diff] [blame] | 250 | split_entry->visibility = entry->visibility; | 
| Ryan Mitchell | 54237ff | 2018-12-13 15:44:29 -0800 | [diff] [blame] | 251 | split_entry->overlayable_item = entry->overlayable_item; | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 252 | } | 
|  | 253 |  | 
|  | 254 | // Copy the selected values into the new Split Entry. | 
|  | 255 | for (ResourceConfigValue* config_value : selected_values) { | 
|  | 256 | ResourceConfigValue* new_config_value = | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 257 | split_entry->FindOrCreateValue(config_value->config, config_value->product); | 
| Ryan Mitchell | efcdb95 | 2021-04-14 17:31:37 -0700 | [diff] [blame] | 258 | new_config_value->value = config_value->value->Transform(cloner); | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 259 | } | 
|  | 260 | } | 
|  | 261 | } | 
|  | 262 |  | 
| Pierre Lecesne | 672384b | 2017-02-06 10:29:02 +0000 | [diff] [blame] | 263 | if (!options_.preferred_densities.empty()) { | 
|  | 264 | MarkNonPreferredDensitiesAsClaimed(options_.preferred_densities, | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 265 | density_groups, | 
|  | 266 | &config_claimed_map); | 
|  | 267 | } | 
|  | 268 |  | 
| Shane Farmer | 0a5b201 | 2017-06-22 12:24:12 -0700 | [diff] [blame] | 269 | // All splits are handled, now check to see what wasn't claimed and remove whatever exists | 
|  | 270 | // in other splits. | 
|  | 271 | for (std::unique_ptr<ResourceConfigValue>& config_value : entry->values) { | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 272 | if (config_value && config_claimed_map[config_value.get()]) { | 
|  | 273 | // Claimed, remove from base. | 
|  | 274 | config_value.reset(); | 
|  | 275 | } | 
|  | 276 | } | 
|  | 277 |  | 
|  | 278 | // Now erase all nullptrs. | 
|  | 279 | entry->values.erase( | 
|  | 280 | std::remove(entry->values.begin(), entry->values.end(), nullptr), | 
|  | 281 | entry->values.end()); | 
|  | 282 | } | 
|  | 283 | } | 
|  | 284 | } | 
| Adam Lesinski | 355f285 | 2016-02-13 20:26:45 -0800 | [diff] [blame] | 285 | } | 
|  | 286 |  | 
| Adam Lesinski | ce5e56e | 2016-10-21 17:56:45 -0700 | [diff] [blame] | 287 | }  // namespace aapt |