Skip to content

Commit 1544e4b

Browse files
committed
Manifest merger #92
1 parent 4c71ecb commit 1544e4b

File tree

4 files changed

+377
-0
lines changed

4 files changed

+377
-0
lines changed
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
* Copyright (C) 2022 github.com/REAndroid
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+
package com.reandroid.apk;
17+
18+
import com.reandroid.app.AndroidManifest;
19+
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
20+
import com.reandroid.arsc.chunk.xml.ResXmlAttribute;
21+
import com.reandroid.arsc.chunk.xml.ResXmlElement;
22+
import com.reandroid.utils.StringsUtil;
23+
import com.reandroid.utils.collection.ArrayCollection;
24+
import com.reandroid.utils.collection.CollectionUtil;
25+
import com.reandroid.xml.XMLPath;
26+
27+
import java.util.Iterator;
28+
import java.util.List;
29+
import java.util.Set;
30+
31+
public class AndroidManifestBlockMerger {
32+
33+
private boolean mEnabled;
34+
private boolean mBuildFusedModules;
35+
private AndroidManifestBlockSplitSanitizer mSplitSanitizer;
36+
37+
private final List<String> fusedModuleNameList;
38+
private final Set<XMLPath> excludePaths;
39+
40+
private AndroidManifestBlock baseManifest;
41+
42+
public AndroidManifestBlockMerger() {
43+
this.mEnabled = true;
44+
this.mBuildFusedModules = true;
45+
this.mSplitSanitizer = new AndroidManifestBlockSplitSanitizer();
46+
47+
this.fusedModuleNameList = new ArrayCollection<>();
48+
this.excludePaths = CollectionUtil.asHashSet(
49+
XMLPath.compile("/manifest/uses-split"),
50+
XMLPath.compile("/manifest/uses-split")
51+
);
52+
}
53+
54+
public AndroidManifestBlockMerger initializeBase(AndroidManifestBlock base) {
55+
if (this.baseManifest != null && this.baseManifest != base) {
56+
throw new IllegalArgumentException("Base manifest already initialized");
57+
}
58+
this.baseManifest = base;
59+
addFusedModuleName("base");
60+
this.addFusedModules(base);
61+
AndroidManifestBlockSplitSanitizer sanitizer = getSplitSanitizer();
62+
if (sanitizer != null) {
63+
sanitizer.sanitize(base);
64+
}
65+
return this;
66+
}
67+
68+
public AndroidManifestBlockMerger setEnabled(boolean enabled) {
69+
this.mEnabled = enabled;
70+
return this;
71+
}
72+
public AndroidManifestBlockMerger setBuildFusedModules(boolean buildFusedModules) {
73+
this.mBuildFusedModules = buildFusedModules;
74+
return this;
75+
}
76+
public AndroidManifestBlockMerger exclude(XMLPath xmlPath) {
77+
this.excludePaths.add(xmlPath);
78+
return this;
79+
}
80+
public AndroidManifestBlockMerger setSplitSanitizer(AndroidManifestBlockSplitSanitizer sanitizer) {
81+
this.mSplitSanitizer = sanitizer;
82+
return this;
83+
}
84+
public AndroidManifestBlockMerger reset() {
85+
this.baseManifest = null;
86+
this.fusedModuleNameList.clear();
87+
return this;
88+
}
89+
90+
public boolean isEnabled() {
91+
return mEnabled;
92+
}
93+
public boolean isBuildFusedModules() {
94+
return mBuildFusedModules;
95+
}
96+
public AndroidManifestBlock getBaseManifestBlock() {
97+
return baseManifest;
98+
}
99+
public AndroidManifestBlockSplitSanitizer getSplitSanitizer() {
100+
return mSplitSanitizer;
101+
}
102+
103+
public boolean merge(AndroidManifestBlock split) {
104+
if (!isEnabled() || split == null || split.getManifestElement() == null) {
105+
return false;
106+
}
107+
AndroidManifestBlock base = requireBaseManifestInitialized();
108+
if (split == base) {
109+
return false;
110+
}
111+
boolean fusedUpdated = addFusedModules(split);
112+
boolean result = mergeManifestElement(split.getManifestElement());
113+
if (fusedUpdated) {
114+
commitFusedModules();
115+
}
116+
AndroidManifestBlockSplitSanitizer sanitizer = getSplitSanitizer();
117+
if (sanitizer != null) {
118+
result = sanitizer.sanitize(base) || result;
119+
}
120+
if (result) {
121+
base.refresh();
122+
}
123+
return result;
124+
}
125+
private boolean mergeManifestElement(ResXmlElement manifest) {
126+
boolean result = false;
127+
Iterator<ResXmlElement> iterator = manifest.getElements();
128+
while (iterator.hasNext()) {
129+
ResXmlElement element = iterator.next();
130+
if (containsNamedManifestChild(element.getName())) {
131+
result = mergeNamedChild(element) || result;
132+
} else if (element.hasAttribute(AndroidManifest.ID_name)) {
133+
result = addNamedElement(element) || result;
134+
}
135+
}
136+
return result;
137+
}
138+
private boolean mergeNamedChild(ResXmlElement application) {
139+
boolean result = false;
140+
Iterator<ResXmlElement> iterator = application.getElements();
141+
while (iterator.hasNext()) {
142+
ResXmlElement element = iterator.next();
143+
if (element.hasAttribute(AndroidManifest.ID_name)) {
144+
result = addNamedElement(element) || result;
145+
} else {
146+
147+
}
148+
}
149+
return result;
150+
}
151+
private boolean addNamedElement(ResXmlElement element) {
152+
if (isExcluded(element)) {
153+
return false;
154+
}
155+
String nameValue = AndroidManifestBlock.getAndroidNameValue(element);
156+
if (StringsUtil.isEmpty(nameValue)) {
157+
return false;
158+
}
159+
XMLPath xmlPath = XMLPath.of(element);
160+
AndroidManifestBlock base = getBaseManifestBlock();
161+
if (base.getNamedElement(xmlPath, nameValue) != null) {
162+
return false;
163+
}
164+
ResXmlElement result = base.newChildElement(xmlPath);
165+
result.merge(element);
166+
moveAboveApplication(result);
167+
return true;
168+
}
169+
private boolean isExcluded(ResXmlElement element) {
170+
for (XMLPath xmlPath : this.excludePaths) {
171+
if (xmlPath.test(element)) {
172+
return true;
173+
}
174+
}
175+
return false;
176+
}
177+
private void moveAboveApplication(ResXmlElement element) {
178+
if (element.getDepth() != 2) {
179+
return;
180+
}
181+
ResXmlElement manifest = element.getParentElement();
182+
if (!AndroidManifest.TAG_manifest.equals(manifest.getName())) {
183+
return;
184+
}
185+
ResXmlElement application = manifest.getElement(AndroidManifest.TAG_application);
186+
if (application == null || application.getIndex() > element.getIndex()) {
187+
return;
188+
}
189+
manifest.moveTo(element, application.getIndex());
190+
}
191+
private AndroidManifestBlock requireBaseManifestInitialized() {
192+
AndroidManifestBlock baseManifest = this.getBaseManifestBlock();
193+
if (baseManifest == null) {
194+
throw new IllegalArgumentException("Base manifest not initialized");
195+
}
196+
return baseManifest;
197+
}
198+
private void commitFusedModules() {
199+
if (!isBuildFusedModules()) {
200+
return;
201+
}
202+
List<String> fusedModuleNameList = this.fusedModuleNameList;
203+
int size = fusedModuleNameList.size();
204+
if (size == 0 || (size == 1 && fusedModuleNameList.contains("base"))) {
205+
return;
206+
}
207+
ResXmlElement element = getBaseManifestBlock()
208+
.getOrCreateNamedElement(XMLPath.compile("/manifest/application/meta-data"),
209+
AndroidManifest.VALUE_com_android_dynamic_apk_fused_modules);
210+
ResXmlAttribute attribute = element.getOrCreateAndroidAttribute(
211+
AndroidManifest.NAME_value,
212+
AndroidManifest.ID_value);
213+
attribute.setValueAsString(StringsUtil.join(fusedModuleNameList, ","));
214+
}
215+
private boolean addFusedModules(AndroidManifestBlock manifestBlock) {
216+
boolean result = false;
217+
if (isBuildFusedModules()) {
218+
result = addFusedModuleName(manifestBlock.getSplit());
219+
result = addFusedModuleNames(manifestBlock.getFusedModuleNames()) || result;
220+
}
221+
return result;
222+
}
223+
private boolean addFusedModuleNames(String[] names) {
224+
boolean result = false;
225+
if (names != null) {
226+
for (String name : names) {
227+
result = addFusedModuleName(name) || result;
228+
}
229+
}
230+
return result;
231+
}
232+
private boolean addFusedModuleName(String name) {
233+
if (StringsUtil.isEmpty(name)) {
234+
return false;
235+
}
236+
if (name.indexOf(',') >= 0) {
237+
return addFusedModuleNames(StringsUtil.split(name, ','));
238+
} else if (!fusedModuleNameList.contains(name)) {
239+
fusedModuleNameList.add(name);
240+
return true;
241+
}
242+
return false;
243+
}
244+
private static boolean containsNamedManifestChild(String tag) {
245+
return AndroidManifest.TAG_application.equals(tag) ||
246+
AndroidManifest.TAG_queries.equals(tag);
247+
}
248+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright (C) 2022 github.com/REAndroid
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+
package com.reandroid.apk;
17+
18+
import com.reandroid.app.AndroidManifest;
19+
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
20+
import com.reandroid.utils.collection.CollectionUtil;
21+
import com.reandroid.xml.XMLPath;
22+
23+
import java.util.Set;
24+
25+
public class AndroidManifestBlockSplitSanitizer {
26+
27+
private boolean mEnabled;
28+
private final Set<XMLPath> removeAttributeList;
29+
private final Set<XMLPath> removeElementList;
30+
31+
public AndroidManifestBlockSplitSanitizer() {
32+
this.mEnabled = true;
33+
34+
XMLPath manifest = XMLPath.newElement(AndroidManifest.TAG_manifest);
35+
XMLPath application = manifest.element(AndroidManifest.TAG_application);
36+
XMLPath appMetaData = application.element(AndroidManifest.TAG_meta_data);
37+
XMLPath appMetaDataName = appMetaData.attribute(AndroidManifest.ID_name);
38+
39+
this.removeAttributeList = CollectionUtil.asHashSet(
40+
XMLPath.newElement(XMLPath.ANY_ELEMENT_PATH)
41+
.attribute(AndroidManifest.ID_isFeatureSplit)
42+
.alternate(AndroidManifest.ID_splitTypes)
43+
.alternate(AndroidManifest.ID_requiredSplitTypes)
44+
.alternate(AndroidManifest.ID_isSplitRequired)
45+
.alternate(AndroidManifest.ID_isolatedSplits)
46+
.alternate(AndroidManifest.NAME_split)
47+
.alternate(AndroidManifest.NAME_isFeatureSplit)
48+
.alternate(AndroidManifest.NAME_splitTypes)
49+
.alternate(AndroidManifest.NAME_requiredSplitTypes)
50+
.alternate(AndroidManifest.NAME_isolatedSplits)
51+
.alternate(AndroidManifest.NAME_isSplitRequired)
52+
);
53+
54+
this.removeElementList = CollectionUtil.asHashSet(
55+
manifest.element(AndroidManifest.TAG_uses_split),
56+
appMetaDataName.value("com.android.vending.splits.required"),
57+
appMetaDataName.value("com.android.vending.splits"),
58+
appMetaDataName.value("com.android.stamp.source"),
59+
appMetaDataName.value("com.android.stamp.type")
60+
);
61+
}
62+
63+
public void setEnabled(boolean enabled) {
64+
this.mEnabled = enabled;
65+
}
66+
67+
public AndroidManifestBlockSplitSanitizer addAttribute(XMLPath xmlPath) {
68+
removeAttributeList.add(xmlPath);
69+
return this;
70+
}
71+
public AndroidManifestBlockSplitSanitizer addElement(XMLPath xmlPath) {
72+
removeAttributeList.add(xmlPath);
73+
return this;
74+
}
75+
76+
public boolean isEnabled() {
77+
return mEnabled;
78+
}
79+
80+
public boolean sanitize(AndroidManifestBlock manifestBlock) {
81+
if (!isEnabled()) {
82+
return false;
83+
}
84+
if (manifestBlock == null || manifestBlock.getManifestElement() == null) {
85+
return false;
86+
}
87+
boolean result;
88+
result = removeAttributes(manifestBlock);
89+
result = removeElements(manifestBlock) || result;
90+
return result;
91+
}
92+
93+
private boolean removeAttributes(AndroidManifestBlock manifestBlock) {
94+
boolean results = false;
95+
for (XMLPath path : removeAttributeList) {
96+
results = manifestBlock.removeAttributes(path) || results;
97+
}
98+
return results;
99+
}
100+
private boolean removeElements(AndroidManifestBlock manifestBlock) {
101+
boolean results = false;
102+
for (XMLPath path : removeElementList) {
103+
results = manifestBlock.removeElements(path) || results;
104+
}
105+
return results;
106+
}
107+
}

0 commit comments

Comments
 (0)