mirror of
https://github.com/awfixers-stuff/OwnDroid.git
synced 2026-03-23 11:05:59 +00:00
app installer: get basic apk info
This commit is contained in:
@@ -0,0 +1,635 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* 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.android.apksig.internal.apk;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AndroidBinXmlParser {
|
||||
public static final int EVENT_START_DOCUMENT = 1;
|
||||
public static final int EVENT_END_DOCUMENT = 2;
|
||||
public static final int EVENT_START_ELEMENT = 3;
|
||||
public static final int EVENT_END_ELEMENT = 4;
|
||||
public static final int VALUE_TYPE_UNSUPPORTED = 0;
|
||||
public static final int VALUE_TYPE_STRING = 1;
|
||||
public static final int VALUE_TYPE_INT = 2;
|
||||
public static final int VALUE_TYPE_REFERENCE = 3;
|
||||
public static final int VALUE_TYPE_BOOLEAN = 4;
|
||||
private static final long NO_NAMESPACE = 0xffffffffL;
|
||||
private final ByteBuffer mXml;
|
||||
private StringPool mStringPool;
|
||||
private ResourceMap mResourceMap;
|
||||
private int mDepth;
|
||||
private int mCurrentEvent = EVENT_START_DOCUMENT;
|
||||
private String mCurrentElementName;
|
||||
private String mCurrentElementNamespace;
|
||||
private int mCurrentElementAttributeCount;
|
||||
private List<Attribute> mCurrentElementAttributes;
|
||||
private ByteBuffer mCurrentElementAttributesContents;
|
||||
private int mCurrentElementAttrSizeBytes;
|
||||
|
||||
public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException {
|
||||
xml.order(ByteOrder.LITTLE_ENDIAN);
|
||||
Chunk resXmlChunk = null;
|
||||
while (xml.hasRemaining()) {
|
||||
Chunk chunk = Chunk.get(xml);
|
||||
if (chunk == null) {
|
||||
break;
|
||||
}
|
||||
if (chunk.getType() == Chunk.TYPE_RES_XML) {
|
||||
resXmlChunk = chunk;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (resXmlChunk == null) {
|
||||
throw new XmlParserException("No XML chunk in file");
|
||||
}
|
||||
mXml = resXmlChunk.getContents();
|
||||
}
|
||||
|
||||
public int getDepth() {
|
||||
return mDepth;
|
||||
}
|
||||
|
||||
public int getEventType() {
|
||||
return mCurrentEvent;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
|
||||
return null;
|
||||
}
|
||||
return mCurrentElementName;
|
||||
}
|
||||
|
||||
public String getNamespace() {
|
||||
if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) {
|
||||
return null;
|
||||
}
|
||||
return mCurrentElementNamespace;
|
||||
}
|
||||
|
||||
public int getAttributeCount() {
|
||||
if (mCurrentEvent != EVENT_START_ELEMENT) {
|
||||
return -1;
|
||||
}
|
||||
return mCurrentElementAttributeCount;
|
||||
}
|
||||
|
||||
public int getAttributeNameResourceId(int index) throws XmlParserException {
|
||||
return getAttribute(index).getNameResourceId();
|
||||
}
|
||||
|
||||
public String getAttributeName(int index) throws XmlParserException {
|
||||
return getAttribute(index).getName();
|
||||
}
|
||||
|
||||
public String getAttributeNamespace(int index) throws XmlParserException {
|
||||
return getAttribute(index).getNamespace();
|
||||
}
|
||||
|
||||
public int getAttributeValueType(int index) throws XmlParserException {
|
||||
int type = getAttribute(index).getValueType();
|
||||
switch (type) {
|
||||
case Attribute.TYPE_STRING:
|
||||
return VALUE_TYPE_STRING;
|
||||
case Attribute.TYPE_INT_DEC:
|
||||
case Attribute.TYPE_INT_HEX:
|
||||
return VALUE_TYPE_INT;
|
||||
case Attribute.TYPE_REFERENCE:
|
||||
return VALUE_TYPE_REFERENCE;
|
||||
case Attribute.TYPE_INT_BOOLEAN:
|
||||
return VALUE_TYPE_BOOLEAN;
|
||||
default:
|
||||
return VALUE_TYPE_UNSUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
public int getAttributeIntValue(int index) throws XmlParserException {
|
||||
return getAttribute(index).getIntValue();
|
||||
}
|
||||
|
||||
public boolean getAttributeBooleanValue(int index) throws XmlParserException {
|
||||
return getAttribute(index).getBooleanValue();
|
||||
}
|
||||
|
||||
public String getAttributeStringValue(int index) throws XmlParserException {
|
||||
return getAttribute(index).getStringValue();
|
||||
}
|
||||
private Attribute getAttribute(int index) {
|
||||
if (mCurrentEvent != EVENT_START_ELEMENT) {
|
||||
throw new IndexOutOfBoundsException("Current event not a START_ELEMENT");
|
||||
}
|
||||
if (index < 0) {
|
||||
throw new IndexOutOfBoundsException("index must be >= 0");
|
||||
}
|
||||
if (index >= mCurrentElementAttributeCount) {
|
||||
throw new IndexOutOfBoundsException(
|
||||
"index must be <= attr count (" + mCurrentElementAttributeCount + ")");
|
||||
}
|
||||
parseCurrentElementAttributesIfNotParsed();
|
||||
return mCurrentElementAttributes.get(index);
|
||||
}
|
||||
|
||||
public int next() throws XmlParserException {
|
||||
if (mCurrentEvent == EVENT_END_ELEMENT) {
|
||||
mDepth--;
|
||||
}
|
||||
while (mXml.hasRemaining()) {
|
||||
Chunk chunk = Chunk.get(mXml);
|
||||
if (chunk == null) {
|
||||
break;
|
||||
}
|
||||
switch (chunk.getType()) {
|
||||
case Chunk.TYPE_STRING_POOL:
|
||||
if (mStringPool != null) {
|
||||
throw new XmlParserException("Multiple string pools not supported");
|
||||
}
|
||||
mStringPool = new StringPool(chunk);
|
||||
break;
|
||||
case Chunk.RES_XML_TYPE_START_ELEMENT:
|
||||
{
|
||||
if (mStringPool == null) {
|
||||
throw new XmlParserException(
|
||||
"Named element encountered before string pool");
|
||||
}
|
||||
ByteBuffer contents = chunk.getContents();
|
||||
if (contents.remaining() < 20) {
|
||||
throw new XmlParserException(
|
||||
"Start element chunk too short. Need at least 20 bytes. Available: "
|
||||
+ contents.remaining() + " bytes");
|
||||
}
|
||||
long nsId = getUnsignedInt32(contents);
|
||||
long nameId = getUnsignedInt32(contents);
|
||||
int attrStartOffset = getUnsignedInt16(contents);
|
||||
int attrSizeBytes = getUnsignedInt16(contents);
|
||||
int attrCount = getUnsignedInt16(contents);
|
||||
long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes;
|
||||
contents.position(0);
|
||||
if (attrStartOffset > contents.remaining()) {
|
||||
throw new XmlParserException(
|
||||
"Attributes start offset out of bounds: " + attrStartOffset
|
||||
+ ", max: " + contents.remaining());
|
||||
}
|
||||
if (attrEndOffset > contents.remaining()) {
|
||||
throw new XmlParserException(
|
||||
"Attributes end offset out of bounds: " + attrEndOffset
|
||||
+ ", max: " + contents.remaining());
|
||||
}
|
||||
mCurrentElementName = mStringPool.getString(nameId);
|
||||
mCurrentElementNamespace =
|
||||
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
|
||||
mCurrentElementAttributeCount = attrCount;
|
||||
mCurrentElementAttributes = null;
|
||||
mCurrentElementAttrSizeBytes = attrSizeBytes;
|
||||
mCurrentElementAttributesContents =
|
||||
sliceFromTo(contents, attrStartOffset, attrEndOffset);
|
||||
mDepth++;
|
||||
mCurrentEvent = EVENT_START_ELEMENT;
|
||||
return mCurrentEvent;
|
||||
}
|
||||
case Chunk.RES_XML_TYPE_END_ELEMENT:
|
||||
{
|
||||
if (mStringPool == null) {
|
||||
throw new XmlParserException(
|
||||
"Named element encountered before string pool");
|
||||
}
|
||||
ByteBuffer contents = chunk.getContents();
|
||||
if (contents.remaining() < 8) {
|
||||
throw new XmlParserException(
|
||||
"End element chunk too short. Need at least 8 bytes. Available: "
|
||||
+ contents.remaining() + " bytes");
|
||||
}
|
||||
long nsId = getUnsignedInt32(contents);
|
||||
long nameId = getUnsignedInt32(contents);
|
||||
mCurrentElementName = mStringPool.getString(nameId);
|
||||
mCurrentElementNamespace =
|
||||
(nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId);
|
||||
mCurrentEvent = EVENT_END_ELEMENT;
|
||||
mCurrentElementAttributes = null;
|
||||
mCurrentElementAttributesContents = null;
|
||||
return mCurrentEvent;
|
||||
}
|
||||
case Chunk.RES_XML_TYPE_RESOURCE_MAP:
|
||||
if (mResourceMap != null) {
|
||||
throw new XmlParserException("Multiple resource maps not supported");
|
||||
}
|
||||
mResourceMap = new ResourceMap(chunk);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
mCurrentEvent = EVENT_END_DOCUMENT;
|
||||
return mCurrentEvent;
|
||||
}
|
||||
private void parseCurrentElementAttributesIfNotParsed() {
|
||||
if (mCurrentElementAttributes != null) {
|
||||
return;
|
||||
}
|
||||
mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount);
|
||||
for (int i = 0; i < mCurrentElementAttributeCount; i++) {
|
||||
int startPosition = i * mCurrentElementAttrSizeBytes;
|
||||
ByteBuffer attr =
|
||||
sliceFromTo(
|
||||
mCurrentElementAttributesContents,
|
||||
startPosition,
|
||||
startPosition + mCurrentElementAttrSizeBytes);
|
||||
long nsId = getUnsignedInt32(attr);
|
||||
long nameId = getUnsignedInt32(attr);
|
||||
attr.position(attr.position() + 7); // skip ignored fields
|
||||
int valueType = getUnsignedInt8(attr);
|
||||
long valueData = getUnsignedInt32(attr);
|
||||
mCurrentElementAttributes.add(
|
||||
new Attribute(
|
||||
nsId,
|
||||
nameId,
|
||||
valueType,
|
||||
(int) valueData,
|
||||
mStringPool,
|
||||
mResourceMap));
|
||||
}
|
||||
}
|
||||
private static class Attribute {
|
||||
private static final int TYPE_REFERENCE = 1;
|
||||
private static final int TYPE_STRING = 3;
|
||||
private static final int TYPE_INT_DEC = 0x10;
|
||||
private static final int TYPE_INT_HEX = 0x11;
|
||||
private static final int TYPE_INT_BOOLEAN = 0x12;
|
||||
private final long mNsId;
|
||||
private final long mNameId;
|
||||
private final int mValueType;
|
||||
private final int mValueData;
|
||||
private final StringPool mStringPool;
|
||||
private final ResourceMap mResourceMap;
|
||||
private Attribute(
|
||||
long nsId,
|
||||
long nameId,
|
||||
int valueType,
|
||||
int valueData,
|
||||
StringPool stringPool,
|
||||
ResourceMap resourceMap) {
|
||||
mNsId = nsId;
|
||||
mNameId = nameId;
|
||||
mValueType = valueType;
|
||||
mValueData = valueData;
|
||||
mStringPool = stringPool;
|
||||
mResourceMap = resourceMap;
|
||||
}
|
||||
public int getNameResourceId() {
|
||||
return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0;
|
||||
}
|
||||
public String getName() throws XmlParserException {
|
||||
return mStringPool.getString(mNameId);
|
||||
}
|
||||
public String getNamespace() throws XmlParserException {
|
||||
return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : "";
|
||||
}
|
||||
public int getValueType() {
|
||||
return mValueType;
|
||||
}
|
||||
public int getIntValue() throws XmlParserException {
|
||||
switch (mValueType) {
|
||||
case TYPE_REFERENCE:
|
||||
case TYPE_INT_DEC:
|
||||
case TYPE_INT_HEX:
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return mValueData;
|
||||
default:
|
||||
throw new XmlParserException("Cannot coerce to int: value type " + mValueType);
|
||||
}
|
||||
}
|
||||
public boolean getBooleanValue() throws XmlParserException {
|
||||
//noinspection SwitchStatementWithTooFewBranches
|
||||
switch (mValueType) {
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return mValueData != 0;
|
||||
default:
|
||||
throw new XmlParserException(
|
||||
"Cannot coerce to boolean: value type " + mValueType);
|
||||
}
|
||||
}
|
||||
public String getStringValue() throws XmlParserException {
|
||||
switch (mValueType) {
|
||||
case TYPE_STRING:
|
||||
return mStringPool.getString(mValueData & 0xffffffffL);
|
||||
case TYPE_INT_DEC:
|
||||
return Integer.toString(mValueData);
|
||||
case TYPE_INT_HEX:
|
||||
return "0x" + Integer.toHexString(mValueData);
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return Boolean.toString(mValueData != 0);
|
||||
case TYPE_REFERENCE:
|
||||
return "@" + Integer.toHexString(mValueData);
|
||||
default:
|
||||
throw new XmlParserException(
|
||||
"Cannot coerce to string: value type " + mValueType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Chunk {
|
||||
public static final int TYPE_STRING_POOL = 1;
|
||||
public static final int TYPE_RES_XML = 3;
|
||||
public static final int RES_XML_TYPE_START_ELEMENT = 0x0102;
|
||||
public static final int RES_XML_TYPE_END_ELEMENT = 0x0103;
|
||||
public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180;
|
||||
static final int HEADER_MIN_SIZE_BYTES = 8;
|
||||
private final int mType;
|
||||
private final ByteBuffer mHeader;
|
||||
private final ByteBuffer mContents;
|
||||
public Chunk(int type, ByteBuffer header, ByteBuffer contents) {
|
||||
mType = type;
|
||||
mHeader = header;
|
||||
mContents = contents;
|
||||
}
|
||||
public ByteBuffer getContents() {
|
||||
ByteBuffer result = mContents.slice();
|
||||
result.order(mContents.order());
|
||||
return result;
|
||||
}
|
||||
public ByteBuffer getHeader() {
|
||||
ByteBuffer result = mHeader.slice();
|
||||
result.order(mHeader.order());
|
||||
return result;
|
||||
}
|
||||
public int getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
public static Chunk get(ByteBuffer input) throws XmlParserException {
|
||||
if (input.remaining() < HEADER_MIN_SIZE_BYTES) {
|
||||
// Android ignores the last chunk if its header is too big to fit into the file
|
||||
input.position(input.limit());
|
||||
return null;
|
||||
}
|
||||
int originalPosition = input.position();
|
||||
int type = getUnsignedInt16(input);
|
||||
int headerSize = getUnsignedInt16(input);
|
||||
long chunkSize = getUnsignedInt32(input);
|
||||
long chunkRemaining = chunkSize - 8;
|
||||
if (chunkRemaining > input.remaining()) {
|
||||
input.position(input.limit());
|
||||
return null;
|
||||
}
|
||||
if (headerSize < HEADER_MIN_SIZE_BYTES) {
|
||||
throw new XmlParserException(
|
||||
"Malformed chunk: header too short: " + headerSize + " bytes");
|
||||
} else if (headerSize > chunkSize) {
|
||||
throw new XmlParserException(
|
||||
"Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: "
|
||||
+ chunkSize + " bytes");
|
||||
}
|
||||
int contentStartPosition = originalPosition + headerSize;
|
||||
long chunkEndPosition = originalPosition + chunkSize;
|
||||
Chunk chunk =
|
||||
new Chunk(
|
||||
type,
|
||||
sliceFromTo(input, originalPosition, contentStartPosition),
|
||||
sliceFromTo(input, contentStartPosition, chunkEndPosition));
|
||||
input.position((int) chunkEndPosition);
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
|
||||
private static class StringPool {
|
||||
private static final int FLAG_UTF8 = 1 << 8;
|
||||
private final ByteBuffer mChunkContents;
|
||||
private final ByteBuffer mStringsSection;
|
||||
private final int mStringCount;
|
||||
private final boolean mUtf8Encoded;
|
||||
private final Map<Integer, String> mCachedStrings = new HashMap<>();
|
||||
|
||||
public StringPool(Chunk chunk) throws XmlParserException {
|
||||
ByteBuffer header = chunk.getHeader();
|
||||
int headerSizeBytes = header.remaining();
|
||||
header.position(Chunk.HEADER_MIN_SIZE_BYTES);
|
||||
if (header.remaining() < 20) {
|
||||
throw new XmlParserException(
|
||||
"XML chunk's header too short. Required at least 20 bytes. Available: "
|
||||
+ header.remaining() + " bytes");
|
||||
}
|
||||
long stringCount = getUnsignedInt32(header);
|
||||
if (stringCount > Integer.MAX_VALUE) {
|
||||
throw new XmlParserException("Too many strings: " + stringCount);
|
||||
}
|
||||
mStringCount = (int) stringCount;
|
||||
long styleCount = getUnsignedInt32(header);
|
||||
if (styleCount > Integer.MAX_VALUE) {
|
||||
throw new XmlParserException("Too many styles: " + styleCount);
|
||||
}
|
||||
long flags = getUnsignedInt32(header);
|
||||
long stringsStartOffset = getUnsignedInt32(header);
|
||||
long stylesStartOffset = getUnsignedInt32(header);
|
||||
ByteBuffer contents = chunk.getContents();
|
||||
if (mStringCount > 0) {
|
||||
int stringsSectionStartOffsetInContents =
|
||||
(int) (stringsStartOffset - headerSizeBytes);
|
||||
int stringsSectionEndOffsetInContents;
|
||||
if (styleCount > 0) {
|
||||
if (stylesStartOffset < stringsStartOffset) {
|
||||
throw new XmlParserException(
|
||||
"Styles offset (" + stylesStartOffset + ") < strings offset ("
|
||||
+ stringsStartOffset + ")");
|
||||
}
|
||||
stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes);
|
||||
} else {
|
||||
stringsSectionEndOffsetInContents = contents.remaining();
|
||||
}
|
||||
mStringsSection =
|
||||
sliceFromTo(
|
||||
contents,
|
||||
stringsSectionStartOffsetInContents,
|
||||
stringsSectionEndOffsetInContents);
|
||||
} else {
|
||||
mStringsSection = ByteBuffer.allocate(0);
|
||||
}
|
||||
mUtf8Encoded = (flags & FLAG_UTF8) != 0;
|
||||
mChunkContents = contents;
|
||||
}
|
||||
|
||||
public String getString(long index) throws XmlParserException {
|
||||
if (index < 0) {
|
||||
throw new XmlParserException("Unsuported string index: " + index);
|
||||
} else if (index >= mStringCount) {
|
||||
throw new XmlParserException(
|
||||
"Unsuported string index: " + index + ", max: " + (mStringCount - 1));
|
||||
}
|
||||
int idx = (int) index;
|
||||
String result = mCachedStrings.get(idx);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4);
|
||||
if (offsetInStringsSection >= mStringsSection.capacity()) {
|
||||
throw new XmlParserException(
|
||||
"Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection
|
||||
+ ", max: " + (mStringsSection.capacity() - 1));
|
||||
}
|
||||
mStringsSection.position((int) offsetInStringsSection);
|
||||
result =
|
||||
(mUtf8Encoded)
|
||||
? getLengthPrefixedUtf8EncodedString(mStringsSection)
|
||||
: getLengthPrefixedUtf16EncodedString(mStringsSection);
|
||||
mCachedStrings.put(idx, result);
|
||||
return result;
|
||||
}
|
||||
private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded)
|
||||
throws XmlParserException {
|
||||
int lengthChars = getUnsignedInt16(encoded);
|
||||
if ((lengthChars & 0x8000) != 0) {
|
||||
lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded);
|
||||
}
|
||||
if (lengthChars > Integer.MAX_VALUE / 2) {
|
||||
throw new XmlParserException("String too long: " + lengthChars + " uint16s");
|
||||
}
|
||||
int lengthBytes = lengthChars * 2;
|
||||
byte[] arr;
|
||||
int arrOffset;
|
||||
if (encoded.hasArray()) {
|
||||
arr = encoded.array();
|
||||
arrOffset = encoded.arrayOffset() + encoded.position();
|
||||
encoded.position(encoded.position() + lengthBytes);
|
||||
} else {
|
||||
arr = new byte[lengthBytes];
|
||||
arrOffset = 0;
|
||||
encoded.get(arr);
|
||||
}
|
||||
if ((arr[arrOffset + lengthBytes] != 0)
|
||||
|| (arr[arrOffset + lengthBytes + 1] != 0)) {
|
||||
throw new XmlParserException("UTF-16 encoded form of string not NULL terminated");
|
||||
}
|
||||
return new String(arr, arrOffset, lengthBytes, StandardCharsets.UTF_16LE);
|
||||
}
|
||||
private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded)
|
||||
throws XmlParserException {
|
||||
int lengthBytes = getUnsignedInt8(encoded);
|
||||
if ((lengthBytes & 0x80) != 0) {
|
||||
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
|
||||
}
|
||||
lengthBytes = getUnsignedInt8(encoded);
|
||||
if ((lengthBytes & 0x80) != 0) {
|
||||
lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded);
|
||||
}
|
||||
byte[] arr;
|
||||
int arrOffset;
|
||||
if (encoded.hasArray()) {
|
||||
arr = encoded.array();
|
||||
arrOffset = encoded.arrayOffset() + encoded.position();
|
||||
encoded.position(encoded.position() + lengthBytes);
|
||||
} else {
|
||||
arr = new byte[lengthBytes];
|
||||
arrOffset = 0;
|
||||
encoded.get(arr);
|
||||
}
|
||||
if (arr[arrOffset + lengthBytes] != 0) {
|
||||
throw new XmlParserException("UTF-8 encoded form of string not NULL terminated");
|
||||
}
|
||||
return new String(arr, arrOffset, lengthBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ResourceMap {
|
||||
private final ByteBuffer mChunkContents;
|
||||
private final int mEntryCount;
|
||||
|
||||
public ResourceMap(Chunk chunk) throws XmlParserException {
|
||||
mChunkContents = chunk.getContents().slice();
|
||||
mChunkContents.order(chunk.getContents().order());
|
||||
// Each entry of the map is four bytes long, containing the int32 resource ID.
|
||||
mEntryCount = mChunkContents.remaining() / 4;
|
||||
}
|
||||
|
||||
public int getResourceId(long index) {
|
||||
if ((index < 0) || (index >= mEntryCount)) {
|
||||
return 0;
|
||||
}
|
||||
int idx = (int) index;
|
||||
// Each entry of the map is four bytes long, containing the int32 resource ID.
|
||||
return mChunkContents.getInt(idx * 4);
|
||||
}
|
||||
}
|
||||
|
||||
private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) {
|
||||
if (start < 0) {
|
||||
throw new IllegalArgumentException("start: " + start);
|
||||
}
|
||||
if (end < start) {
|
||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||
}
|
||||
int capacity = source.capacity();
|
||||
if (end > source.capacity()) {
|
||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||
}
|
||||
return sliceFromTo(source, (int) start, (int) end);
|
||||
}
|
||||
|
||||
private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
|
||||
if (start < 0) {
|
||||
throw new IllegalArgumentException("start: " + start);
|
||||
}
|
||||
if (end < start) {
|
||||
throw new IllegalArgumentException("end < start: " + end + " < " + start);
|
||||
}
|
||||
int capacity = source.capacity();
|
||||
if (end > source.capacity()) {
|
||||
throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
|
||||
}
|
||||
int originalLimit = source.limit();
|
||||
int originalPosition = source.position();
|
||||
try {
|
||||
source.position(0);
|
||||
source.limit(end);
|
||||
source.position(start);
|
||||
ByteBuffer result = source.slice();
|
||||
result.order(source.order());
|
||||
return result;
|
||||
} finally {
|
||||
source.position(0);
|
||||
source.limit(originalLimit);
|
||||
source.position(originalPosition);
|
||||
}
|
||||
}
|
||||
private static int getUnsignedInt8(ByteBuffer buffer) {
|
||||
return buffer.get() & 0xff;
|
||||
}
|
||||
private static int getUnsignedInt16(ByteBuffer buffer) {
|
||||
return buffer.getShort() & 0xffff;
|
||||
}
|
||||
private static long getUnsignedInt32(ByteBuffer buffer) {
|
||||
return buffer.getInt() & 0xffffffffL;
|
||||
}
|
||||
private static long getUnsignedInt32(ByteBuffer buffer, int position) {
|
||||
return buffer.getInt(position) & 0xffffffffL;
|
||||
}
|
||||
|
||||
public static class XmlParserException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public XmlParserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
public XmlParserException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
@@ -17,13 +19,21 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bintianqi.owndroid.dpm.installPackage
|
||||
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
|
||||
import com.github.fishb1.apkinfo.ApkInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileInputStream
|
||||
|
||||
class InstallAppActivity: FragmentActivity() {
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
@@ -34,37 +44,64 @@ class InstallAppActivity: FragmentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
val context = applicationContext
|
||||
val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE)
|
||||
window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
val uri = this.intent.data!!
|
||||
var apkInfoText by mutableStateOf(context.getString(R.string.parsing_apk_info))
|
||||
var status by mutableStateOf("parsing")
|
||||
this.lifecycleScope.launch(Dispatchers.IO) {
|
||||
val fd = applicationContext.contentResolver.openFileDescriptor(uri, "r")
|
||||
val apkInfo = ApkInfo.fromInputStream(
|
||||
FileInputStream(fd?.fileDescriptor)
|
||||
)
|
||||
fd?.close()
|
||||
withContext(Dispatchers.Main) {
|
||||
status = "waiting"
|
||||
apkInfoText = "${context.getString(R.string.package_name)}: ${apkInfo.packageName}\n"
|
||||
apkInfoText += "${context.getString(R.string.version_name)}: ${apkInfo.versionName}\n"
|
||||
apkInfoText += "${context.getString(R.string.version_code)}: ${apkInfo.versionCode}"
|
||||
}
|
||||
}
|
||||
setContent {
|
||||
var installing by remember { mutableStateOf(false) }
|
||||
OwnDroidTheme(
|
||||
sharedPref.getBoolean("material_you", true),
|
||||
sharedPref.getBoolean("black_theme", false)
|
||||
) {
|
||||
AlertDialog(
|
||||
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
|
||||
title = {
|
||||
Text(stringResource(R.string.install_app))
|
||||
},
|
||||
onDismissRequest = {
|
||||
finish()
|
||||
if(status != "installing") finish()
|
||||
},
|
||||
text = {
|
||||
AnimatedVisibility(installing) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
Column {
|
||||
AnimatedVisibility(status != "waiting") {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
Text(text = apkInfoText, modifier = Modifier.padding(top = 4.dp))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { finish() }) { Text(stringResource(R.string.cancel)) }
|
||||
TextButton(
|
||||
onClick = { finish() },
|
||||
enabled = status != "installing"
|
||||
) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
installing = true
|
||||
status = "installing"
|
||||
uriToStream(applicationContext, this.intent.data) { stream -> installPackage(applicationContext, stream) }
|
||||
}
|
||||
},
|
||||
enabled = status != "installing"
|
||||
) {
|
||||
Text(stringResource(R.string.confirm))
|
||||
Text(stringResource(R.string.install))
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.bintianqi.owndroid
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
@@ -41,7 +40,6 @@ import androidx.navigation.compose.rememberNavController
|
||||
import com.bintianqi.owndroid.dpm.*
|
||||
import com.bintianqi.owndroid.ui.Animations
|
||||
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.util.Locale
|
||||
|
||||
@@ -50,7 +48,6 @@ var backToHomeStateFlow = MutableStateFlow(false)
|
||||
class MainActivity : FragmentActivity() {
|
||||
private val showAuth = mutableStateOf(false)
|
||||
|
||||
@SuppressLint("UnrememberedMutableState")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
registerActivityResult(this)
|
||||
enableEdgeToEdge()
|
||||
@@ -63,8 +60,8 @@ class MainActivity : FragmentActivity() {
|
||||
val locale = applicationContext.resources?.configuration?.locale
|
||||
zhCN = locale == Locale.SIMPLIFIED_CHINESE || locale == Locale.CHINESE || locale == Locale.CHINA
|
||||
setContent {
|
||||
val materialYou = mutableStateOf(sharedPref.getBoolean("material_you", true))
|
||||
val blackTheme = mutableStateOf(sharedPref.getBoolean("black_theme", false))
|
||||
val materialYou = remember { mutableStateOf(sharedPref.getBoolean("material_you", true)) }
|
||||
val blackTheme = remember { mutableStateOf(sharedPref.getBoolean("black_theme", false)) }
|
||||
OwnDroidTheme(materialYou.value, blackTheme.value) {
|
||||
Home(materialYou, blackTheme)
|
||||
if(showAuth.value) {
|
||||
@@ -87,7 +84,6 @@ class MainActivity : FragmentActivity() {
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("UnrememberedMutableState")
|
||||
@ExperimentalMaterial3Api
|
||||
@Composable
|
||||
fun Home(materialYou:MutableState<Boolean>, blackTheme:MutableState<Boolean>) {
|
||||
@@ -97,8 +93,8 @@ fun Home(materialYou:MutableState<Boolean>, blackTheme:MutableState<Boolean>) {
|
||||
val receiver = ComponentName(context,Receiver::class.java)
|
||||
val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE)
|
||||
val focusMgr = LocalFocusManager.current
|
||||
val pkgName = mutableStateOf("")
|
||||
val dialogStatus = mutableIntStateOf(0)
|
||||
val pkgName = remember { mutableStateOf("") }
|
||||
val dialogStatus = remember { mutableIntStateOf(0) }
|
||||
val backToHome by backToHomeStateFlow.collectAsState()
|
||||
LaunchedEffect(backToHome) {
|
||||
if(backToHome) { navCtrl.navigateUp(); backToHomeStateFlow.value = false }
|
||||
|
||||
52
app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt
Normal file
52
app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2022 fishbone
|
||||
*
|
||||
* This code is licensed under MIT license (see LICENSE file for details)
|
||||
*/
|
||||
|
||||
package com.github.fishb1.apkinfo
|
||||
|
||||
import com.android.apksig.internal.apk.AndroidBinXmlParser
|
||||
import com.android.apksig.internal.apk.AndroidBinXmlParser.XmlParserException
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
data class ApkInfo(
|
||||
val compileSdkVersion: Int = 0,
|
||||
val compileSdkVersionCodename: String = "",
|
||||
val installLocation: String = "",
|
||||
val packageName: String = "",
|
||||
val platformBuildVersionCode: Int = 0,
|
||||
val platformBuildVersionName: String = "",
|
||||
val versionCode: Int = 0,
|
||||
val versionName: String = "",
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
private val EMPTY = ApkInfo()
|
||||
|
||||
private const val MANIFEST_FILE_NAME = "AndroidManifest.xml"
|
||||
|
||||
fun fromInputStream(stream: InputStream): ApkInfo {
|
||||
ZipInputStream(stream).use { zip ->
|
||||
var entry: ZipEntry?
|
||||
do {
|
||||
entry = zip.nextEntry
|
||||
if (entry?.name == MANIFEST_FILE_NAME) {
|
||||
val data = ByteBuffer.wrap(zip.readBytes())
|
||||
return try {
|
||||
val parser = AndroidBinXmlParser(data)
|
||||
ManifestUtils.readApkInfo(parser)
|
||||
} catch (e: XmlParserException) {
|
||||
EMPTY
|
||||
}
|
||||
}
|
||||
} while (entry != null)
|
||||
}
|
||||
return EMPTY
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2022 fishbone
|
||||
*
|
||||
* This code is licensed under MIT license (see LICENSE file for details)
|
||||
*/
|
||||
|
||||
package com.github.fishb1.apkinfo
|
||||
|
||||
internal class ApkInfoBuilder {
|
||||
|
||||
private var compileSdkVersion: Int = 0
|
||||
private var compileSdkVersionCodename: String = ""
|
||||
private var installLocation: String = ""
|
||||
private var packageName: String = ""
|
||||
private var platformBuildVersionCode: Int = 0
|
||||
private var platformBuildVersionName: String = ""
|
||||
private var versionCode: Int = 0
|
||||
private var versionName: String = ""
|
||||
|
||||
fun compileSdkVersion(value: Int) = apply {
|
||||
compileSdkVersion = value
|
||||
}
|
||||
|
||||
fun compileSdkVersionCodename(value: String) = apply {
|
||||
compileSdkVersionCodename = value
|
||||
}
|
||||
|
||||
fun installLocation(value: String) = apply {
|
||||
installLocation = value
|
||||
}
|
||||
|
||||
fun packageName(value: String) = apply {
|
||||
packageName = value
|
||||
}
|
||||
|
||||
fun platformBuildVersionCode(value: Int) = apply {
|
||||
platformBuildVersionCode = value
|
||||
}
|
||||
|
||||
fun platformBuildVersionName(value: String) = apply {
|
||||
platformBuildVersionName = value
|
||||
}
|
||||
|
||||
fun versionCode(value: Int) = apply {
|
||||
versionCode = value
|
||||
}
|
||||
|
||||
fun versionName(value: String) = apply {
|
||||
versionName = value
|
||||
}
|
||||
|
||||
fun build(): ApkInfo {
|
||||
return ApkInfo(
|
||||
compileSdkVersion = compileSdkVersion,
|
||||
compileSdkVersionCodename =compileSdkVersionCodename,
|
||||
installLocation = installLocation,
|
||||
packageName = packageName,
|
||||
platformBuildVersionCode = platformBuildVersionCode,
|
||||
platformBuildVersionName = platformBuildVersionName,
|
||||
versionCode = versionCode,
|
||||
versionName = versionName,
|
||||
)
|
||||
}
|
||||
}
|
||||
55
app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt
Normal file
55
app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2022 fishbone
|
||||
*
|
||||
* This code is licensed under MIT license (see LICENSE file for details)
|
||||
*/
|
||||
|
||||
package com.github.fishb1.apkinfo
|
||||
|
||||
import com.android.apksig.internal.apk.AndroidBinXmlParser
|
||||
|
||||
internal object ManifestUtils {
|
||||
|
||||
private const val TAG_MANIFEST = "manifest"
|
||||
|
||||
private const val ATTR_COMPILE_SDK_VERSION = "compileSdkVersion"
|
||||
private const val ATTR_COMPILE_SDK_VERSION_CODENAME = "compileSdkVersionCodename"
|
||||
private const val ATTR_INSTALL_LOCATION = "installLocation"
|
||||
private const val ATTR_PACKAGE = "package"
|
||||
private const val ATTR_PLATFORM_BUILD_VERSION_CODE = "platformBuildVersionCode"
|
||||
private const val ATTR_PLATFORM_BUILD_VERSION_NAME = "platformBuildVersionName"
|
||||
private const val ATTR_VERSION_CODE = "versionCode"
|
||||
private const val ATTR_VERSION_NAME = "versionName"
|
||||
|
||||
fun readApkInfo(parser: AndroidBinXmlParser): ApkInfo {
|
||||
val builder = ApkInfoBuilder()
|
||||
var eventType = parser.eventType
|
||||
while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) {
|
||||
if (eventType == AndroidBinXmlParser.EVENT_START_ELEMENT && parser.name == TAG_MANIFEST) {
|
||||
|
||||
for (i in 0 until parser.attributeCount) {
|
||||
when (parser.getAttributeName(i)) {
|
||||
ATTR_COMPILE_SDK_VERSION ->
|
||||
builder.compileSdkVersion(parser.getAttributeIntValue(i))
|
||||
ATTR_COMPILE_SDK_VERSION_CODENAME ->
|
||||
builder.compileSdkVersionCodename(parser.getAttributeStringValue(i))
|
||||
ATTR_INSTALL_LOCATION ->
|
||||
builder.installLocation(parser.getAttributeStringValue(i))
|
||||
ATTR_PACKAGE ->
|
||||
builder.packageName(parser.getAttributeStringValue(i))
|
||||
ATTR_PLATFORM_BUILD_VERSION_CODE ->
|
||||
builder.platformBuildVersionCode(parser.getAttributeIntValue(i))
|
||||
ATTR_PLATFORM_BUILD_VERSION_NAME ->
|
||||
builder.platformBuildVersionName(parser.getAttributeStringValue(i))
|
||||
ATTR_VERSION_CODE ->
|
||||
builder.versionCode(parser.getAttributeIntValue(i))
|
||||
ATTR_VERSION_NAME ->
|
||||
builder.versionName(parser.getAttributeStringValue(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
eventType = parser.next()
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
}
|
||||
@@ -572,4 +572,7 @@
|
||||
<string name="permission_ACTIVITY_RECOGNITION">Aktivite tanıma</string>
|
||||
<string name="permission_POST_NOTIFICATIONS">Bildirim gönder</string>
|
||||
|
||||
<string name="version_name">Version name</string> <!--TODO-->
|
||||
<string name="version_code">Version code</string> <!--TODO-->
|
||||
<string name="parsing_apk_info" tools:ignore="TypographyEllipsis">Parsing APK info...</string>
|
||||
</resources>
|
||||
|
||||
@@ -563,4 +563,7 @@
|
||||
<string name="permission_ACTIVITY_RECOGNITION">查看使用情况</string>
|
||||
<string name="permission_POST_NOTIFICATIONS">发送通知</string>
|
||||
|
||||
<string name="version_name">版本名</string>
|
||||
<string name="version_code">版本号</string>
|
||||
<string name="parsing_apk_info" tools:ignore="TypographyEllipsis">解析APK信息中...</string>
|
||||
</resources>
|
||||
|
||||
@@ -579,4 +579,7 @@
|
||||
<string name="permission_ACTIVITY_RECOGNITION">Activity recognition</string>
|
||||
<string name="permission_POST_NOTIFICATIONS">Post notifications</string>
|
||||
|
||||
<string name="version_name">Version name</string>
|
||||
<string name="version_code">Version code</string>
|
||||
<string name="parsing_apk_info" tools:ignore="TypographyEllipsis">Parsing APK info...</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user