From dd442c665363b77ebf7d460de01d9c5bc6a7fd94 Mon Sep 17 00:00:00 2001 From: Aria Moradi Date: Sun, 28 Mar 2021 13:40:18 +0430 Subject: [PATCH] fix extensions not being installed correctly --- .../ir/armor/tachidesk/impl/Extension.kt | 2 +- .../tachidesk/impl/util/APKExtractor.java | 254 --------- .../armor/tachidesk/impl/util/APKExtractor.kt | 510 ++++++++++++++++++ 3 files changed, 511 insertions(+), 255 deletions(-) delete mode 100644 server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.java create mode 100644 server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.kt diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt index 36f43f6..8392993 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/Extension.kt @@ -120,7 +120,7 @@ fun installAPK(apkName: String): Int { it[extension] = extensionId } } - logger.debug("Installed source ${httpSource.name} with id {httpSource.id}") + logger.debug("Installed source ${httpSource.name} with id ${httpSource.id}") } } else { // multi source val sourceFactory = instance as SourceFactory diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.java b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.java deleted file mode 100644 index 4210d04..0000000 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.java +++ /dev/null @@ -1,254 +0,0 @@ -package ir.armor.tachidesk.impl.util; - -/* - * Copyright (C) Contributors to the Suwayomi project - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -import org.w3c.dom.Document; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import java.io.*; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public class APKExtractor { - // decompressXML -- Parse the 'compressed' binary form of Android XML docs - // such as for AndroidManifest.xml in .apk files - public static int endDocTag = 0x00100101; - public static int startTag = 0x00100102; - public static int endTag = 0x00100103; - - static void prt(String str) { - //System.err.print(str); - } - - public static String decompressXML(byte[] xml) { - - StringBuilder finalXML = new StringBuilder(); - - // Compressed XML file/bytes starts with 24x bytes of data, - // 9 32 bit words in little endian order (LSB first): - // 0th word is 03 00 08 00 - // 3rd word SEEMS TO BE: Offset at then of StringTable - // 4th word is: Number of strings in string table - // WARNING: Sometime I indiscriminently display or refer to word in - // little endian storage format, or in integer format (ie MSB first). - int numbStrings = LEW(xml, 4 * 4); - - // StringIndexTable starts at offset 24x, an array of 32 bit LE offsets - // of the length/string data in the StringTable. - int sitOff = 0x24; // Offset of start of StringIndexTable - - // StringTable, each string is represented with a 16 bit little endian - // character count, followed by that number of 16 bit (LE) (Unicode) - // chars. - int stOff = sitOff + numbStrings * 4; // StringTable follows - // StrIndexTable - - // XMLTags, The XML tag tree starts after some unknown content after the - // StringTable. There is some unknown data after the StringTable, scan - // forward from this point to the flag for the start of an XML start - // tag. - int xmlTagOff = LEW(xml, 3 * 4); // Start from the offset in the 3rd - // word. - // Scan forward until we find the bytes: 0x02011000(x00100102 in normal - // int) - for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) { - if (LEW(xml, ii) == startTag) { - xmlTagOff = ii; - break; - } - } // end of hack, scanning for start of first start tag - - // XML tags and attributes: - // Every XML start and end tag consists of 6 32 bit words: - // 0th word: 02011000 for startTag and 03011000 for endTag - // 1st word: a flag?, like 38000000 - // 2nd word: Line of where this tag appeared in the original source file - // 3rd word: FFFFFFFF ?? - // 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS - // 5th word: StringIndex of Element Name - // (Note: 01011000 in 0th word means end of XML document, endDocTag) - - // Start tags (not end tags) contain 3 more words: - // 6th word: 14001400 meaning?? - // 7th word: Number of Attributes that follow this tag(follow word 8th) - // 8th word: 00000000 meaning?? - - // Attributes consist of 5 words: - // 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF - // 1st word: StringIndex of Attribute Name - // 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId - // used - // 3rd word: Flags? - // 4th word: str ind of attr value again, or ResourceId of value - - // TMP, dump string table to tr for debugging - // tr.addSelect("strings", null); - // for (int ii=0; ii"); - prtIndent(indent, "<" + name + sb + ">"); - indent++; - - } else if (tag0 == endTag) { // XML END TAG - indent--; - off += 6 * 4; // Skip over 6 words of endTag data - String name = compXmlString(xml, sitOff, stOff, nameSi); - finalXML.append(""); - prtIndent(indent, " (line " + startTagLineNo - + "-" + lineNo + ")"); - // tr.parent(); // Step back up the NobTree - - } else if (tag0 == endDocTag) { // END OF XML DOC TAG - break; - - } else { - prt(" Unrecognized tag code '" + Integer.toHexString(tag0) - + "' at offset " + off); - break; - } - } // end of while loop scanning tags and attributes of XML tree - //prt(" end at offset " + off); - return finalXML.toString(); - } // end of decompressXML - - public static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) { - if (strInd < 0) - return null; - int strOff = stOff + LEW(xml, sitOff + strInd * 4); - return compXmlStringAt(xml, strOff); - } - - public static String spaces = " "; - - public static void prtIndent(int indent, String str) { - prt(spaces.substring(0, Math.min(indent * 2, spaces.length())) + str); - } - - // compXmlStringAt -- Return the string stored in StringTable format at - // offset strOff. This offset points to the 16 bit string length, which - // is followed by that number of 16 bit (Unicode) chars. - public static String compXmlStringAt(byte[] arr, int strOff) { - int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff; - byte[] chars = new byte[strLen]; - for (int ii = 0; ii < strLen; ii++) { - chars[ii] = arr[strOff + 2 + ii * 2]; - } - return new String(chars); // Hack, just use 8 byte chars - } // end of compXmlStringAt - - // LEW -- Return value of a Little Endian 32 bit word from the byte array - // at offset off. - public static int LEW(byte[] arr, int off) { - return arr[off + 3] << 24 & 0xff000000 | arr[off + 2] << 16 & 0xff0000 - | arr[off + 1] << 8 & 0xff00 | arr[off] & 0xFF; - } // end of LEW - - public static Document loadXMLFromString(String xml) throws Exception { - DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); - return docBuilder.parse(new InputSource(new StringReader(xml))); - } - - public static String extract_dex_and_read_className(String filePath, String dexPath) throws IOException { - ZipFile zip = null; - - zip = new ZipFile(filePath); - ZipEntry androidManifest = zip.getEntry("AndroidManifest.xml"); - ZipEntry classesDex = zip.getEntry("classes.dex"); - - // write dex file - InputStream dexStream = zip.getInputStream(classesDex); - try (OutputStream os = Files.newOutputStream(Paths.get(dexPath))) { - byte[] buffer = new byte[1024]; - int len; - while ((len = dexStream.read(buffer)) > 0) { - os.write(buffer, 0, len); - } - } - - // read xml file - InputStream is = zip.getInputStream(androidManifest); - byte[] buf = new byte[1024000]; // 100 kb - is.read(buf); - is.close(); - zip.close(); - - String xml = APKExtractor.decompressXML(buf); - try { - Document xmlDoc = loadXMLFromString(xml); - String pkg = xmlDoc.getDocumentElement().getAttribute("package"); - NodeList nodes = xmlDoc.getElementsByTagName("meta-data"); - for (int i = 0; i < nodes.getLength(); i++) { - NamedNodeMap attributes = nodes.item(i).getAttributes(); - System.out.println(attributes.getNamedItem("name").getNodeValue()); - if (attributes.getNamedItem("name").getNodeValue().equals("tachiyomi.extension.class")) - return pkg + attributes.getNamedItem("value").getNodeValue(); - } - } catch (Exception e) { - e.printStackTrace(); - } - return ""; - } -} \ No newline at end of file diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.kt new file mode 100644 index 0000000..6318c2e --- /dev/null +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/util/APKExtractor.kt @@ -0,0 +1,510 @@ +package ir.armor.tachidesk.impl.util + +/* +* Copyright (C) Contributors to the Suwayomi project +* +* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import org.w3c.dom.Document +import org.xml.sax.InputSource +import java.io.IOException +import java.io.StringReader +import java.nio.file.Files +import java.nio.file.Paths +import java.util.zip.ZipFile +import javax.xml.parsers.DocumentBuilderFactory + +object APKExtractor { + // decompressXML -- Parse the 'compressed' binary form of Android XML docs + // such as for AndroidManifest.xml in .apk files + var endDocTag = 0x00100101 + var startTag = 0x00100102 + var endTag = 0x00100103 + fun prt(str: String?) { + // System.err.print(str); + } + + fun decompressXML(xml: ByteArray): String { + val finalXML = StringBuilder() + + // Compressed XML file/bytes starts with 24x bytes of data, + // 9 32 bit words in little endian order (LSB first): + // 0th word is 03 00 08 00 + // 3rd word SEEMS TO BE: Offset at then of StringTable + // 4th word is: Number of strings in string table + // WARNING: Sometime I indiscriminently display or refer to word in + // little endian storage format, or in integer format (ie MSB first). + val numbStrings = LEW(xml, 4 * 4) + + // StringIndexTable starts at offset 24x, an array of 32 bit LE offsets + // of the length/string data in the StringTable. + val sitOff = 0x24 // Offset of start of StringIndexTable + + // StringTable, each string is represented with a 16 bit little endian + // character count, followed by that number of 16 bit (LE) (Unicode) + // chars. + val stOff = sitOff + numbStrings * 4 // StringTable follows + // StrIndexTable + + // XMLTags, The XML tag tree starts after some unknown content after the + // StringTable. There is some unknown data after the StringTable, scan + // forward from this point to the flag for the start of an XML start + // tag. + var xmlTagOff = LEW(xml, 3 * 4) // Start from the offset in the 3rd + // word. + // Scan forward until we find the bytes: 0x02011000(x00100102 in normal + // int) + var ii = xmlTagOff + while (ii < xml.size - 4) { + if (LEW(xml, ii) == startTag) { + xmlTagOff = ii + break + } + ii += 4 + } + + // XML tags and attributes: + // Every XML start and end tag consists of 6 32 bit words: + // 0th word: 02011000 for startTag and 03011000 for endTag + // 1st word: a flag?, like 38000000 + // 2nd word: Line of where this tag appeared in the original source file + // 3rd word: FFFFFFFF ?? + // 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS + // 5th word: StringIndex of Element Name + // (Note: 01011000 in 0th word means end of XML document, endDocTag) + + // Start tags (not end tags) contain 3 more words: + // 6th word: 14001400 meaning?? + // 7th word: Number of Attributes that follow this tag(follow word 8th) + // 8th word: 00000000 meaning?? + + // Attributes consist of 5 words: + // 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF + // 1st word: StringIndex of Attribute Name + // 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId + // used + // 3rd word: Flags? + // 4th word: str ind of attr value again, or ResourceId of value + + // TMP, dump string table to tr for debugging + // tr.addSelect("strings", null); + // for (int ii=0; ii") + prtIndent(indent, "<$name$sb>") + indent++ + } else if (tag0 == endTag) { // XML END TAG + indent-- + off += 6 * 4 // Skip over 6 words of endTag data + val name = compXmlString(xml, sitOff, stOff, nameSi) + finalXML.append("") + prtIndent( + indent, + " (line " + startTagLineNo + + "-" + lineNo + ")" + ) + // tr.parent(); // Step back up the NobTree + } else if (tag0 == endDocTag) { // END OF XML DOC TAG + break + } else { + prt( + " Unrecognized tag code '" + Integer.toHexString(tag0) + + "' at offset " + off + ) + break + } + } // end of while loop scanning tags and attributes of XML tree + // prt(" end at offset " + off); + return finalXML.toString() + } // end of decompressXML + + fun compXmlString(xml: ByteArray, sitOff: Int, stOff: Int, strInd: Int): String? { + if (strInd < 0) return null + val strOff = stOff + LEW(xml, sitOff + strInd * 4) + return compXmlStringAt(xml, strOff) + } + + var spaces = " " + fun prtIndent(indent: Int, str: String) { + prt(spaces.substring(0, Math.min(indent * 2, spaces.length)) + str) + } + + // compXmlStringAt -- Return the string stored in StringTable format at + // offset strOff. This offset points to the 16 bit string length, which + // is followed by that number of 16 bit (Unicode) chars. + fun compXmlStringAt(arr: ByteArray, strOff: Int): String { + val strLen: Int = arr[strOff + 1].toInt() shl 8 and 0xff00 or arr[strOff].toInt() and 0xff + val chars = ByteArray(strLen) + for (ii in 0 until strLen) { + chars[ii] = arr[strOff + 2 + ii * 2] + } + return String(chars) // Hack, just use 8 byte chars + } // end of compXmlStringAt + + // LEW -- Return value of a Little Endian 32 bit word from the byte array + // at offset off. + fun LEW(arr: ByteArray, off: Int): Int { + + return (arr[off + 3].toInt() shl 24) and -0x1000000 or + (arr[off + 2].toInt() shl 16 and 0xff0000) or + (arr[off + 1].toInt() shl 8 and 0xff00) or + (arr[off].toInt() and 0xFF) + } // end of LEW + + @Throws(Exception::class) + fun loadXMLFromString(xml: String?): Document { + val docBuilderFactory = DocumentBuilderFactory.newInstance() + val docBuilder = docBuilderFactory.newDocumentBuilder() + return docBuilder.parse(InputSource(StringReader(xml))) + } + + @Throws(IOException::class) + fun extract_dex_and_read_className(filePath: String?, dexPath: String?): String { + var zip: ZipFile? = null + zip = ZipFile(filePath) + val androidManifest = zip.getEntry("AndroidManifest.xml") + val classesDex = zip.getEntry("classes.dex") + + // write dex file + val dexStream = zip.getInputStream(classesDex) + Files.newOutputStream(Paths.get(dexPath)).use { os -> + val buffer = ByteArray(1024) + var len: Int + while (dexStream.read(buffer).also { len = it } > 0) { + os.write(buffer, 0, len) + } + } + + // read xml file + val `is` = zip.getInputStream(androidManifest) + val buf = ByteArray(1024000) // 100 kb + `is`.read(buf) + `is`.close() + zip.close() + val xml = decompressXML(buf) + try { + val xmlDoc = loadXMLFromString(xml) + val pkg = xmlDoc.documentElement.getAttribute("package") + val nodes = xmlDoc.getElementsByTagName("meta-data") + for (i in 0 until nodes.length) { + val attributes = nodes.item(i).attributes + println(attributes.getNamedItem("name").nodeValue) + if (attributes.getNamedItem("name").nodeValue == "tachiyomi.extension.class") return pkg + attributes.getNamedItem("value").nodeValue + } + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } +} + +// original Java code + +// package ir.armor.tachidesk.impl.util; +// +// /* +// * Copyright (C) Contributors to the Suwayomi project +// * +// * This Source Code Form is subject to the terms of the Mozilla Public +// * License, v. 2.0. If a copy of the MPL was not distributed with this +// * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +// +// import org.w3c.dom.Document; +// import org.w3c.dom.NamedNodeMap; +// import org.w3c.dom.NodeList; +// import org.xml.sax.InputSource; +// +// import javax.xml.parsers.DocumentBuilder; +// import javax.xml.parsers.DocumentBuilderFactory; +// import java.io.*; +// import java.nio.file.Files; +// import java.nio.file.Paths; +// import java.util.zip.ZipEntry; +// import java.util.zip.ZipFile; +// +// public class APKExtractor { +// // decompressXML -- Parse the 'compressed' binary form of Android XML docs +// // such as for AndroidManifest.xml in .apk files +// public static int endDocTag = 0x00100101; +// public static int startTag = 0x00100102; +// public static int endTag = 0x00100103; +// +// static void prt(String str) { +// //System.err.print(str); +// } +// +// public static String decompressXML(byte[] xml) { +// +// StringBuilder finalXML = new StringBuilder(); +// +// // Compressed XML file/bytes starts with 24x bytes of data, +// // 9 32 bit words in little endian order (LSB first): +// // 0th word is 03 00 08 00 +// // 3rd word SEEMS TO BE: Offset at then of StringTable +// // 4th word is: Number of strings in string table +// // WARNING: Sometime I indiscriminently display or refer to word in +// // little endian storage format, or in integer format (ie MSB first). +// int numbStrings = LEW(xml, 4 * 4); +// +// // StringIndexTable starts at offset 24x, an array of 32 bit LE offsets +// // of the length/string data in the StringTable. +// int sitOff = 0x24; // Offset of start of StringIndexTable +// +// // StringTable, each string is represented with a 16 bit little endian +// // character count, followed by that number of 16 bit (LE) (Unicode) +// // chars. +// int stOff = sitOff + numbStrings * 4; // StringTable follows +// // StrIndexTable +// +// // XMLTags, The XML tag tree starts after some unknown content after the +// // StringTable. There is some unknown data after the StringTable, scan +// // forward from this point to the flag for the start of an XML start +// // tag. +// int xmlTagOff = LEW(xml, 3 * 4); // Start from the offset in the 3rd +// // word. +// // Scan forward until we find the bytes: 0x02011000(x00100102 in normal +// // int) +// for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) { +// if (LEW(xml, ii) == startTag) { +// xmlTagOff = ii; +// break; +// } +// } // end of hack, scanning for start of first start tag +// +// // XML tags and attributes: +// // Every XML start and end tag consists of 6 32 bit words: +// // 0th word: 02011000 for startTag and 03011000 for endTag +// // 1st word: a flag?, like 38000000 +// // 2nd word: Line of where this tag appeared in the original source file +// // 3rd word: FFFFFFFF ?? +// // 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS +// // 5th word: StringIndex of Element Name +// // (Note: 01011000 in 0th word means end of XML document, endDocTag) +// +// // Start tags (not end tags) contain 3 more words: +// // 6th word: 14001400 meaning?? +// // 7th word: Number of Attributes that follow this tag(follow word 8th) +// // 8th word: 00000000 meaning?? +// +// // Attributes consist of 5 words: +// // 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF +// // 1st word: StringIndex of Attribute Name +// // 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId +// // used +// // 3rd word: Flags? +// // 4th word: str ind of attr value again, or ResourceId of value +// +// // TMP, dump string table to tr for debugging +// // tr.addSelect("strings", null); +// // for (int ii=0; ii"); +// prtIndent(indent, "<" + name + sb + ">"); +// indent++; +// +// } else if (tag0 == endTag) { // XML END TAG +// indent--; +// off += 6 * 4; // Skip over 6 words of endTag data +// String name = compXmlString(xml, sitOff, stOff, nameSi); +// finalXML.append(""); +// prtIndent(indent, " (line " + startTagLineNo +// + "-" + lineNo + ")"); +// // tr.parent(); // Step back up the NobTree +// +// } else if (tag0 == endDocTag) { // END OF XML DOC TAG +// break; +// +// } else { +// prt(" Unrecognized tag code '" + Integer.toHexString(tag0) +// + "' at offset " + off); +// break; +// } +// } // end of while loop scanning tags and attributes of XML tree +// //prt(" end at offset " + off); +// return finalXML.toString(); +// } // end of decompressXML +// +// public static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) { +// if (strInd < 0) +// return null; +// int strOff = stOff + LEW(xml, sitOff + strInd * 4); +// return compXmlStringAt(xml, strOff); +// } +// +// public static String spaces = " "; +// +// public static void prtIndent(int indent, String str) { +// prt(spaces.substring(0, Math.min(indent * 2, spaces.length())) + str); +// } +// +// // compXmlStringAt -- Return the string stored in StringTable format at +// // offset strOff. This offset points to the 16 bit string length, which +// // is followed by that number of 16 bit (Unicode) chars. +// public static String compXmlStringAt(byte[] arr, int strOff) { +// int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff; +// byte[] chars = new byte[strLen]; +// for (int ii = 0; ii < strLen; ii++) { +// chars[ii] = arr[strOff + 2 + ii * 2]; +// } +// return new String(chars); // Hack, just use 8 byte chars +// } // end of compXmlStringAt +// +// // LEW -- Return value of a Little Endian 32 bit word from the byte array +// // at offset off. +// public static int LEW(byte[] arr, int off) { +// return ((arr[off + 3] << 24) & 0xff000000) | +// ((arr[off + 2] << 16) & 0xff0000) | +// ((arr[off + 1] << 8) & 0xff00) | +// (arr[off] & 0xFF); +// } // end of LEW +// +// public static Document loadXMLFromString(String xml) throws Exception { +// DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); +// DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); +// return docBuilder.parse(new InputSource(new StringReader(xml))); +// } +// +// public static String extract_dex_and_read_className(String filePath, String dexPath) throws IOException { +// ZipFile zip = null; +// +// zip = new ZipFile(filePath); +// ZipEntry androidManifest = zip.getEntry("AndroidManifest.xml"); +// ZipEntry classesDex = zip.getEntry("classes.dex"); +// +// // write dex file +// InputStream dexStream = zip.getInputStream(classesDex); +// try (OutputStream os = Files.newOutputStream(Paths.get(dexPath))) { +// byte[] buffer = new byte[1024]; +// int len; +// while ((len = dexStream.read(buffer)) > 0) { +// os.write(buffer, 0, len); +// } +// } +// +// // read xml file +// InputStream is = zip.getInputStream(androidManifest); +// byte[] buf = new byte[1024000]; // 100 kb +// is.read(buf); +// is.close(); +// zip.close(); +// +// String xml = APKExtractor.decompressXML(buf); +// try { +// Document xmlDoc = loadXMLFromString(xml); +// String pkg = xmlDoc.getDocumentElement().getAttribute("package"); +// NodeList nodes = xmlDoc.getElementsByTagName("meta-data"); +// for (int i = 0; i < nodes.getLength(); i++) { +// NamedNodeMap attributes = nodes.item(i).getAttributes(); +// System.out.println(attributes.getNamedItem("name").getNodeValue()); +// if (attributes.getNamedItem("name").getNodeValue().equals("tachiyomi.extension.class")) +// return pkg + attributes.getNamedItem("value").getNodeValue(); +// } +// } catch (Exception e) { +// e.printStackTrace(); +// } +// return ""; +// } +// }