You've already forked android_translation_layer
mirror of
https://gitlab.com/android_translation_layer/android_translation_layer.git
synced 2025-10-27 11:48:10 -07:00
321 lines
12 KiB
Java
321 lines
12 KiB
Java
|
|
/*
|
||
|
|
* Copyright (C) 2022 github.com/REAndroid
|
||
|
|
*
|
||
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
|
* you may not use this file except in compliance with the License.
|
||
|
|
* You may obtain a copy of the License at
|
||
|
|
*
|
||
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
*
|
||
|
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
|
* WITHOUT 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.reandroid.apk;
|
||
|
|
|
||
|
|
import com.reandroid.apk.xmldecoder.*;
|
||
|
|
import com.reandroid.archive.InputSource;
|
||
|
|
import com.reandroid.arsc.chunk.PackageBlock;
|
||
|
|
import com.reandroid.arsc.chunk.TableBlock;
|
||
|
|
import com.reandroid.arsc.chunk.xml.AndroidManifestBlock;
|
||
|
|
import com.reandroid.arsc.chunk.xml.ResXmlDocument;
|
||
|
|
import com.reandroid.arsc.container.SpecTypePair;
|
||
|
|
import com.reandroid.arsc.value.*;
|
||
|
|
import com.reandroid.identifiers.PackageIdentifier;
|
||
|
|
import com.reandroid.json.JSONObject;
|
||
|
|
import com.reandroid.xml.XMLDocument;
|
||
|
|
import org.xmlpull.v1.XmlPullParserException;
|
||
|
|
|
||
|
|
import java.io.File;
|
||
|
|
import java.io.FileOutputStream;
|
||
|
|
import java.io.IOException;
|
||
|
|
import java.util.*;
|
||
|
|
import java.util.function.Predicate;
|
||
|
|
|
||
|
|
public class ApkModuleXmlDecoder extends ApkDecoder implements Predicate<Entry> {
|
||
|
|
private final ApkModule apkModule;
|
||
|
|
private final Map<Integer, Set<ResConfig>> decodedEntries;
|
||
|
|
|
||
|
|
private ResXmlDocumentSerializer documentSerializer;
|
||
|
|
private XMLEntryDecoderSerializer entrySerializer;
|
||
|
|
|
||
|
|
|
||
|
|
public ApkModuleXmlDecoder(ApkModule apkModule){
|
||
|
|
super();
|
||
|
|
this.apkModule = apkModule;
|
||
|
|
this.decodedEntries = new HashMap<>();
|
||
|
|
super.setApkLogger(apkModule.getApkLogger());
|
||
|
|
}
|
||
|
|
public void sanitizeFilePaths(){
|
||
|
|
PathSanitizer sanitizer = PathSanitizer.create(apkModule);
|
||
|
|
sanitizer.sanitize();
|
||
|
|
}
|
||
|
|
@Override
|
||
|
|
void onDecodeTo(File outDir) throws IOException{
|
||
|
|
this.decodedEntries.clear();
|
||
|
|
logMessage("Decoding ...");
|
||
|
|
|
||
|
|
if(!apkModule.hasTableBlock()){
|
||
|
|
logOrThrow(null, new IOException("Don't have resource table"));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
decodeUncompressedFiles(outDir);
|
||
|
|
|
||
|
|
TableBlock tableBlock = apkModule.getTableBlock();
|
||
|
|
|
||
|
|
this.entrySerializer = new XMLEntryDecoderSerializer(tableBlock);
|
||
|
|
this.entrySerializer.setDecodedEntries(this);
|
||
|
|
|
||
|
|
decodeAndroidManifest(outDir, apkModule.getAndroidManifestBlock());
|
||
|
|
decodeTableBlock(outDir, tableBlock);
|
||
|
|
|
||
|
|
logMessage("Decoding resource files ...");
|
||
|
|
List<ResFile> resFileList = apkModule.listResFiles();
|
||
|
|
for(ResFile resFile:resFileList){
|
||
|
|
decodeResFile(outDir, resFile);
|
||
|
|
}
|
||
|
|
decodeValues(outDir, tableBlock);
|
||
|
|
|
||
|
|
extractRootFiles(outDir);
|
||
|
|
|
||
|
|
writePathMap(outDir, apkModule.getApkArchive().listInputSources());
|
||
|
|
|
||
|
|
dumpSignatures(outDir, apkModule.getApkSignatureBlock());
|
||
|
|
}
|
||
|
|
private void decodeTableBlock(File outDir, TableBlock tableBlock) throws IOException {
|
||
|
|
try{
|
||
|
|
decodePackageInfo(outDir, tableBlock);
|
||
|
|
decodePublicXml(tableBlock, outDir);
|
||
|
|
addDecodedPath(TableBlock.FILE_NAME);
|
||
|
|
}catch (IOException exception){
|
||
|
|
logOrThrow("Error decoding resource table", exception);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
private void decodePackageInfo(File outDir, TableBlock tableBlock) throws IOException {
|
||
|
|
for(PackageBlock packageBlock:tableBlock.listPackages()){
|
||
|
|
decodePackageInfo(outDir, packageBlock);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
private void decodePackageInfo(File outDir, PackageBlock packageBlock) throws IOException {
|
||
|
|
File pkgDir = new File(outDir, getPackageDirName(packageBlock));
|
||
|
|
File packageJsonFile = new File(pkgDir, PackageBlock.JSON_FILE_NAME);
|
||
|
|
JSONObject jsonObject = packageBlock.toJson(false);
|
||
|
|
jsonObject.write(packageJsonFile);
|
||
|
|
}
|
||
|
|
private void decodeUncompressedFiles(File outDir)
|
||
|
|
throws IOException {
|
||
|
|
File file=new File(outDir, UncompressedFiles.JSON_FILE);
|
||
|
|
UncompressedFiles uncompressedFiles = apkModule.getUncompressedFiles();
|
||
|
|
uncompressedFiles.toJson().write(file);
|
||
|
|
}
|
||
|
|
private void decodeResFile(File outDir, ResFile resFile)
|
||
|
|
throws IOException{
|
||
|
|
if(resFile.isBinaryXml()){
|
||
|
|
decodeResXml(outDir, resFile);
|
||
|
|
}else {
|
||
|
|
decodeResRaw(outDir, resFile);
|
||
|
|
}
|
||
|
|
addDecodedPath(resFile.getFilePath());
|
||
|
|
}
|
||
|
|
private void decodeResRaw(File outDir, ResFile resFile)
|
||
|
|
throws IOException {
|
||
|
|
Entry entry = resFile.pickOne();
|
||
|
|
PackageBlock packageBlock= entry.getPackageBlock();
|
||
|
|
|
||
|
|
File pkgDir=new File(outDir, getPackageDirName(packageBlock));
|
||
|
|
String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME);
|
||
|
|
String path = alias.replace('/', File.separatorChar);
|
||
|
|
File file=new File(pkgDir, path);
|
||
|
|
File dir=file.getParentFile();
|
||
|
|
if(!dir.exists()){
|
||
|
|
dir.mkdirs();
|
||
|
|
}
|
||
|
|
FileOutputStream outputStream=new FileOutputStream(file);
|
||
|
|
resFile.getInputSource().write(outputStream);
|
||
|
|
outputStream.close();
|
||
|
|
resFile.setFilePath(alias);
|
||
|
|
|
||
|
|
addDecodedEntry(entry);
|
||
|
|
}
|
||
|
|
private void decodeResXml(File outDir, ResFile resFile)
|
||
|
|
throws IOException{
|
||
|
|
Entry entry = resFile.pickOne();
|
||
|
|
PackageBlock packageBlock = entry.getPackageBlock();
|
||
|
|
|
||
|
|
File pkgDir = new File(outDir, getPackageDirName(packageBlock));
|
||
|
|
String alias = resFile.buildPath(ApkUtil.RES_DIR_NAME);
|
||
|
|
String path = alias.replace('/', File.separatorChar);
|
||
|
|
path = path.replace('/', File.separatorChar);
|
||
|
|
File file = new File(pkgDir, path);
|
||
|
|
|
||
|
|
logVerbose("Decoding: " + path);
|
||
|
|
serializeXml(packageBlock.getId(), resFile.getInputSource(), file);
|
||
|
|
|
||
|
|
resFile.setFilePath(alias);
|
||
|
|
addDecodedEntry(entry);
|
||
|
|
}
|
||
|
|
private ResXmlDocumentSerializer getDocumentSerializer(){
|
||
|
|
if(documentSerializer == null){
|
||
|
|
documentSerializer = new ResXmlDocumentSerializer(apkModule);
|
||
|
|
documentSerializer.setValidateXmlNamespace(true);
|
||
|
|
}
|
||
|
|
return documentSerializer;
|
||
|
|
}
|
||
|
|
private void decodePublicXml(TableBlock tableBlock, File outDir)
|
||
|
|
throws IOException{
|
||
|
|
for(PackageBlock packageBlock:tableBlock.listPackages()){
|
||
|
|
decodePublicXml(packageBlock, outDir);
|
||
|
|
}
|
||
|
|
if(tableBlock.getPackageArray().childesCount()==0){
|
||
|
|
decodeEmptyTable(outDir);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
private void decodeEmptyTable(File outDir) throws IOException {
|
||
|
|
logMessage("Decoding empty table ...");
|
||
|
|
String pkgName = apkModule.getPackageName();
|
||
|
|
if(pkgName==null){
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
File pkgDir = new File(outDir, "0-"+pkgName);
|
||
|
|
File resDir = new File(pkgDir, ApkUtil.RES_DIR_NAME);
|
||
|
|
File values = new File(resDir, "values");
|
||
|
|
File pubXml = new File(values, ApkUtil.FILE_NAME_PUBLIC_XML);
|
||
|
|
XMLDocument xmlDocument = new XMLDocument("resources");
|
||
|
|
xmlDocument.save(pubXml, false);
|
||
|
|
}
|
||
|
|
private void decodePublicXml(PackageBlock packageBlock, File outDir)
|
||
|
|
throws IOException {
|
||
|
|
String packageDirName=getPackageDirName(packageBlock);
|
||
|
|
logMessage("Decoding public.xml: "+packageDirName);
|
||
|
|
File file=new File(outDir, packageDirName);
|
||
|
|
file=new File(file, ApkUtil.RES_DIR_NAME);
|
||
|
|
file=new File(file, "values");
|
||
|
|
file=new File(file, ApkUtil.FILE_NAME_PUBLIC_XML);
|
||
|
|
PackageIdentifier packageIdentifier = new PackageIdentifier();
|
||
|
|
packageIdentifier.load(packageBlock);
|
||
|
|
packageIdentifier.writePublicXml(file);
|
||
|
|
}
|
||
|
|
private void decodeAndroidManifest(File outDir, AndroidManifestBlock manifestBlock)
|
||
|
|
throws IOException {
|
||
|
|
if(!apkModule.hasAndroidManifestBlock()){
|
||
|
|
logMessage("Don't have: "+ AndroidManifestBlock.FILE_NAME);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
File file=new File(outDir, AndroidManifestBlock.FILE_NAME);
|
||
|
|
logMessage("Decoding: "+file.getName());
|
||
|
|
int currentPackageId = manifestBlock.guessCurrentPackageId();
|
||
|
|
serializeXml(currentPackageId, manifestBlock, file);
|
||
|
|
addDecodedPath(AndroidManifestBlock.FILE_NAME);
|
||
|
|
}
|
||
|
|
private void serializeXml(int currentPackageId, ResXmlDocument document, File outFile)
|
||
|
|
throws IOException {
|
||
|
|
XMLNamespaceValidator.validateNamespaces(document);
|
||
|
|
ResXmlDocumentSerializer serializer = getDocumentSerializer();
|
||
|
|
if(currentPackageId != 0){
|
||
|
|
serializer.getDecoder().setCurrentPackageId(currentPackageId);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
serializer.write(document, outFile);
|
||
|
|
} catch (XmlPullParserException ex) {
|
||
|
|
throw new IOException("Error: "+outFile.getName(), ex);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
private void serializeXml(int currentPackageId, InputSource inputSource, File outFile)
|
||
|
|
throws IOException {
|
||
|
|
ResXmlDocumentSerializer serializer = getDocumentSerializer();
|
||
|
|
if(currentPackageId != 0){
|
||
|
|
serializer.getDecoder().setCurrentPackageId(currentPackageId);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
serializer.write(inputSource, outFile);
|
||
|
|
} catch (XmlPullParserException ex) {
|
||
|
|
throw new IOException("Error: "+outFile.getName(), ex);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
private void addDecodedEntry(Entry entry){
|
||
|
|
if(entry.isNull()){
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
int resourceId= entry.getResourceId();
|
||
|
|
Set<ResConfig> resConfigSet=decodedEntries.get(resourceId);
|
||
|
|
if(resConfigSet==null){
|
||
|
|
resConfigSet=new HashSet<>();
|
||
|
|
decodedEntries.put(resourceId, resConfigSet);
|
||
|
|
}
|
||
|
|
resConfigSet.add(entry.getResConfig());
|
||
|
|
}
|
||
|
|
private boolean containsDecodedEntry(Entry entry){
|
||
|
|
Set<ResConfig> resConfigSet=decodedEntries.get(entry.getResourceId());
|
||
|
|
if(resConfigSet==null){
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return resConfigSet.contains(entry.getResConfig());
|
||
|
|
}
|
||
|
|
private void decodeValues(File outDir, TableBlock tableBlock) throws IOException {
|
||
|
|
for(PackageBlock packageBlock:tableBlock.listPackages()){
|
||
|
|
decodeValues(outDir, packageBlock);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
private void decodeValues(File outDir, PackageBlock packageBlock) throws IOException {
|
||
|
|
logMessage("Decoding values: "
|
||
|
|
+ getPackageDirName(packageBlock));
|
||
|
|
|
||
|
|
packageBlock.sortTypes();
|
||
|
|
|
||
|
|
File pkgDir = new File(outDir, getPackageDirName(packageBlock));
|
||
|
|
File resDir = new File(pkgDir, ApkUtil.RES_DIR_NAME);
|
||
|
|
|
||
|
|
for(SpecTypePair specTypePair : packageBlock.listSpecTypePairs()){
|
||
|
|
decodeValues(resDir, specTypePair);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
private void decodeValues(File outDir, SpecTypePair specTypePair) throws IOException {
|
||
|
|
entrySerializer.decode(outDir, specTypePair);
|
||
|
|
}
|
||
|
|
private String getPackageDirName(PackageBlock packageBlock){
|
||
|
|
String name = ApkUtil.sanitizeForFileName(packageBlock.getName());
|
||
|
|
if(name==null){
|
||
|
|
name="package";
|
||
|
|
}
|
||
|
|
TableBlock tableBlock = packageBlock.getTableBlock();
|
||
|
|
int index = packageBlock.getIndex();
|
||
|
|
String prefix;
|
||
|
|
if(index < 10 && tableBlock.countPackages() > 10){
|
||
|
|
prefix = "0" + index;
|
||
|
|
}else {
|
||
|
|
prefix = Integer.toString(index);
|
||
|
|
}
|
||
|
|
return prefix + "-" + name;
|
||
|
|
}
|
||
|
|
private void extractRootFiles(File outDir) throws IOException {
|
||
|
|
logMessage("Extracting root files");
|
||
|
|
File rootDir = new File(outDir, "root");
|
||
|
|
for(InputSource inputSource:apkModule.getApkArchive().listInputSources()){
|
||
|
|
if(containsDecodedPath(inputSource.getAlias())){
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
extractRootFiles(rootDir, inputSource);
|
||
|
|
addDecodedPath(inputSource.getAlias());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
private void extractRootFiles(File rootDir, InputSource inputSource) throws IOException {
|
||
|
|
String path=inputSource.getAlias();
|
||
|
|
path=path.replace(File.separatorChar, '/');
|
||
|
|
File file=new File(rootDir, path);
|
||
|
|
File dir=file.getParentFile();
|
||
|
|
if(!dir.exists()){
|
||
|
|
dir.mkdirs();
|
||
|
|
}
|
||
|
|
FileOutputStream outputStream=new FileOutputStream(file);
|
||
|
|
inputSource.write(outputStream);
|
||
|
|
outputStream.close();
|
||
|
|
}
|
||
|
|
@Override
|
||
|
|
public boolean test(Entry entry) {
|
||
|
|
return !containsDecodedEntry(entry);
|
||
|
|
}
|
||
|
|
}
|