app installer: get basic apk info

This commit is contained in:
BinTianqi
2024-07-06 11:03:54 +08:00
parent 4d8c3a7a60
commit 89cfafb03a
9 changed files with 866 additions and 18 deletions

View File

@@ -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);
}
}
}

View File

@@ -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()

View File

@@ -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 }

View 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
}
}
}

View File

@@ -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,
)
}
}

View 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()
}
}

View File

@@ -571,5 +571,8 @@
<string name="permission_BODY_SENSORS_BACKGROUND">Arka planda vücut sensörlerine eriş</string>
<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>

View File

@@ -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>

View File

@@ -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>