From 2dff0e0825b3b165e7d9ea8b2fcf87efe219b891 Mon Sep 17 00:00:00 2001 From: REAndroid Date: Tue, 1 Oct 2024 22:59:26 +0200 Subject: [PATCH] Strong resource protection --- .../reandroid/apkeditor/protect/Confuser.java | 87 ++++++++++++ .../apkeditor/protect/DirectoryConfuser.java | 128 ++++++++++++++++++ .../apkeditor/protect/FileNameConfuser.java | 114 ++++++++++++++++ .../apkeditor/protect/ManifestConfuser.java | 55 ++++++++ .../apkeditor/protect/Protector.java | 126 +++-------------- .../apkeditor/protect/ProtectorOptions.java | 18 +++ .../apkeditor/protect/TableConfuser.java | 86 ++++++++++++ .../apkeditor/utils/CyclicIterator.java | 76 +++++++++++ src/main/resources/strings/strings.properties | 1 + 9 files changed, 586 insertions(+), 105 deletions(-) create mode 100644 src/main/java/com/reandroid/apkeditor/protect/Confuser.java create mode 100644 src/main/java/com/reandroid/apkeditor/protect/DirectoryConfuser.java create mode 100644 src/main/java/com/reandroid/apkeditor/protect/FileNameConfuser.java create mode 100644 src/main/java/com/reandroid/apkeditor/protect/ManifestConfuser.java create mode 100644 src/main/java/com/reandroid/apkeditor/protect/TableConfuser.java create mode 100644 src/main/java/com/reandroid/apkeditor/utils/CyclicIterator.java diff --git a/src/main/java/com/reandroid/apkeditor/protect/Confuser.java b/src/main/java/com/reandroid/apkeditor/protect/Confuser.java new file mode 100644 index 00000000..0101e0aa --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/protect/Confuser.java @@ -0,0 +1,87 @@ +/* + * 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.apkeditor.protect; + +import com.reandroid.apk.APKLogger; +import com.reandroid.apk.ApkModule; +import com.reandroid.archive.InputSource; +import com.reandroid.archive.ZipEntryMap; +import com.reandroid.utils.collection.CollectionUtil; +import com.reandroid.utils.collection.ComputeIterator; + +import java.util.Set; + +public abstract class Confuser implements APKLogger { + + private final Protector protector; + private final String logTag; + private Set filePaths; + + public Confuser(Protector protector, String logTag) { + this.protector = protector; + this.logTag = logTag; + } + + public abstract void confuse(); + + + public boolean containsFilePath(String path) { + return getFilePaths().contains(path); + } + public void onPathChanged(String original, String newPath) { + Set filePaths = getFilePaths(); + filePaths.add(newPath); + logVerbose(original + " -> " + newPath); + } + public Set getFilePaths() { + if (this.filePaths == null) { + + ZipEntryMap zipEntryMap = getApkModule().getZipEntryMap(); + + this.filePaths = CollectionUtil.toHashSet( + ComputeIterator.of(zipEntryMap.iterator(), InputSource::getAlias)); + this.filePaths.addAll(CollectionUtil.toHashSet( + ComputeIterator.of(zipEntryMap.iterator(), InputSource::getName))); + + } + return filePaths; + } + public boolean isKeepType(String type) { + return getOptions().keepTypes.contains(type); + } + public Protector getProtector() { + return protector; + } + public ProtectorOptions getOptions() { + return getProtector().getOptions(); + } + public ApkModule getApkModule() { + return getProtector().getApkModule(); + } + + @Override + public void logMessage(String msg) { + protector.logMessage(logTag + msg); + } + @Override + public void logError(String msg, Throwable tr) { + protector.logError(msg, tr); + } + @Override + public void logVerbose(String msg) { + protector.logVerbose(msg); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/protect/DirectoryConfuser.java b/src/main/java/com/reandroid/apkeditor/protect/DirectoryConfuser.java new file mode 100644 index 00000000..1f1af533 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/protect/DirectoryConfuser.java @@ -0,0 +1,128 @@ +/* + * 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.apkeditor.protect; + +import com.reandroid.apk.ApkModule; +import com.reandroid.apk.DexFileInputSource; +import com.reandroid.apk.ResFile; +import com.reandroid.apk.UncompressedFiles; +import com.reandroid.apkeditor.utils.CyclicIterator; +import com.reandroid.archive.Archive; +import com.reandroid.utils.collection.CollectionUtil; + +import java.util.List; + +public class DirectoryConfuser extends Confuser { + + private final CyclicIterator namesIterator; + + public DirectoryConfuser(Protector protector) { + super(protector, "DirectoryConfuser: "); + this.namesIterator = new CyclicIterator<>(loadDirNameList(protector.getApkModule())); + } + + @Override + public void confuse() { + logMessage("Confusing ..."); + + ApkModule apkModule = getApkModule(); + UncompressedFiles uf = apkModule.getUncompressedFiles(); + + for(ResFile resFile : getApkModule().listResFiles()){ + int method = resFile.getInputSource().getMethod(); + String pathNew = generateNewPath(resFile); + if(pathNew != null) { + String path = resFile.getFilePath(); + if(method == Archive.STORED) { + uf.replacePath(path, pathNew); + } + resFile.setFilePath(pathNew); + onPathChanged(path, pathNew); + } + } + } + private String generateNewPath(ResFile resFile) { + if (isKeepType(resFile.pickOne().getTypeName())) { + return null; + } + return generateNewPath(resFile.getFilePath()); + } + private String generateNewPath(String path) { + CyclicIterator iterator = this.namesIterator; + iterator.resetCycleCount(); + while (iterator.getCycleCount() == 0) { + String newPath = replaceDirectory(path, iterator.next()); + if (!containsFilePath(newPath)) { + return newPath; + } + } + return null; + } + + private static String replaceDirectory(String path, String dirName) { + int i = path.lastIndexOf('/'); + if (i < 0) { + i = 0; + } else { + i = i + 1; + if (i == path.length()) { + i = i - 1; + } + } + String simpleName = path.substring(i); + if (dirName.length() != 0) { + dirName = dirName + "/"; + } + return dirName + simpleName; + } + private static String[] loadDirNameList(ApkModule apkModule) { + List nameList = CollectionUtil.asList( + "AndroidManifest.xml", + "/AndroidManifest.xml", + "resources.arsc", + "/resources.arsc", + "classes.dex", + "/classes.dex", + "kotlin", + "META-INF", + "", + "kotlin/annotation", + "kotlin/collections", + "kotlin/coroutines", + "kotlin/internal", + "kotlin/ranges", + "kotlin/reflect", + "res/values/arrays.xml", + "res/values/attrs.xml", + "res/values/bools.xml", + "res/values/colors.xml", + "res/values/dimens.xml", + "res/values/drawables.xml", + "res/values/ids.xml", + "res/values/integers.xml", + "res/values/plurals.xml", + "res/values/public.xml", + "res/values/strings.xml", + "res/values/styles.xml" + ); + List dexList = apkModule.listDexFiles(); + int size = dexList.size(); + for (int i = 1; i < size; i++) { + nameList.add(dexList.get(i).getAlias()); + } + return nameList.toArray(new String[0]); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/protect/FileNameConfuser.java b/src/main/java/com/reandroid/apkeditor/protect/FileNameConfuser.java new file mode 100644 index 00000000..045e140f --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/protect/FileNameConfuser.java @@ -0,0 +1,114 @@ +/* + * 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.apkeditor.protect; + +import com.reandroid.apk.ApkModule; +import com.reandroid.apk.ResFile; +import com.reandroid.apk.UncompressedFiles; +import com.reandroid.apkeditor.utils.CyclicIterator; +import com.reandroid.archive.Archive; + +public class FileNameConfuser extends Confuser { + + private final CyclicIterator namesIterator; + + public FileNameConfuser(Protector protector) { + super(protector, "FileNameConfuser: "); + this.namesIterator = new CyclicIterator<>(loadFileNames()); + } + + @Override + public void confuse() { + logMessage("Confusing ..."); + + ApkModule apkModule = getApkModule(); + UncompressedFiles uf = apkModule.getUncompressedFiles(); + + for(ResFile resFile : getApkModule().listResFiles()){ + int method = resFile.getInputSource().getMethod(); + String pathNew = generateNewPath(resFile); + if(pathNew != null) { + String path = resFile.getFilePath(); + if(method == Archive.STORED) { + uf.replacePath(path, pathNew); + } + resFile.setFilePath(pathNew); + onPathChanged(path, pathNew); + } + } + } + private String generateNewPath(ResFile resFile) { + if (isKeepType(resFile.pickOne().getTypeName())) { + return null; + } + return generateNewPath(resFile.getFilePath()); + } + + private String generateNewPath(String path) { + CyclicIterator iterator = this.namesIterator; + iterator.resetCycleCount(); + while (iterator.getCycleCount() == 0) { + String newPath = replaceSimpleName(path, iterator.next()); + if (!containsFilePath(newPath)) { + return newPath; + } + } + return null; + } + + private static String replaceSimpleName(String path, String symbol) { + int i = path.lastIndexOf('/'); + String dirName; + String simpleName; + if (i < 0) { + dirName = ""; + simpleName = path; + } else { + i = i + 1; + dirName = path.substring(0, i); + simpleName = path.substring(i); + } + i = simpleName.lastIndexOf('.'); + String ext; + if (i < 0) { + ext = ""; + } else { + if (simpleName.endsWith(".9.png")) { + ext = ".9.png"; + } else { + ext = simpleName.substring(i); + } + } + return dirName + symbol + ext; + } + private static String[] loadFileNames() { + return new String[]{ + ".", + "//", + "///", + "////", + "\\\\", + "\\\\\\", + "\\/", + " ", + " ", + "classes.dex", + "AndroidManifest.xml", + "AndroidManifest", + "resources.arsc", + }; + } +} diff --git a/src/main/java/com/reandroid/apkeditor/protect/ManifestConfuser.java b/src/main/java/com/reandroid/apkeditor/protect/ManifestConfuser.java new file mode 100644 index 00000000..a8a90885 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/protect/ManifestConfuser.java @@ -0,0 +1,55 @@ +/* + * 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.apkeditor.protect; + +import com.reandroid.apk.ApkModule; +import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; +import com.reandroid.arsc.chunk.xml.ResXmlAttribute; +import com.reandroid.arsc.chunk.xml.ResXmlElement; +import com.reandroid.utils.collection.CollectionUtil; + +import java.util.List; +import java.util.Random; + +public class ManifestConfuser extends Confuser { + + public ManifestConfuser(Protector protector) { + super(protector, "ManifestConfuser: "); + } + + @Override + public void confuse() { + if (getOptions().skipManifest) { + logMessage("Skip"); + return; + } + ApkModule apkModule = getApkModule(); + AndroidManifestBlock manifestBlock = apkModule.getAndroidManifest(); + int defaultAttributeSize = 20; + List elementList = CollectionUtil.toList(manifestBlock.recursiveElements()); + Random random = new Random(); + for (ResXmlElement element : elementList) { + int size = defaultAttributeSize + random.nextInt(6) + 1; + element.setAttributesUnitSize(size, false); + ResXmlAttribute attribute = element.newAttribute(); + attribute.setName(" >\n \n android:name", 0); + attribute.setValueAsBoolean(false); + } + manifestBlock.getManifestElement().setAttributesUnitSize( + defaultAttributeSize, false); + manifestBlock.refresh(); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/protect/Protector.java b/src/main/java/com/reandroid/apkeditor/protect/Protector.java index 3386c278..3974277f 100644 --- a/src/main/java/com/reandroid/apkeditor/protect/Protector.java +++ b/src/main/java/com/reandroid/apkeditor/protect/Protector.java @@ -15,33 +15,33 @@ */ package com.reandroid.apkeditor.protect; -import com.reandroid.apkeditor.APKEditor; import com.reandroid.apkeditor.CommandExecutor; import com.reandroid.apkeditor.Util; -import com.reandroid.archive.Archive; -import com.reandroid.arsc.ARSCLib; -import com.reandroid.arsc.chunk.UnknownChunk; -import com.reandroid.arsc.chunk.xml.AndroidManifestBlock; -import com.reandroid.arsc.chunk.xml.ResXmlElement; -import com.reandroid.arsc.item.ByteArray; -import com.reandroid.arsc.item.FixedLengthString; import com.reandroid.apk.*; -import com.reandroid.arsc.chunk.PackageBlock; -import com.reandroid.arsc.chunk.TableBlock; -import com.reandroid.arsc.container.SpecTypePair; -import com.reandroid.arsc.value.Entry; -import com.reandroid.arsc.value.ResConfig; import java.io.IOException; -import java.util.Iterator; -import java.util.Random; public class Protector extends CommandExecutor { + private ApkModule mApkModule; + public Protector(ProtectorOptions options) { super(options, "[PROTECT] "); } + public ApkModule getApkModule() { + return this.mApkModule; + } + + public void setApkModule(ApkModule apkModule) { + this.mApkModule = apkModule; + } + + @Override + public ProtectorOptions getOptions() { + return super.getOptions(); + } + @Override public void runCommand() throws IOException { ProtectorOptions options = getOptions(); @@ -54,100 +54,16 @@ public void runCommand() throws IOException { logMessage(protect); return; } - confuseAndroidManifest(module); - logMessage("Protecting files .."); - confuseResDir(module); - logMessage("Protecting resource table .."); - confuseByteOffset(module); - confuseResourceTable(module); - Util.addApkEditorInfo(module, Util.EDIT_TYPE_PROTECTED); + setApkModule(module); + new ManifestConfuser(this).confuse(); + new DirectoryConfuser(this).confuse(); + new FileNameConfuser(this).confuse(); + new TableConfuser(this).confuse(); module.getTableBlock().refresh(); logMessage("Writing apk ..."); module.writeApk(options.outputFile); module.close(); logMessage("Saved to: " + options.outputFile); - } - private void confuseAndroidManifest(ApkModule apkModule) { - ProtectorOptions options = getOptions(); - if(options.skipManifest){ - logMessage("Skip AndroidManifest"); - return; - } - logMessage("Confusing AndroidManifest ..."); - AndroidManifestBlock manifestBlock = apkModule.getAndroidManifest(); - manifestBlock.setAttributesUnitSize(25, true); - int defaultAttributeSize = 20; - Iterator iterator = manifestBlock.recursiveElements(); - Random random = new Random(); - while (iterator.hasNext()) { - ResXmlElement element = iterator.next(); - int size = defaultAttributeSize + random.nextInt(6) + 1; - element.setAttributesUnitSize(size, false); - logMessage("ATTR SIZE: " + element.getName() + ": " + size); - } - manifestBlock.getManifestElement().setAttributesUnitSize( - defaultAttributeSize, false); - manifestBlock.refresh(); - } - private void confuseByteOffset(ApkModule apkModule) { - logMessage("METHOD-1 Protecting resource table .."); - TableBlock tableBlock=apkModule.getTableBlock(); - for(PackageBlock packageBlock:tableBlock.listPackages()){ - for(SpecTypePair specTypePair:packageBlock.listSpecTypePairs()){ - for(ResConfig resConfig:specTypePair.listResConfig()){ - resConfig.trimToSize(ResConfig.SIZE_16); - } - } - } - tableBlock.refresh(); - } - private void confuseResourceTable(ApkModule apkModule) { - logMessage("METHOD-2 Protecting resource table .."); - TableBlock tableBlock=apkModule.getTableBlock(); - UnknownChunk unknownChunk = new UnknownChunk(); - FixedLengthString fixedLengthString = new FixedLengthString(256); - fixedLengthString.set(APKEditor.getRepo()); - ByteArray extra = unknownChunk.getHeaderBlock().getExtraBytes(); - byte[] bts = fixedLengthString.getBytes(); - extra.setSize(bts.length); - extra.putByteArray(0, bts); - fixedLengthString.set(ARSCLib.getRepo()); - extra = unknownChunk.getBody(); - bts = fixedLengthString.getBytes(); - extra.setSize(bts.length); - extra.putByteArray(0, bts); - fixedLengthString.set(ARSCLib.getRepo()); - unknownChunk.refresh(); - tableBlock.getFirstPlaceHolder().setItem(unknownChunk); - tableBlock.refresh(); - } - private void confuseResDir(ApkModule apkModule) { - logMessage("Protecting files .."); - String[] dirNames=new String[]{ - "AndroidManifest.xml", - "resources.arsc", - "classes.dex" - }; - UncompressedFiles uf = apkModule.getUncompressedFiles(); - int i=0; - for(ResFile resFile:apkModule.listResFiles()){ - if(i>=dirNames.length){ - i=0; - } - int method=resFile.getInputSource().getMethod(); - String path = resFile.getFilePath(); - Entry entryBlock = resFile.pickOne(); - // TODO: make other solution to decide user which types/dirs to ignore - if(entryBlock!=null && "font".equals(entryBlock.getTypeName())){ - logMessage(" Ignored: "+path); - continue; - } - String pathNew = ApkUtil.replaceRootDir(path, dirNames[i]); - if(method == Archive.STORED) { - uf.replacePath(path, pathNew); - } - resFile.setFilePath(pathNew); - i++; - } + module.close(); } } diff --git a/src/main/java/com/reandroid/apkeditor/protect/ProtectorOptions.java b/src/main/java/com/reandroid/apkeditor/protect/ProtectorOptions.java index 4004005e..7dc2497c 100644 --- a/src/main/java/com/reandroid/apkeditor/protect/ProtectorOptions.java +++ b/src/main/java/com/reandroid/apkeditor/protect/ProtectorOptions.java @@ -20,6 +20,8 @@ import com.reandroid.jcommand.annotations.OptionArg; import java.io.File; +import java.util.HashSet; +import java.util.Set; @CommandOptions( name = "p", @@ -33,10 +35,26 @@ public class ProtectorOptions extends Options { @OptionArg(name = "-skip-manifest", flag = true, description = "protect_skip_manifest") public boolean skipManifest; + @OptionArg(name = "-keep-type", description = "protect_keep_type") + public final Set keepTypes = new HashSet<>(); + public ProtectorOptions() { super(); } + @Override + public void validateValues() { + super.validateValues(); + addDefaultKeepTypes(); + } + private void addDefaultKeepTypes() { + Set keepTypes = this.keepTypes; + if (!keepTypes.isEmpty()) { + return; + } + keepTypes.add("font"); + } + @Override public Protector newCommandExecutor() { return new Protector(this); diff --git a/src/main/java/com/reandroid/apkeditor/protect/TableConfuser.java b/src/main/java/com/reandroid/apkeditor/protect/TableConfuser.java new file mode 100644 index 00000000..b54b085a --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/protect/TableConfuser.java @@ -0,0 +1,86 @@ +/* + * 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.apkeditor.protect; + +import com.reandroid.apk.ApkModule; +import com.reandroid.apkeditor.APKEditor; +import com.reandroid.arsc.ARSCLib; +import com.reandroid.arsc.chunk.PackageBlock; +import com.reandroid.arsc.chunk.TableBlock; +import com.reandroid.arsc.chunk.UnknownChunk; +import com.reandroid.arsc.item.ByteArray; +import com.reandroid.arsc.item.FixedLengthString; +import com.reandroid.arsc.item.TypeString; +import com.reandroid.arsc.pool.TypeStringPool; + +public class TableConfuser extends Confuser { + + public TableConfuser(Protector protector) { + super(protector, "TableConfuser: "); + } + + @Override + public void confuse() { + logMessage("Confusing ..."); + confuseWithUnknownChunk(); + confuseTypeNames(); + } + private void confuseWithUnknownChunk() { + ApkModule apkModule = getApkModule(); + TableBlock tableBlock = apkModule.getTableBlock(); + UnknownChunk unknownChunk = new UnknownChunk(); + FixedLengthString fixedLengthString = new FixedLengthString(256); + fixedLengthString.set(APKEditor.getRepo()); + ByteArray extra = unknownChunk.getHeaderBlock().getExtraBytes(); + byte[] bytes = fixedLengthString.getBytes(); + extra.setSize(bytes.length); + extra.putByteArray(0, bytes); + fixedLengthString.set(ARSCLib.getRepo()); + extra = unknownChunk.getBody(); + bytes = fixedLengthString.getBytes(); + extra.setSize(bytes.length); + extra.putByteArray(0, bytes); + fixedLengthString.set(ARSCLib.getRepo()); + unknownChunk.refresh(); + tableBlock.getFirstPlaceHolder().setItem(unknownChunk); + tableBlock.refresh(); + } + private void confuseTypeNames() { + //TODO: let the user choose which types to confuse, + // and use user provided type names + ApkModule apkModule = getApkModule(); + TableBlock tableBlock = apkModule.getTableBlock(); + for(PackageBlock packageBlock:tableBlock.listPackages()){ + TypeStringPool pool = packageBlock.getTypeStringPool(); + for(TypeString typeString : pool) { + String type = typeString.get(); + if ("attr".equals(type) ) { + typeString.set("style"); + } else if ("style".equals(type)) { + typeString.set("plurals"); + } else if ("id".equals(type)) { + typeString.set("attr"); + } else if ( "mipmap".equals(type)) { + typeString.set("id"); + } else { + continue; + } + logVerbose("'" + type + "' -> '" + typeString.get() + "'"); + } + } + tableBlock.refresh(); + } +} diff --git a/src/main/java/com/reandroid/apkeditor/utils/CyclicIterator.java b/src/main/java/com/reandroid/apkeditor/utils/CyclicIterator.java new file mode 100644 index 00000000..49cc3361 --- /dev/null +++ b/src/main/java/com/reandroid/apkeditor/utils/CyclicIterator.java @@ -0,0 +1,76 @@ +/* + * 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.apkeditor.utils; + +import java.util.Iterator; + +public class CyclicIterator implements Iterator { + + private final T[] elements; + private int mIndex; + private int cycleCount; + private int count; + + public CyclicIterator(T[] elements) { + if (elements == null || elements.length == 0) { + throw new IllegalArgumentException("Elements can not be empty"); + } + this.elements = elements; + } + @Override + public boolean hasNext() { + return elements.length != 0; + } + + @Override + public T next() { + int i = mIndex; + T item = elements[i]; + i ++; + int length = elements.length; + if (i >= length) { + i = 0; + } + this.mIndex = i; + this.count ++; + if (this.count >= length) { + this.count = 0; + this.cycleCount ++; + } + return item; + } + public int length() { + return elements.length; + } + + public int getIndex() { + return mIndex; + } + + public int getCycleCount() { + return cycleCount; + } + public void resetCycleCount() { + this.cycleCount = 0; + this.count = 0; + } + + @Override + public String toString() { + return "[cycle = " + getCycleCount() + ": " + getIndex() + + "/" + length() + "] " + elements[mIndex]; + } +} diff --git a/src/main/resources/strings/strings.properties b/src/main/resources/strings/strings.properties index afd3249e..5ebb04a7 100644 --- a/src/main/resources/strings/strings.properties +++ b/src/main/resources/strings/strings.properties @@ -102,6 +102,7 @@ path_is_file_expect_directory=Path is a file expecting directory: '%s' path_of_framework=Path of framework file (can be multiple). protect_description=Protects/Obfuscates apk resource files. Using unique obfuscation techniques. protect_example_1=[Basic]\n java -jar APKEditor.jar p -i path/input.apk -o path/output.apk +protect_keep_type=Keep specific resource type names (e.g drawable), By default keeps only resource type.\n *Can be multiple protect_skip_manifest=Do not protect manifest. raw_dex=Copy raw dex files / skip smali. res_dir_name=Sets resource files root dir name. e.g. for obfuscation to move files from 'res/*' to 'r/*' or vice versa.