From 89cfafb03a2cd8cebd365bdd47757ad162e03638 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 6 Jul 2024 11:03:54 +0800 Subject: [PATCH] app installer: get basic apk info --- .../internal/apk/AndroidBinXmlParser.java | 635 ++++++++++++++++++ .../bintianqi/owndroid/InstallAppActivity.kt | 55 +- .../com/bintianqi/owndroid/MainActivity.kt | 12 +- .../java/com/github/fishb1/apkinfo/ApkInfo.kt | 52 ++ .../github/fishb1/apkinfo/ApkInfoBuilder.kt | 64 ++ .../github/fishb1/apkinfo/ManifestUtils.kt | 55 ++ app/src/main/res/values-tr/strings.xml | 5 +- app/src/main/res/values-zh-rCN/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 9 files changed, 866 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java create mode 100644 app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt create mode 100644 app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt create mode 100644 app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt diff --git a/app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java b/app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java new file mode 100644 index 0000000..573fd93 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java @@ -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 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 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); + } + } +} diff --git a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt index 84f8802..178b19e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt @@ -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() diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index e568df1..a180ab2 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -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, blackTheme:MutableState) { @@ -97,8 +93,8 @@ fun Home(materialYou:MutableState, blackTheme:MutableState) { 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 } diff --git a/app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt new file mode 100644 index 0000000..716e55f --- /dev/null +++ b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt @@ -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 + } + } +} diff --git a/app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt new file mode 100644 index 0000000..b932e11 --- /dev/null +++ b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt @@ -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, + ) + } +} diff --git a/app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt b/app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt new file mode 100644 index 0000000..f178e45 --- /dev/null +++ b/app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt @@ -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() + } +} diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 42d07e5..d6a939e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -571,5 +571,8 @@ Arka planda vücut sensörlerine eriş Aktivite tanıma Bildirim gönder - + + Version name + Version code + Parsing APK info... diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 05a71bb..a762204 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -563,4 +563,7 @@ 查看使用情况 发送通知 + 版本名 + 版本号 + 解析APK信息中... diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da159d2..842c01f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -579,4 +579,7 @@ Activity recognition Post notifications + Version name + Version code + Parsing APK info...