From 44d2ab7e2e12d063be93fbe4facf73464f49d8dc Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Wed, 12 Feb 2025 17:27:07 +0800 Subject: [PATCH] Refactor code related to navigation, close #104 New PackageChooserActivity Delete code of parsing apk metadata --- Readme.md | 2 +- app/src/main/AndroidManifest.xml | 8 +- .../internal/apk/AndroidBinXmlParser.java | 635 ------------------ .../com/bintianqi/owndroid/ApiReceiver.kt | 2 +- .../owndroid/AppInstallerActivity.kt | 3 +- .../main/java/com/bintianqi/owndroid/Auth.kt | 7 +- .../com/bintianqi/owndroid/MainActivity.kt | 362 ++++++---- .../bintianqi/owndroid/ManageSpaceActivity.kt | 4 +- .../com/bintianqi/owndroid/MyViewModel.kt | 2 - .../com/bintianqi/owndroid/PackageChooser.kt | 224 ++++++ .../com/bintianqi/owndroid/PackageSelector.kt | 230 ------- .../java/com/bintianqi/owndroid/Settings.kt | 60 +- .../main/java/com/bintianqi/owndroid/Utils.kt | 26 + .../bintianqi/owndroid/dpm/Applications.kt | 105 +-- .../com/bintianqi/owndroid/dpm/Network.kt | 261 +++---- .../com/bintianqi/owndroid/dpm/Password.kt | 55 +- .../com/bintianqi/owndroid/dpm/Permissions.kt | 91 +-- .../com/bintianqi/owndroid/dpm/Shizuku.kt | 30 +- .../java/com/bintianqi/owndroid/dpm/System.kt | 181 ++--- .../bintianqi/owndroid/dpm/UserRestriction.kt | 53 +- .../java/com/bintianqi/owndroid/dpm/Users.kt | 72 +- .../dpm/{ManagedProfile.kt => WorkProfile.kt} | 48 +- .../com/bintianqi/owndroid/ui/Components.kt | 12 +- .../com/bintianqi/owndroid/ui/theme/Theme.kt | 7 +- .../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-ru/strings.xml | 5 +- 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 | 5 +- 31 files changed, 1048 insertions(+), 1621 deletions(-) delete mode 100644 app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java create mode 100644 app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt delete mode 100644 app/src/main/java/com/bintianqi/owndroid/PackageSelector.kt rename app/src/main/java/com/bintianqi/owndroid/dpm/{ManagedProfile.kt => WorkProfile.kt} (91%) delete mode 100644 app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt delete mode 100644 app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt delete mode 100644 app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt diff --git a/Readme.md b/Readme.md index fd664c6..3f5eb0f 100644 --- a/Readme.md +++ b/Readme.md @@ -85,7 +85,7 @@ java.lang.SecurityException: Neither user 2000 nor current process has android.p ``` 解决办法: -- 在开发者设置中打开`USB debugging (Security setting)`。 +- 在开发者设置中打开`USB调试(安全设置)`。 - 在root命令行中执行激活命令 #### ColorOS diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ef75dfe..0aa7b3e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,7 +25,8 @@ android:name=".MainActivity" android:exported="true" android:windowSoftInputMode="adjustResize|stateHidden" - android:theme="@style/Theme.OwnDroid"> + android:theme="@style/Theme.OwnDroid" + android:launchMode="singleInstance"> @@ -55,6 +56,11 @@ + 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/ApiReceiver.kt b/app/src/main/java/com/bintianqi/owndroid/ApiReceiver.kt index 25e7772..a45f3af 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ApiReceiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ApiReceiver.kt @@ -32,7 +32,7 @@ class ApiReceiver: BroadcastReceiver() { false } } - log += "success: $ok" + log += "\nsuccess: $ok" } catch(e: Exception) { e.printStackTrace() val message = (e::class.qualifiedName ?: "Exception") + ": " + (e.message ?: "") diff --git a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt index 619e4d7..c2e60aa 100644 --- a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt @@ -73,7 +73,8 @@ class AppInstallerActivity:FragmentActivity() { val vm by viewModels() vm.initialize(intent) setContent { - OwnDroidTheme(myVm) { + val theme by myVm.theme.collectAsStateWithLifecycle() + OwnDroidTheme(theme) { val installing by vm.installing.collectAsStateWithLifecycle() val sessionMode by vm.sessionMode.collectAsStateWithLifecycle() val packages by vm.packages.collectAsStateWithLifecycle() diff --git a/app/src/main/java/com/bintianqi/owndroid/Auth.kt b/app/src/main/java/com/bintianqi/owndroid/Auth.kt index cae524c..6771a3c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Auth.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Auth.kt @@ -22,15 +22,16 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity -import androidx.navigation.NavHostController import kotlinx.coroutines.delay +import kotlinx.serialization.Serializable + +@Serializable object Authenticate @Composable -fun Authenticate(activity: FragmentActivity, navCtrl: NavHostController) { +fun AuthenticateScreen(activity: FragmentActivity, onAuthSucceed: () -> Unit) { val context = LocalContext.current BackHandler { activity.moveTaskToBack(true) } var status by rememberSaveable { mutableIntStateOf(0) } // 0:Prompt automatically, 1:Authenticating, 2:Prompt manually - val onAuthSucceed = { navCtrl.navigateUp() } val callback = object: AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 65ca8e2..5c9810c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -56,80 +56,147 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope -import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.bintianqi.owndroid.dpm.AccountsViewer -import com.bintianqi.owndroid.dpm.AffiliationID -import com.bintianqi.owndroid.dpm.AlwaysOnVPNPackage -import com.bintianqi.owndroid.dpm.ApplicationManage -import com.bintianqi.owndroid.dpm.CACert +import androidx.navigation.toRoute +import com.bintianqi.owndroid.dpm.Accounts +import com.bintianqi.owndroid.dpm.AccountsScreen +import com.bintianqi.owndroid.dpm.AddNetwork +import com.bintianqi.owndroid.dpm.AddNetworkScreen +import com.bintianqi.owndroid.dpm.AffiliationId +import com.bintianqi.owndroid.dpm.AffiliationIdScreen +import com.bintianqi.owndroid.dpm.AlwaysOnVpnPackage +import com.bintianqi.owndroid.dpm.AlwaysOnVpnPackageScreen +import com.bintianqi.owndroid.dpm.Applications +import com.bintianqi.owndroid.dpm.ApplicationsScreen +import com.bintianqi.owndroid.dpm.CaCert +import com.bintianqi.owndroid.dpm.CaCertScreen import com.bintianqi.owndroid.dpm.ChangeTime +import com.bintianqi.owndroid.dpm.ChangeTimeScreen import com.bintianqi.owndroid.dpm.ChangeTimeZone +import com.bintianqi.owndroid.dpm.ChangeTimeZoneScreen import com.bintianqi.owndroid.dpm.ChangeUserIcon +import com.bintianqi.owndroid.dpm.ChangeUserIconScreen import com.bintianqi.owndroid.dpm.ChangeUsername +import com.bintianqi.owndroid.dpm.ChangeUsernameScreen import com.bintianqi.owndroid.dpm.ContentProtectionPolicy +import com.bintianqi.owndroid.dpm.ContentProtectionPolicyScreen import com.bintianqi.owndroid.dpm.CreateUser +import com.bintianqi.owndroid.dpm.CreateUserScreen import com.bintianqi.owndroid.dpm.CreateWorkProfile -import com.bintianqi.owndroid.dpm.CurrentUserInfo +import com.bintianqi.owndroid.dpm.CreateWorkProfileScreen +import com.bintianqi.owndroid.dpm.CrossProfileIntentFilter +import com.bintianqi.owndroid.dpm.CrossProfileIntentFilterScreen import com.bintianqi.owndroid.dpm.DelegatedAdmins +import com.bintianqi.owndroid.dpm.DelegatedAdminsScreen import com.bintianqi.owndroid.dpm.DeleteWorkProfile +import com.bintianqi.owndroid.dpm.DeleteWorkProfileScreen import com.bintianqi.owndroid.dpm.DeviceAdmin +import com.bintianqi.owndroid.dpm.DeviceAdminScreen import com.bintianqi.owndroid.dpm.DeviceInfo +import com.bintianqi.owndroid.dpm.DeviceInfoScreen import com.bintianqi.owndroid.dpm.DeviceOwner +import com.bintianqi.owndroid.dpm.DeviceOwnerScreen import com.bintianqi.owndroid.dpm.DisableAccountManagement -import com.bintianqi.owndroid.dpm.DisableKeyguardFeatures -import com.bintianqi.owndroid.dpm.FRPPolicy +import com.bintianqi.owndroid.dpm.DisableAccountManagementScreen +import com.bintianqi.owndroid.dpm.FrpPolicy +import com.bintianqi.owndroid.dpm.FrpPolicyScreen import com.bintianqi.owndroid.dpm.HardwareMonitor +import com.bintianqi.owndroid.dpm.HardwareMonitorScreen import com.bintianqi.owndroid.dpm.InstallSystemUpdate -import com.bintianqi.owndroid.dpm.IntentFilter +import com.bintianqi.owndroid.dpm.InstallSystemUpdateScreen import com.bintianqi.owndroid.dpm.Keyguard +import com.bintianqi.owndroid.dpm.KeyguardDisabledFeatures +import com.bintianqi.owndroid.dpm.KeyguardDisabledFeaturesScreen +import com.bintianqi.owndroid.dpm.KeyguardScreen import com.bintianqi.owndroid.dpm.LockScreenInfo +import com.bintianqi.owndroid.dpm.LockScreenInfoScreen import com.bintianqi.owndroid.dpm.LockTaskMode -import com.bintianqi.owndroid.dpm.MTEPolicy +import com.bintianqi.owndroid.dpm.LockTaskModeScreen +import com.bintianqi.owndroid.dpm.MtePolicy +import com.bintianqi.owndroid.dpm.MtePolicyScreen import com.bintianqi.owndroid.dpm.NearbyStreamingPolicy +import com.bintianqi.owndroid.dpm.NearbyStreamingPolicyScreen import com.bintianqi.owndroid.dpm.Network import com.bintianqi.owndroid.dpm.NetworkLogging +import com.bintianqi.owndroid.dpm.NetworkLoggingScreen import com.bintianqi.owndroid.dpm.NetworkOptions -import com.bintianqi.owndroid.dpm.NetworkStats +import com.bintianqi.owndroid.dpm.NetworkOptionsScreen +import com.bintianqi.owndroid.dpm.NetworkScreen +import com.bintianqi.owndroid.dpm.NetworkStatsScreen import com.bintianqi.owndroid.dpm.NetworkStatsViewer -import com.bintianqi.owndroid.dpm.OrgOwnedProfile -import com.bintianqi.owndroid.dpm.OverrideAPN +import com.bintianqi.owndroid.dpm.NetworkStatsViewerScreen +import com.bintianqi.owndroid.dpm.OrganizationOwnedProfile +import com.bintianqi.owndroid.dpm.OrganizationOwnedProfileScreen +import com.bintianqi.owndroid.dpm.OverrideApn +import com.bintianqi.owndroid.dpm.OverrideApnScreen import com.bintianqi.owndroid.dpm.Password -import com.bintianqi.owndroid.dpm.PasswordComplexity import com.bintianqi.owndroid.dpm.PasswordInfo -import com.bintianqi.owndroid.dpm.PasswordQuality +import com.bintianqi.owndroid.dpm.PasswordInfoScreen +import com.bintianqi.owndroid.dpm.PasswordScreen import com.bintianqi.owndroid.dpm.PermissionPolicy +import com.bintianqi.owndroid.dpm.PermissionPolicyScreen import com.bintianqi.owndroid.dpm.Permissions +import com.bintianqi.owndroid.dpm.PermissionsScreen import com.bintianqi.owndroid.dpm.PreferentialNetworkService -import com.bintianqi.owndroid.dpm.PrivateDNS +import com.bintianqi.owndroid.dpm.PreferentialNetworkServiceScreen +import com.bintianqi.owndroid.dpm.PrivateDns +import com.bintianqi.owndroid.dpm.PrivateDnsScreen import com.bintianqi.owndroid.dpm.ProfileOwner +import com.bintianqi.owndroid.dpm.ProfileOwnerScreen +import com.bintianqi.owndroid.dpm.QueryNetworkStats import com.bintianqi.owndroid.dpm.RecommendedGlobalProxy +import com.bintianqi.owndroid.dpm.RecommendedGlobalProxyScreen +import com.bintianqi.owndroid.dpm.RequiredPasswordComplexity +import com.bintianqi.owndroid.dpm.RequiredPasswordComplexityScreen +import com.bintianqi.owndroid.dpm.RequiredPasswordQuality +import com.bintianqi.owndroid.dpm.RequiredPasswordQualityScreen import com.bintianqi.owndroid.dpm.ResetPassword +import com.bintianqi.owndroid.dpm.ResetPasswordScreen import com.bintianqi.owndroid.dpm.ResetPasswordToken -import com.bintianqi.owndroid.dpm.RestrictionData +import com.bintianqi.owndroid.dpm.ResetPasswordTokenScreen +import com.bintianqi.owndroid.dpm.Restriction import com.bintianqi.owndroid.dpm.SecurityLogging -import com.bintianqi.owndroid.dpm.Shizuku -import com.bintianqi.owndroid.dpm.SupportMessages +import com.bintianqi.owndroid.dpm.SecurityLoggingScreen +import com.bintianqi.owndroid.dpm.SetSystemUpdatePolicy +import com.bintianqi.owndroid.dpm.ShizukuScreen +import com.bintianqi.owndroid.dpm.SupportMessage +import com.bintianqi.owndroid.dpm.SupportMessageScreen import com.bintianqi.owndroid.dpm.SuspendPersonalApp -import com.bintianqi.owndroid.dpm.SystemManage +import com.bintianqi.owndroid.dpm.SuspendPersonalAppScreen +import com.bintianqi.owndroid.dpm.SystemManager +import com.bintianqi.owndroid.dpm.SystemManagerScreen import com.bintianqi.owndroid.dpm.SystemOptions +import com.bintianqi.owndroid.dpm.SystemOptionsScreen import com.bintianqi.owndroid.dpm.SystemUpdatePolicy import com.bintianqi.owndroid.dpm.TransferOwnership -import com.bintianqi.owndroid.dpm.UpdateNetwork +import com.bintianqi.owndroid.dpm.TransferOwnershipScreen +import com.bintianqi.owndroid.dpm.UserInfo +import com.bintianqi.owndroid.dpm.UserInfoScreen import com.bintianqi.owndroid.dpm.UserOperation -import com.bintianqi.owndroid.dpm.UserOptions +import com.bintianqi.owndroid.dpm.UserOperationScreen import com.bintianqi.owndroid.dpm.UserRestriction +import com.bintianqi.owndroid.dpm.UserRestrictionOptions +import com.bintianqi.owndroid.dpm.UserRestrictionOptionsScreen import com.bintianqi.owndroid.dpm.UserRestrictionScreen import com.bintianqi.owndroid.dpm.UserSessionMessage +import com.bintianqi.owndroid.dpm.UserSessionMessageScreen import com.bintianqi.owndroid.dpm.Users -import com.bintianqi.owndroid.dpm.Wifi +import com.bintianqi.owndroid.dpm.UsersOptions +import com.bintianqi.owndroid.dpm.UsersOptionsScreen +import com.bintianqi.owndroid.dpm.UsersScreen +import com.bintianqi.owndroid.dpm.WiFi import com.bintianqi.owndroid.dpm.WifiAuthKeypair +import com.bintianqi.owndroid.dpm.WifiAuthKeypairScreen +import com.bintianqi.owndroid.dpm.WifiScreen import com.bintianqi.owndroid.dpm.WifiSecurityLevel -import com.bintianqi.owndroid.dpm.WifiSsidPolicy +import com.bintianqi.owndroid.dpm.WifiSecurityLevelScreen +import com.bintianqi.owndroid.dpm.WifiSsidPolicyScreen import com.bintianqi.owndroid.dpm.WipeData +import com.bintianqi.owndroid.dpm.WipeDataScreen import com.bintianqi.owndroid.dpm.WorkProfile +import com.bintianqi.owndroid.dpm.WorkProfileScreen import com.bintianqi.owndroid.dpm.dhizukuErrorStatus import com.bintianqi.owndroid.dpm.dhizukuPermissionGranted import com.bintianqi.owndroid.dpm.getDPM @@ -144,6 +211,7 @@ import com.rosan.dhizuku.api.Dhizuku import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable import org.lsposed.hiddenapibypass.HiddenApiBypass import java.util.Locale @@ -162,7 +230,8 @@ class MainActivity : FragmentActivity() { val vm by viewModels() lifecycleScope.launch { delay(5000); setDefaultAffiliationID(context) } setContent { - OwnDroidTheme(vm) { + val theme by vm.theme.collectAsStateWithLifecycle() + OwnDroidTheme(theme) { Home(this, vm) } } @@ -208,9 +277,10 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { context.showOperationResultToast(false) } } + fun navigateUp() { navCtrl.navigateUp() } @Suppress("NewApi") NavHost( navController = navCtrl, - startDestination = "HomePage", + startDestination = Home, modifier = Modifier .fillMaxSize() .background(colorScheme.background) @@ -221,129 +291,121 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { popEnterTransition = Animations.navHostPopEnterTransition, popExitTransition = Animations.navHostPopExitTransition ) { - composable(route = "HomePage") { HomePage(navCtrl) } + composable { HomeScreen { navCtrl.navigate(it) } } - composable(route = "Permissions") { Permissions(navCtrl) } - composable(route = "Shizuku") { Shizuku(navCtrl, it.arguments!!) } - composable(route = "AccountsViewer") { AccountsViewer(navCtrl, it.arguments!!) } - composable(route = "DeviceAdmin") { DeviceAdmin(navCtrl) } - composable(route = "ProfileOwner") { ProfileOwner(navCtrl) } - composable(route = "DeviceOwner") { DeviceOwner(navCtrl) } - composable(route = "DelegatedAdmins") { DelegatedAdmins(navCtrl, vm) } - composable(route = "DeviceInfo") { DeviceInfo(navCtrl) } - composable(route = "LockScreenInfo") { LockScreenInfo(navCtrl) } - composable(route = "SupportMessages") { SupportMessages(navCtrl) } - composable(route = "TransferOwnership") { TransferOwnership(navCtrl) } - - composable(route = "System") { SystemManage(navCtrl) } - composable(route = "SystemOptions") { SystemOptions(navCtrl) } - composable(route = "Keyguard") { Keyguard(navCtrl) } - composable(route = "HardwareMonitor") { HardwareMonitor(navCtrl) } - composable(route = "ChangeTime") { ChangeTime(navCtrl) } - composable(route = "ChangeTimeZone") { ChangeTimeZone(navCtrl) } - //composable(route = "KeyPairs") { KeyPairs(navCtrl) } - composable(route = "ContentProtectionPolicy") { ContentProtectionPolicy(navCtrl) } - composable(route = "PermissionPolicy") { PermissionPolicy(navCtrl) } - composable(route = "MTEPolicy") { MTEPolicy(navCtrl) } - composable(route = "NearbyStreamingPolicy") { NearbyStreamingPolicy(navCtrl) } - composable(route = "LockTaskMode") { LockTaskMode(navCtrl, vm) } - composable(route = "CACert") { CACert(navCtrl) } - composable(route = "SecurityLogging") { SecurityLogging(navCtrl) } - composable(route = "DisableAccountManagement") { DisableAccountManagement(navCtrl) } - composable(route = "SystemUpdatePolicy") { SystemUpdatePolicy(navCtrl) } - composable(route = "InstallSystemUpdate") { InstallSystemUpdate(navCtrl) } - composable(route = "FRPPolicy") { FRPPolicy(navCtrl) } - composable(route = "WipeData") { WipeData(navCtrl) } - - composable(route = "Network") { Network(navCtrl) } - composable(route = "Wifi") { Wifi(navCtrl) } - composable(route = "NetworkOptions") { NetworkOptions(navCtrl) } - composable(route = "UpdateNetwork") { UpdateNetwork(it.arguments!!, navCtrl) } - composable(route = "MinWifiSecurityLevel") { WifiSecurityLevel(navCtrl) } - composable(route = "WifiSsidPolicy") { WifiSsidPolicy(navCtrl) } - composable(route = "NetworkStats") { NetworkStats(navCtrl, vm) } - composable(route = "NetworkStatsViewer") { NetworkStatsViewer(navCtrl, it.arguments!!) } - composable(route = "PrivateDNS") { PrivateDNS(navCtrl) } - composable(route = "AlwaysOnVpn") { AlwaysOnVPNPackage(navCtrl, vm) } - composable(route = "RecommendedGlobalProxy") { RecommendedGlobalProxy(navCtrl) } - composable(route = "NetworkLog") { NetworkLogging(navCtrl) } - composable(route = "WifiAuthKeypair") { WifiAuthKeypair(navCtrl) } - composable(route = "PreferentialNetworkService") { PreferentialNetworkService(navCtrl) } - composable(route = "OverrideAPN") { OverrideAPN(navCtrl) } - - composable(route = "WorkProfile") { WorkProfile(navCtrl) } - composable(route = "OrgOwnedWorkProfile") { OrgOwnedProfile(navCtrl) } - composable(route = "CreateWorkProfile") { CreateWorkProfile(navCtrl) } - composable(route = "SuspendPersonalApp") { SuspendPersonalApp(navCtrl) } - composable(route = "IntentFilter") { IntentFilter(navCtrl) } - composable(route = "DeleteWorkProfile") { DeleteWorkProfile(navCtrl) } - - composable(route = "Applications") { ApplicationManage(navCtrl, vm) } - - composable(route = "UserRestriction") { UserRestriction(navCtrl, vm) } - composable(route = "UR-Internet") { - UserRestrictionScreen(R.string.network_and_internet, RestrictionData.internet, userRestrictions, ::onUserRestrictionsChange) { - navCtrl.navigateUp() + composable { + PermissionsScreen(::navigateUp, { navCtrl.navigate(it) }) { + val dest = navCtrl.graph.findNode(ShizukuScreen)!!.id + navCtrl.navigate(dest, it) } } - composable(route = "UR-Connectivity") { - UserRestrictionScreen(R.string.connectivity, RestrictionData.connectivity, userRestrictions, ::onUserRestrictionsChange) { - navCtrl.navigateUp() + composable { ShizukuScreen(it.arguments!!, ::navigateUp) { navCtrl.navigate(it) } } + composable(mapOf(serializableNavTypePair>())) { AccountsScreen(it.toRoute(), ::navigateUp) } + composable { DeviceAdminScreen(::navigateUp) } + composable { ProfileOwnerScreen(::navigateUp) } + composable { DeviceOwnerScreen(::navigateUp) } + composable { DelegatedAdminsScreen(::navigateUp) } + composable { DeviceInfoScreen(::navigateUp) } + composable { LockScreenInfoScreen(::navigateUp) } + composable { SupportMessageScreen(::navigateUp) } + composable { TransferOwnershipScreen(::navigateUp) } + + composable { SystemManagerScreen(::navigateUp) { navCtrl.navigate(it) } } + composable { SystemOptionsScreen(::navigateUp) } + composable { KeyguardScreen(::navigateUp) } + composable { HardwareMonitorScreen(::navigateUp) } + composable { ChangeTimeScreen(::navigateUp) } + composable { ChangeTimeZoneScreen(::navigateUp) } + //composable<> { KeyPairs(::navigateUp) } + composable { ContentProtectionPolicyScreen(::navigateUp) } + composable { PermissionPolicyScreen(::navigateUp) } + composable { MtePolicyScreen(::navigateUp) } + composable { NearbyStreamingPolicyScreen(::navigateUp) } + composable { LockTaskModeScreen(::navigateUp) } + composable { CaCertScreen(::navigateUp) } + composable { SecurityLoggingScreen(::navigateUp) } + composable { DisableAccountManagementScreen(::navigateUp) } + composable { SystemUpdatePolicy(::navigateUp) } + composable { InstallSystemUpdateScreen(::navigateUp) } + composable { FrpPolicyScreen(::navigateUp) } + composable { WipeDataScreen(::navigateUp) } + + composable { NetworkScreen(::navigateUp) { navCtrl.navigate(it) } } + composable { + WifiScreen(::navigateUp, { navCtrl.navigate(it) }) { + val dest = navCtrl.graph.findNode(AddNetwork)!!.id + navCtrl.navigate(dest, it) } } - composable(route = "UR-Applications") { - UserRestrictionScreen(R.string.applications, RestrictionData.applications, userRestrictions, ::onUserRestrictionsChange) { - navCtrl.navigateUp() + composable { NetworkOptionsScreen(::navigateUp) } + composable { AddNetworkScreen(it.arguments!!, ::navigateUp) } + composable { WifiSecurityLevelScreen(::navigateUp) } + composable { WifiSsidPolicyScreen(::navigateUp) } + composable { NetworkStatsScreen(::navigateUp) { navCtrl.navigate(it) } } + composable(mapOf(serializableNavTypePair>())) { + NetworkStatsViewerScreen(it.toRoute()) { navCtrl.navigateUp() } + } + composable { PrivateDnsScreen(::navigateUp) } + composable { AlwaysOnVpnPackageScreen(::navigateUp) } + composable { RecommendedGlobalProxyScreen(::navigateUp) } + composable { NetworkLoggingScreen(::navigateUp) } + composable { WifiAuthKeypairScreen(::navigateUp) } + composable { PreferentialNetworkServiceScreen(::navigateUp) } + composable { OverrideApnScreen(::navigateUp) } + + composable { WorkProfileScreen(::navigateUp) { navCtrl.navigate(it) } } + composable { OrganizationOwnedProfileScreen(::navigateUp) } + composable { CreateWorkProfileScreen(::navigateUp) } + composable { SuspendPersonalAppScreen(::navigateUp) } + composable { CrossProfileIntentFilterScreen(::navigateUp) } + composable { DeleteWorkProfileScreen(::navigateUp) } + + composable { ApplicationsScreen(::navigateUp) } + + composable { + LaunchedEffect(Unit) { + vm.userRestrictions.value = dpm.getUserRestrictions(receiver) + } + UserRestrictionScreen(::navigateUp) { title, items -> + navCtrl.navigate(UserRestrictionOptions(title, items)) } } - composable(route = "UR-Users") { - UserRestrictionScreen(R.string.users, RestrictionData.users, userRestrictions, ::onUserRestrictionsChange) { - navCtrl.navigateUp() - } - } - composable(route = "UR-Media") { - UserRestrictionScreen(R.string.media, RestrictionData.media, userRestrictions, ::onUserRestrictionsChange) { - navCtrl.navigateUp() - } - } - composable(route = "UR-Other") { - UserRestrictionScreen(R.string.other, RestrictionData.other, userRestrictions, ::onUserRestrictionsChange) { - navCtrl.navigateUp() - } + composable(mapOf(serializableNavTypePair>())) { + UserRestrictionOptionsScreen(it.toRoute(), userRestrictions, ::onUserRestrictionsChange, ::navigateUp) } - composable(route = "Users") { Users(navCtrl) } - composable(route = "UserInfo") { CurrentUserInfo(navCtrl) } - composable(route = "UserOptions") { UserOptions(navCtrl) } - composable(route = "UserOperation") { UserOperation(navCtrl) } - composable(route = "CreateUser") { CreateUser(navCtrl) } - composable(route = "ChangeUsername") { ChangeUsername(navCtrl) } - composable(route = "ChangeUserIcon") { ChangeUserIcon(navCtrl) } - composable(route = "UserSessionMessage") { UserSessionMessage(navCtrl) } - composable(route = "AffiliationID") { AffiliationID(navCtrl) } + composable { UsersScreen(::navigateUp) { navCtrl.navigate(it) } } + composable { UserInfoScreen(::navigateUp) } + composable { UsersOptionsScreen(::navigateUp) } + composable { UserOperationScreen(::navigateUp) } + composable { CreateUserScreen(::navigateUp) } + composable { ChangeUsernameScreen(::navigateUp) } + composable { ChangeUserIconScreen(::navigateUp) } + composable { UserSessionMessageScreen(::navigateUp) } + composable { AffiliationIdScreen(::navigateUp) } - composable(route = "Password") { Password(navCtrl) } - composable(route = "PasswordInfo") { PasswordInfo(navCtrl) } - composable(route = "ResetPasswordToken") { ResetPasswordToken(navCtrl) } - composable(route = "ResetPassword") { ResetPassword(navCtrl) } - composable(route = "RequirePasswordComplexity") { PasswordComplexity(navCtrl) } - composable(route = "DisableKeyguardFeatures") { DisableKeyguardFeatures(navCtrl) } - composable(route = "RequirePasswordQuality") { PasswordQuality(navCtrl) } + composable { PasswordScreen(::navigateUp) { navCtrl.navigate(it) } } + composable { PasswordInfoScreen(::navigateUp) } + composable { ResetPasswordTokenScreen(::navigateUp) } + composable { ResetPasswordScreen(::navigateUp) } + composable { RequiredPasswordComplexityScreen(::navigateUp) } + composable { KeyguardDisabledFeaturesScreen(::navigateUp) } + composable { RequiredPasswordQualityScreen(::navigateUp) } - composable(route = "Settings") { Settings(navCtrl) } - composable(route = "Options") { SettingsOptions(navCtrl) } - composable(route = "Appearance") { Appearance(navCtrl, vm) } - composable(route = "AuthSettings") { AuthSettings(navCtrl) } - composable(route = "ApiSettings") { ApiSettings(navCtrl) } - composable(route = "About") { About(navCtrl) } + composable { SettingsScreen(::navigateUp) { navCtrl.navigate(it) } } + composable { SettingsOptionsScreen(::navigateUp) } + composable { + val theme by vm.theme.collectAsStateWithLifecycle() + AppearanceScreen(::navigateUp, theme) { vm.theme.value = it } + } + composable { AuthSettingsScreen(::navigateUp) } + composable { ApiSettings(::navigateUp) } + composable { AboutScreen(::navigateUp) } - composable(route = "PackageSelector") { PackageSelector(navCtrl, vm) } - - composable( - route = "Authenticate", + composable( enterTransition = { fadeIn(animationSpec = tween(200)) }, popExitTransition = { fadeOut(animationSpec = tween(400)) } - ) { Authenticate(activity, navCtrl) } + ) { AuthenticateScreen(activity, ::navigateUp) } } DisposableEffect(lifecycleOwner) { val observer = LifecycleEventObserver { _, event -> @@ -351,7 +413,7 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { (event == Lifecycle.Event.ON_RESUME && sp.auth && sp.lockInBackground) || (event == Lifecycle.Event.ON_CREATE && sp.auth) ) { - navCtrl.navigate("Authenticate") { launchSingleTop = true } + navCtrl.navigate(Authenticate) { launchSingleTop = true } } } lifecycleOwner.lifecycle.addObserver(observer) @@ -370,8 +432,10 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { DhizukuErrorDialog() } +@Serializable private object Home + @Composable -private fun HomePage(navCtrl:NavHostController) { +private fun HomeScreen(onNavigate: (Any) -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -406,7 +470,7 @@ private fun HomePage(navCtrl:NavHostController) { .padding(vertical = 8.dp, horizontal = 8.dp) .clip(RoundedCornerShape(15)) .background(color = colorScheme.primary) - .clickable(onClick = { navCtrl.navigate("Permissions") }) + .clickable(onClick = { onNavigate(Permissions) }) .padding(vertical = 16.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -427,33 +491,33 @@ private fun HomePage(navCtrl:NavHostController) { if(activateType != "") { Text(text = activateType, color = colorScheme.onPrimary) } } } - HomePageItem(R.string.system, R.drawable.android_fill0, "System", navCtrl) - if(deviceOwner || profileOwner) { HomePageItem(R.string.network, R.drawable.wifi_fill0, "Network", navCtrl) } + HomePageItem(R.string.system, R.drawable.android_fill0) { onNavigate(SystemManager) } + if(deviceOwner || profileOwner) { HomePageItem(R.string.network, R.drawable.wifi_fill0) { onNavigate(Network) } } if( (VERSION.SDK_INT < 24 && !deviceOwner) || (dpm.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE) || (profileOwner && dpm.isManagedProfile(receiver)) ) ) { - HomePageItem(R.string.work_profile, R.drawable.work_fill0, "ManagedProfile", navCtrl) + HomePageItem(R.string.work_profile, R.drawable.work_fill0) { onNavigate(WorkProfile) } } - if(deviceOwner || profileOwner) HomePageItem(R.string.applications, R.drawable.apps_fill0, "Applications", navCtrl) + if(deviceOwner || profileOwner) HomePageItem(R.string.applications, R.drawable.apps_fill0) { onNavigate(Applications) } if(VERSION.SDK_INT >= 24 && (profileOwner || deviceOwner)) { - HomePageItem(R.string.user_restriction, R.drawable.person_off, "UserRestriction", navCtrl) + HomePageItem(R.string.user_restriction, R.drawable.person_off) { onNavigate(UserRestriction) } } - HomePageItem(R.string.users,R.drawable.manage_accounts_fill0,"Users", navCtrl) - HomePageItem(R.string.password_and_keyguard, R.drawable.password_fill0, "Password", navCtrl) - HomePageItem(R.string.settings, R.drawable.settings_fill0, "Settings", navCtrl) + HomePageItem(R.string.users,R.drawable.manage_accounts_fill0) { onNavigate(Users) } + HomePageItem(R.string.password_and_keyguard, R.drawable.password_fill0) { onNavigate(Password) } + HomePageItem(R.string.settings, R.drawable.settings_fill0) { onNavigate(Settings) } Spacer(Modifier.padding(vertical = 20.dp)) } } } @Composable -fun HomePageItem(name: Int, imgVector: Int, navTo: String, navCtrl: NavHostController) { +fun HomePageItem(name: Int, imgVector: Int, onClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() - .clickable(onClick = { navCtrl.navigate(navTo) }) + .clickable(onClick = onClick) .padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { diff --git a/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt b/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt index 5279221..1bf3f21 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.core.view.WindowCompat import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bintianqi.owndroid.ui.theme.OwnDroidTheme import kotlin.system.exitProcess @@ -55,7 +56,8 @@ class ManageSpaceActivity: FragmentActivity() { } } } - OwnDroidTheme(vm) { + val theme by vm.theme.collectAsStateWithLifecycle() + OwnDroidTheme(theme) { AlertDialog( text = { Text(stringResource(R.string.clear_storage)) diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index efeb6a9..f4f0eea 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -9,8 +9,6 @@ import kotlinx.coroutines.launch class MyViewModel(application: Application): AndroidViewModel(application) { val theme = MutableStateFlow(ThemeSettings()) - val installedPackages = mutableListOf() - val selectedPackage = MutableStateFlow("") val userRestrictions = MutableStateFlow(Bundle()) init { diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt new file mode 100644 index 0000000..62bb659 --- /dev/null +++ b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt @@ -0,0 +1,224 @@ +package com.bintianqi.owndroid + +import android.app.Application +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope +import com.bintianqi.owndroid.ui.theme.OwnDroidTheme +import com.google.accompanist.drawablepainter.rememberDrawablePainter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class PackageChooserActivity: ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val myVm by viewModels() + val vm by viewModels() + vm.initialize() + setContent { + val theme by myVm.theme.collectAsStateWithLifecycle() + OwnDroidTheme(theme) { + val packages by vm.packages.collectAsStateWithLifecycle() + val progress by vm.progress.collectAsStateWithLifecycle() + PackageChooserScreen(packages, progress, vm::getPackages) { + setResult(0, Intent().putExtra("package", it)) + finish() + } + } + } + } +} + +class PackageChooserViewModel(application: Application): AndroidViewModel(application) { + val packages = MutableStateFlow(emptyList()) + val progress = MutableStateFlow(0F) + val flags = if(Build.VERSION.SDK_INT >= 24) PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_UNINSTALLED_PACKAGES else 0 + fun initialize() { + if(progress.value < 1F) getPackages() + } + fun getPackages() { + packages.value = emptyList() + viewModelScope.launch(Dispatchers.IO) { + val pm = getApplication().packageManager + val apps = pm.getInstalledApplications(flags) + for(pkg in apps) { + packages.update { + it + PackageInfo( + pkg.packageName, pkg.loadLabel(pm).toString(), pkg.loadIcon(pm), + (pkg.flags and ApplicationInfo.FLAG_SYSTEM) != 0 + ) + } + withContext(Dispatchers.Main) { progress.value = packages.value.size.toFloat() / apps.size } + } + } + } +} + +data class PackageInfo( + val name: String, + val label: String, + val icon: Drawable, + val system: Boolean +) + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +private fun PackageChooserScreen( + packages: List, progress: Float, onRefresh: () -> Unit, onChoosePackage: (String?) -> Unit +) { + val context = LocalContext.current + var system by remember { mutableStateOf(false) } + var search by remember { mutableStateOf("") } + var searchMode by remember { mutableStateOf(false) } + val filteredPackages = packages.filter { + system == it.system && + (if(search.isEmpty()) true + else it.name.contains(search, ignoreCase = true) || it.label.contains(search, ignoreCase = true)) + } + val focusMgr = LocalFocusManager.current + Scaffold( + topBar = { + TopAppBar( + actions = { + if(!searchMode) { + IconButton({ searchMode = true }) { + Icon(painter = painterResource(R.drawable.search_fill0), contentDescription = stringResource(R.string.search)) + } + IconButton({ + system = !system + Toast.makeText(context, if(system) R.string.show_system_app else R.string.show_user_app, Toast.LENGTH_SHORT).show() + }) { + Icon(painter = painterResource(R.drawable.filter_alt_fill0), contentDescription = null) + } + IconButton(onRefresh) { + Icon(painter = painterResource(R.drawable.refresh_fill0), contentDescription = null) + } + } + }, + title = { + if(searchMode) { + val fr = FocusRequester() + LaunchedEffect(Unit) { fr.requestFocus() } + OutlinedTextField( + value = search, + onValueChange = { search = it }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions { focusMgr.clearFocus() }, + placeholder = { Text(stringResource(R.string.search)) }, + trailingIcon = { + Icon( + painter = painterResource(R.drawable.close_fill0), + contentDescription = null, + modifier = Modifier.clickable { + focusMgr.clearFocus() + search = "" + searchMode = false + } + ) + }, + textStyle = typography.bodyLarge, + modifier = Modifier.fillMaxWidth().focusRequester(fr) + ) + } else { + Text(stringResource(R.string.package_chooser)) + } + }, + navigationIcon = { + IconButton({ onChoosePackage(null) }) { + Icon(Icons.AutoMirrored.Default.ArrowBack, null) + } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = colorScheme.background) + ) + } + ) { paddingValues-> + LazyColumn( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize().padding(top = paddingValues.calculateTopPadding()) + ) { + stickyHeader { + AnimatedVisibility(progress < 1F) { + LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth()) + } + } + items(filteredPackages, { it.name }) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable { onChoosePackage(it.name) } + .padding(horizontal = 8.dp, vertical = 10.dp) + .animateItem() + ) { + Image( + painter = rememberDrawablePainter(it.icon), contentDescription = null, + modifier = Modifier.padding(start = 12.dp, end = 18.dp).size(40.dp) + ) + Column { + Text(text = it.label, style = typography.titleLarge) + Text(text = it.name, modifier = Modifier.alpha(0.8F)) + } + } + } + item { Spacer(Modifier.padding(vertical = 30.dp)) } + } + } +} diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageSelector.kt b/app/src/main/java/com/bintianqi/owndroid/PackageSelector.kt deleted file mode 100644 index 08098ba..0000000 --- a/app/src/main/java/com/bintianqi/owndroid/PackageSelector.kt +++ /dev/null @@ -1,230 +0,0 @@ -package com.bintianqi.owndroid - -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.graphics.drawable.Drawable -import android.os.Build -import android.widget.Toast -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.material3.MaterialTheme.typography -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import com.bintianqi.owndroid.ui.NavIcon -import com.google.accompanist.drawablepainter.rememberDrawablePainter -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -data class PackageInfo( - val pkgName: String, - val label: String, - val icon: Drawable, - val system: Boolean -) - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PackageSelector(navCtrl: NavHostController, vm: MyViewModel) { - val context = LocalContext.current - val pm = context.packageManager - val flags = if(Build.VERSION.SDK_INT >= 24) PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_UNINSTALLED_PACKAGES else 0 - val apps = pm.getInstalledApplications(flags) - var progress by remember { mutableIntStateOf(0) } - var show by remember { mutableStateOf(true) } - var hideProgress by remember { mutableStateOf(true) } - var system by remember { mutableStateOf(false) } - var search by remember { mutableStateOf("") } - var searchMode by remember { mutableStateOf(false) } - val scrollState = rememberLazyListState() - val focusMgr = LocalFocusManager.current - val co = rememberCoroutineScope() - suspend fun getPkgList() { - show = false - progress = 0 - hideProgress = false - vm.installedPackages.clear() - for(pkg in apps) { - vm.installedPackages += PackageInfo( - pkg.packageName, pkg.loadLabel(pm).toString(), pkg.loadIcon(pm), - (pkg.flags and ApplicationInfo.FLAG_SYSTEM) != 0 - ) - withContext(Dispatchers.Main) { progress += 1 } - } - show = true - delay(500) - hideProgress = true - } - Scaffold( - topBar = { - TopAppBar( - actions = { - if(!searchMode) { - Icon( - painter = painterResource(R.drawable.search_fill0), - contentDescription = "search", - modifier = Modifier - .padding(horizontal = 4.dp) - .clip(RoundedCornerShape(50)) - .clickable { searchMode = true } - .padding(5.dp) - ) - Icon( - painter = painterResource(R.drawable.filter_alt_fill0), - contentDescription = "filter", - modifier = Modifier - .padding(horizontal = 4.dp) - .clip(RoundedCornerShape(50)) - .clickable { - system = !system - Toast.makeText(context, if(system) R.string.show_system_app else R.string.show_user_app, Toast.LENGTH_SHORT).show() - } - .padding(5.dp) - ) - Icon( - painter = painterResource(R.drawable.refresh_fill0), - contentDescription = "refresh", - modifier = Modifier - .padding(horizontal = 4.dp) - .clip(RoundedCornerShape(50)) - .clickable { co.launch { getPkgList() } } - .padding(5.dp) - ) - } - }, - title = { - if(searchMode) { - val fr = FocusRequester() - LaunchedEffect(Unit) { fr.requestFocus() } - OutlinedTextField( - value = search, - onValueChange = { search = it }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions { focusMgr.clearFocus() }, - placeholder = { Text(stringResource(R.string.search)) }, - trailingIcon = { - Icon( - painter = painterResource(R.drawable.close_fill0), - contentDescription = "clear search", - modifier = Modifier.clickable { - focusMgr.clearFocus() - search = "" - searchMode = false - } - ) - }, - textStyle = typography.bodyLarge, - modifier = Modifier.fillMaxWidth().focusRequester(fr) - ) - } else { - Text(stringResource(R.string.pkg_selector)) - } - }, - navigationIcon = { NavIcon{ navCtrl.navigateUp() } }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = colorScheme.background) - ) - } - ) { paddingValues-> - LazyColumn( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize().padding(top = paddingValues.calculateTopPadding()), - state = scrollState - ) { - items(1) { - AnimatedVisibility(!hideProgress) { - LinearProgressIndicator(progress = { progress.toFloat()/apps.size }, modifier = Modifier.fillMaxWidth()) - } - } - if(show) { - items(vm.installedPackages) { - if(system == it.system) { - if(search != "") { - if(it.pkgName.contains(search, ignoreCase = true) || it.label.contains(search, ignoreCase = true)) { - PackageItem(it, navCtrl, vm) - } - } else { - PackageItem(it, navCtrl, vm) - } - } - } - items(1) { Spacer(Modifier.padding(vertical = 30.dp)) } - } else { - items(1) { - Spacer(Modifier.padding(top = 5.dp)) - Text(text = stringResource(R.string.loading), modifier = Modifier.alpha(0.8F)) - } - } - } - LaunchedEffect(Unit) { - if(vm.installedPackages.isEmpty()) { getPkgList() } - } - } -} - -@Composable -private fun PackageItem(pkg: PackageInfo, navCtrl: NavHostController, vm: MyViewModel) { - val focusMgr = LocalFocusManager.current - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable{ - focusMgr.clearFocus() - vm.selectedPackage.value = pkg.pkgName - navCtrl.navigateUp() - } - .padding(horizontal = 8.dp, vertical = 10.dp) - ) { - Image( - painter = rememberDrawablePainter(pkg.icon), contentDescription = "App icon", - modifier = Modifier.padding(start = 12.dp, end = 18.dp).size(40.dp) - ) - Column { - Text(text = pkg.label, style = typography.titleLarge) - Text(text = pkg.pkgName, modifier = Modifier.alpha(0.8F)) - } - } -} diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index 6d9c23e..4fd2de7 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -32,29 +32,32 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.core.content.edit -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.SwitchItem +import kotlinx.serialization.Serializable import java.security.SecureRandom +@Serializable object Settings + @Composable -fun Settings(navCtrl: NavHostController) { - MyScaffold(R.string.settings, 0.dp, navCtrl) { - FunctionItem(title = R.string.options, icon = R.drawable.tune_fill0) { navCtrl.navigate("Options") } - FunctionItem(title = R.string.appearance, icon = R.drawable.format_paint_fill0) { navCtrl.navigate("Appearance") } - FunctionItem(title = R.string.security, icon = R.drawable.lock_fill0) { navCtrl.navigate("AuthSettings") } - FunctionItem(title = R.string.api, icon = R.drawable.apps_fill0) { navCtrl.navigate("ApiSettings") } - FunctionItem(title = R.string.about, icon = R.drawable.info_fill0) { navCtrl.navigate("About") } +fun SettingsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { + MyScaffold(R.string.settings, 0.dp, onNavigateUp) { + FunctionItem(title = R.string.options, icon = R.drawable.tune_fill0) { onNavigate(SettingsOptions) } + FunctionItem(title = R.string.appearance, icon = R.drawable.format_paint_fill0) { onNavigate(Appearance) } + FunctionItem(title = R.string.security, icon = R.drawable.lock_fill0) { onNavigate(AuthSettings) } + FunctionItem(title = R.string.api, icon = R.drawable.apps_fill0) { onNavigate(ApiSettings) } + FunctionItem(title = R.string.about, icon = R.drawable.info_fill0) { onNavigate(About) } } } +@Serializable object SettingsOptions + @Composable -fun SettingsOptions(navCtrl: NavHostController) { +fun SettingsOptionsScreen(onNavigateUp: () -> Unit) { val sp = SharedPrefs(LocalContext.current) - MyScaffold(R.string.options, 0.dp, navCtrl) { + MyScaffold(R.string.options, 0.dp, onNavigateUp) { SwitchItem( R.string.show_dangerous_features, icon = R.drawable.warning_fill0, getState = { sp.displayDangerousFeatures }, @@ -63,18 +66,19 @@ fun SettingsOptions(navCtrl: NavHostController) { } } +@Serializable object Appearance + @Composable -fun Appearance(navCtrl: NavHostController, vm: MyViewModel) { - val theme by vm.theme.collectAsStateWithLifecycle() +fun AppearanceScreen(onNavigateUp: () -> Unit, theme: ThemeSettings, onThemeChange: (ThemeSettings) -> Unit) { var darkThemeMenu by remember { mutableStateOf(false) } val darkThemeTextID = when(theme.darkTheme) { 1 -> R.string.on 0 -> R.string.off else -> R.string.follow_system } - MyScaffold(R.string.appearance, 0.dp, navCtrl) { + MyScaffold(R.string.appearance, 0.dp, onNavigateUp) { if(VERSION.SDK_INT >= 31) { - SwitchItem(R.string.material_you_color, state = theme.materialYou, onCheckedChange = { vm.theme.value = theme.copy(materialYou = it) }) + SwitchItem(R.string.material_you_color, state = theme.materialYou, onCheckedChange = { onThemeChange(theme.copy(materialYou = it)) }) } Box { FunctionItem(R.string.dark_theme, stringResource(darkThemeTextID)) { darkThemeMenu = true } @@ -85,38 +89,40 @@ fun Appearance(navCtrl: NavHostController, vm: MyViewModel) { DropdownMenuItem( text = { Text(stringResource(R.string.follow_system)) }, onClick = { - vm.theme.value = theme.copy(darkTheme = -1) + onThemeChange(theme.copy(darkTheme = -1)) darkThemeMenu = false } ) DropdownMenuItem( text = { Text(stringResource(R.string.on)) }, onClick = { - vm.theme.value = theme.copy(darkTheme = 1) + onThemeChange(theme.copy(darkTheme = 1)) darkThemeMenu = false } ) DropdownMenuItem( text = { Text(stringResource(R.string.off)) }, onClick = { - vm.theme.value = theme.copy(darkTheme = 0) + onThemeChange(theme.copy(darkTheme = 0)) darkThemeMenu = false } ) } } AnimatedVisibility(theme.darkTheme == 1 || (theme.darkTheme == -1 && isSystemInDarkTheme())) { - SwitchItem(R.string.black_theme, state = theme.blackTheme, onCheckedChange = { vm.theme.value = theme.copy(blackTheme = it) }) + SwitchItem(R.string.black_theme, state = theme.blackTheme, onCheckedChange = { onThemeChange(theme.copy(blackTheme = it)) }) } } } +@Serializable object AuthSettings + @Composable -fun AuthSettings(navCtrl: NavHostController) { +fun AuthSettingsScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val sp = SharedPrefs(context) var auth by remember{ mutableStateOf(sp.auth) } - MyScaffold(R.string.security, 0.dp, navCtrl) { + MyScaffold(R.string.security, 0.dp, onNavigateUp) { SwitchItem( R.string.lock_owndroid, state = auth, onCheckedChange = { @@ -146,11 +152,13 @@ fun AuthSettings(navCtrl: NavHostController) { } } +@Serializable object ApiSettings + @Composable -fun ApiSettings(navCtrl: NavHostController) { +fun ApiSettings(onNavigateUp: () -> Unit) { val context = LocalContext.current val sp = SharedPrefs(context) - MyScaffold(R.string.api, 8.dp, navCtrl) { + MyScaffold(R.string.api, 8.dp, onNavigateUp) { var enabled by remember { mutableStateOf(sp.isApiEnabled) } SwitchItem(R.string.enable, state = enabled, onCheckedChange = { enabled = it @@ -188,13 +196,15 @@ fun ApiSettings(navCtrl: NavHostController) { } } +@Serializable object About + @Composable -fun About(navCtrl: NavHostController) { +fun AboutScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val pkgInfo = context.packageManager.getPackageInfo(context.packageName,0) val verCode = pkgInfo.versionCode val verName = pkgInfo.versionName - MyScaffold(R.string.about, 0.dp, navCtrl) { + MyScaffold(R.string.about, 0.dp, onNavigateUp) { Text(text = stringResource(R.string.app_name)+" v$verName ($verCode)", modifier = Modifier.padding(start = 16.dp)) Spacer(Modifier.padding(vertical = 5.dp)) FunctionItem(R.string.project_homepage, "GitHub", R.drawable.open_in_new) { shareLink(context, "https://github.com/BinTianqi/OwnDroid") } diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 3fd33c1..7bc9bf4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -6,13 +6,19 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.ComponentName import android.content.Context +import android.content.Intent import android.net.Uri +import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContract import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.annotation.StringRes +import androidx.navigation.NavType import com.bintianqi.owndroid.dpm.addDeviceAdmin +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream @@ -22,6 +28,7 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Date import java.util.Locale +import kotlin.reflect.typeOf var zhCN = true @@ -93,3 +100,22 @@ fun getContext(): Context { } const val APK_MIME = "application/vnd.android.package-archive" + +inline fun serializableNavTypePair() = + typeOf() to object : NavType(false) { + override fun get(bundle: Bundle, key: String): T? = + bundle.getString(key)?.let { parseValue(it) } + override fun put(bundle: Bundle, key: String, value: T) = + bundle.putString(key, serializeAsValue(value)) + override fun parseValue(value: String): T = + Json.decodeFromString(value) + override fun serializeAsValue(value: T): String = + Json.encodeToString(value) +} + +class ChoosePackageContract: ActivityResultContract() { + override fun createIntent(context: Context, input: Nothing?): Intent = + Intent(context, PackageChooserActivity::class.java) + override fun parseResult(resultCode: Int, intent: Intent?): String? = + intent?.getStringExtra("package") +} diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index 45e5180..3976e42 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -79,16 +79,14 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat.startActivity -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.APK_MIME import com.bintianqi.owndroid.AppInstallerActivity import com.bintianqi.owndroid.AppInstallerViewModel -import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.R +import com.bintianqi.owndroid.ChoosePackageContract import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.FunctionItem @@ -97,20 +95,20 @@ import com.bintianqi.owndroid.ui.ListItem import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.RadioButtonItem import com.bintianqi.owndroid.ui.SwitchItem +import kotlinx.serialization.Serializable import java.util.concurrent.Executors +@Serializable +object Applications + @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ApplicationManage(navCtrl:NavHostController, vm: MyViewModel) { +fun ApplicationsScreen(onNavigateUp: () -> Unit) { val focusMgr = LocalFocusManager.current - val localNavCtrl = rememberNavController() + val navController = rememberNavController() var pkgName by rememberSaveable { mutableStateOf("") } - val updatePackage by vm.selectedPackage.collectAsStateWithLifecycle() - LaunchedEffect(updatePackage) { - if(updatePackage != "") { - pkgName = updatePackage - vm.selectedPackage.value = "" - } + val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) {result -> + result?.let { pkgName = it } } Scaffold( topBar = { @@ -129,7 +127,7 @@ fun ApplicationManage(navCtrl:NavHostController, vm: MyViewModel) { .clip(RoundedCornerShape(50)) .clickable(onClick = { focusMgr.clearFocus() - navCtrl.navigate("PackageSelector") + choosePackage.launch(null) }) .padding(3.dp)) }, @@ -137,35 +135,37 @@ fun ApplicationManage(navCtrl:NavHostController, vm: MyViewModel) { singleLine = true ) }, - navigationIcon = { NavIcon { navCtrl.navigateUp() } }, + navigationIcon = { NavIcon(onNavigateUp) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = colorScheme.background) ) } ) { paddingValues-> @Suppress("NewApi") NavHost( modifier = Modifier.padding(top = paddingValues.calculateTopPadding()), - navController = localNavCtrl, startDestination = "Home", + navController = navController, startDestination = Home, enterTransition = Animations.navHostEnterTransition, exitTransition = Animations.navHostExitTransition, popEnterTransition = Animations.navHostPopEnterTransition, popExitTransition = Animations.navHostPopExitTransition ) { - composable(route = "Home") { Home(localNavCtrl, pkgName) } - composable(route = "UserControlDisabled") { UserCtrlDisabledPkg(pkgName) } - composable(route = "PermissionManage") { PermissionManage(pkgName) } - composable(route = "CrossProfilePackage") { CrossProfilePkg(pkgName) } - composable(route = "CrossProfileWidget") { CrossProfileWidget(pkgName) } - composable(route = "CredentialManagePolicy") { CredentialManagePolicy(pkgName) } - composable(route = "Accessibility") { PermittedAccessibility(pkgName) } - composable(route = "IME") { PermittedIME(pkgName) } - composable(route = "KeepUninstalled") { KeepUninstalledApp(pkgName) } - composable(route = "UninstallApp") { UninstallApp(pkgName) } + composable { HomeScreen(pkgName) { navController.navigate(it) } } + composable { UserControlDisabledPackagesScreen(pkgName) } + composable { PermissionManagerScreen(pkgName) } + composable { CrossProfilePackagesScreen(pkgName) } + composable { CrossProfileWidgetProvidersScreen(pkgName) } + composable { CredentialManagerPolicyScreen(pkgName) } + composable { PermittedAccessibilityServicesScreen(pkgName) } + composable { PermittedInputMethodsScreen(pkgName) } + composable { KeepUninstalledPackagesScreen(pkgName) } + composable { UninstallPackageScreen(pkgName) } } } } +@Serializable private object Home + @Composable -private fun Home(navCtrl:NavHostController, pkgName: String) { +private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) { var dialogStatus by remember { mutableIntStateOf(0) } val context = LocalContext.current val dpm = context.getDPM() @@ -231,27 +231,29 @@ private fun Home(navCtrl:NavHostController, pkgName: String) { onClickBlank = { appControlAction = 3; dialogStatus = 4 } ) if(VERSION.SDK_INT >= 30 && (deviceOwner || (VERSION.SDK_INT >= 33 && profileOwner))) { - FunctionItem(title = R.string.ucd, icon = R.drawable.do_not_touch_fill0) { navCtrl.navigate("UserControlDisabled") } + FunctionItem(title = R.string.ucd, icon = R.drawable.do_not_touch_fill0) { onNavigate(UserControlDisabledPackages) } } if(VERSION.SDK_INT>=23) { - FunctionItem(title = R.string.permission_manage, icon = R.drawable.key_fill0) { navCtrl.navigate("PermissionManage") } + FunctionItem(title = R.string.permission_manage, icon = R.drawable.key_fill0) { onNavigate(PermissionManager) } } if(VERSION.SDK_INT >= 30 && profileOwner && dpm.isManagedProfile(receiver)) { - FunctionItem(title = R.string.cross_profile_package, icon = R.drawable.work_fill0) { navCtrl.navigate("CrossProfilePackage") } + FunctionItem(title = R.string.cross_profile_package, icon = R.drawable.work_fill0) { onNavigate(CrossProfilePackages) } } if(profileOwner) { - FunctionItem(title = R.string.cross_profile_widget, icon = R.drawable.widgets_fill0) { navCtrl.navigate("CrossProfileWidget") } + FunctionItem(title = R.string.cross_profile_widget, icon = R.drawable.widgets_fill0) { onNavigate(CrossProfileWidgetProviders) } } if(VERSION.SDK_INT >= 34 && deviceOwner) { - FunctionItem(title = R.string.credential_manage_policy, icon = R.drawable.license_fill0) { navCtrl.navigate("CredentialManagePolicy") } + FunctionItem(title = R.string.credential_manage_policy, icon = R.drawable.license_fill0) { onNavigate(CredentialManagerPolicy) } } - FunctionItem(title = R.string.permitted_accessibility_services, icon = R.drawable.settings_accessibility_fill0) { navCtrl.navigate("Accessibility") } - FunctionItem(title = R.string.permitted_ime, icon = R.drawable.keyboard_fill0) { navCtrl.navigate("IME") } + FunctionItem(title = R.string.permitted_accessibility_services, icon = R.drawable.settings_accessibility_fill0) { + onNavigate(PermittedAccessibilityServices) + } + FunctionItem(title = R.string.permitted_ime, icon = R.drawable.keyboard_fill0) { onNavigate(PermittedInputMethods) } FunctionItem(title = R.string.enable_system_app, icon = R.drawable.enable_fill0) { if(pkgName != "") dialogStatus = 1 } if(VERSION.SDK_INT >= 28 && deviceOwner) { - FunctionItem(title = R.string.keep_uninstalled_packages, icon = R.drawable.delete_fill0) { navCtrl.navigate("KeepUninstalled") } + FunctionItem(title = R.string.keep_uninstalled_packages, icon = R.drawable.delete_fill0) { onNavigate(KeepUninstalledPackages) } } if(VERSION.SDK_INT >= 28) { FunctionItem(title = R.string.clear_app_storage, icon = R.drawable.mop_fill0) { @@ -267,7 +269,7 @@ private fun Home(navCtrl:NavHostController, pkgName: String) { FunctionItem(title = R.string.install_app, icon = R.drawable.install_mobile_fill0) { chooseApks.launch(APK_MIME) } - FunctionItem(title = R.string.uninstall_app, icon = R.drawable.delete_fill0) { navCtrl.navigate("UninstallApp") } + FunctionItem(title = R.string.uninstall_app, icon = R.drawable.delete_fill0) { onNavigate(UninstallPackage) } if(VERSION.SDK_INT >= 34 && (deviceOwner || dpm.isOrgProfile(receiver))) { FunctionItem(title = R.string.set_default_dialer, icon = R.drawable.call_fill0) { if(pkgName != "") dialogStatus = 3 @@ -425,10 +427,11 @@ private fun Home(navCtrl:NavHostController, pkgName: String) { LaunchedEffect(dialogStatus) { focusMgr.clearFocus() } } +@Serializable private object UserControlDisabledPackages @RequiresApi(30) @Composable -private fun UserCtrlDisabledPkg(pkgName:String) { +private fun UserControlDisabledPackagesScreen(pkgName:String) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -469,9 +472,11 @@ private fun UserCtrlDisabledPkg(pkgName:String) { } } +@Serializable private object PermissionManager + @RequiresApi(23) @Composable -private fun PermissionManage(pkgName: String) { +private fun PermissionManagerScreen(pkgName: String) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -569,9 +574,11 @@ private fun PermissionManage(pkgName: String) { } } +@Serializable private object CrossProfilePackages + @RequiresApi(30) @Composable -private fun CrossProfilePkg(pkgName: String) { +private fun CrossProfilePackagesScreen(pkgName: String) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -610,8 +617,10 @@ private fun CrossProfilePkg(pkgName: String) { } } +@Serializable private object CrossProfileWidgetProviders + @Composable -private fun CrossProfileWidget(pkgName: String) { +private fun CrossProfileWidgetProvidersScreen(pkgName: String) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -649,9 +658,11 @@ private fun CrossProfileWidget(pkgName: String) { } } +@Serializable private object CredentialManagerPolicy + @RequiresApi(34) @Composable -private fun CredentialManagePolicy(pkgName: String) { +private fun CredentialManagerPolicyScreen(pkgName: String) { // TODO: rename "manage" to "manager" val context = LocalContext.current val dpm = context.getDPM() var policy: PackagePolicy? @@ -716,8 +727,10 @@ private fun CredentialManagePolicy(pkgName: String) { } } +@Serializable private object PermittedAccessibilityServices + @Composable -private fun PermittedAccessibility(pkgName: String) { +private fun PermittedAccessibilityServicesScreen(pkgName: String) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -779,8 +792,10 @@ private fun PermittedAccessibility(pkgName: String) { } } +@Serializable private object PermittedInputMethods + @Composable -private fun PermittedIME(pkgName: String) { +private fun PermittedInputMethodsScreen(pkgName: String) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -835,9 +850,11 @@ private fun PermittedIME(pkgName: String) { } } +@Serializable private object KeepUninstalledPackages + @RequiresApi(28) @Composable -private fun KeepUninstalledApp(pkgName: String) { +private fun KeepUninstalledPackagesScreen(pkgName: String) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -879,8 +896,10 @@ private fun KeepUninstalledApp(pkgName: String) { } } +@Serializable private object UninstallPackage + @Composable -private fun UninstallApp(pkgName: String) { +private fun UninstallPackageScreen(pkgName: String) { val context = LocalContext.current Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { Spacer(Modifier.padding(vertical = 10.dp)) diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index 9863495..24f4a64 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -108,7 +108,6 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableLongStateOf @@ -130,9 +129,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.core.os.bundleOf -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController -import com.bintianqi.owndroid.MyViewModel +import com.bintianqi.owndroid.ChoosePackageContract import com.bintianqi.owndroid.R import com.bintianqi.owndroid.SharedPrefs import com.bintianqi.owndroid.formatFileSize @@ -154,57 +151,62 @@ import com.google.accompanist.permissions.rememberPermissionState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable import java.net.InetAddress import kotlin.math.max import kotlin.reflect.jvm.jvmErasure +@Serializable object Network + @Composable -fun Network(navCtrl:NavHostController) { +fun NetworkScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner val dhizuku = SharedPrefs(context).dhizuku - MyScaffold(R.string.network, 0.dp, navCtrl) { - if(!dhizuku) FunctionItem(R.string.wifi, icon = R.drawable.wifi_fill0) { navCtrl.navigate("Wifi") } + MyScaffold(R.string.network, 0.dp, onNavigateUp) { + if(!dhizuku) FunctionItem(R.string.wifi, icon = R.drawable.wifi_fill0) { onNavigate(WiFi) } if(VERSION.SDK_INT >= 30) { - FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { navCtrl.navigate("NetworkOptions") } + FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { onNavigate(NetworkOptions) } } if(VERSION.SDK_INT >= 23 && (deviceOwner || profileOwner)) - FunctionItem(R.string.network_stats, icon = R.drawable.query_stats_fill0) { navCtrl.navigate("NetworkStats") } + FunctionItem(R.string.network_stats, icon = R.drawable.query_stats_fill0) { onNavigate(QueryNetworkStats) } if(VERSION.SDK_INT >= 29 && deviceOwner) { - FunctionItem(R.string.private_dns, icon = R.drawable.dns_fill0) { navCtrl.navigate("PrivateDNS") } + FunctionItem(R.string.private_dns, icon = R.drawable.dns_fill0) { onNavigate(PrivateDns) } } if(VERSION.SDK_INT >= 24) { - FunctionItem(R.string.always_on_vpn, icon = R.drawable.vpn_key_fill0) { navCtrl.navigate("AlwaysOnVpn") } + FunctionItem(R.string.always_on_vpn, icon = R.drawable.vpn_key_fill0) { onNavigate(AlwaysOnVpnPackage) } } if(deviceOwner) { - FunctionItem(R.string.recommended_global_proxy, icon = R.drawable.vpn_key_fill0) { navCtrl.navigate("RecommendedGlobalProxy") } + FunctionItem(R.string.recommended_global_proxy, icon = R.drawable.vpn_key_fill0) { onNavigate(RecommendedGlobalProxy) } } if(VERSION.SDK_INT >= 26 && !dhizuku && (deviceOwner || (profileOwner && dpm.isManagedProfile(receiver)))) { - FunctionItem(R.string.network_logging, icon = R.drawable.description_fill0) { navCtrl.navigate("NetworkLog") } + FunctionItem(R.string.network_logging, icon = R.drawable.description_fill0) { onNavigate(NetworkLogging) } } if(VERSION.SDK_INT >= 31) { - FunctionItem(R.string.wifi_auth_keypair, icon = R.drawable.key_fill0) { navCtrl.navigate("WifiAuthKeypair") } + FunctionItem(R.string.wifi_auth_keypair, icon = R.drawable.key_fill0) { onNavigate(WifiAuthKeypair) } } if(VERSION.SDK_INT >= 33) { - FunctionItem(R.string.preferential_network_service, icon = R.drawable.globe_fill0) { navCtrl.navigate("PreferentialNetworkService") } + FunctionItem(R.string.preferential_network_service, icon = R.drawable.globe_fill0) { onNavigate(PreferentialNetworkService) } } if(VERSION.SDK_INT >= 28 && deviceOwner) { - FunctionItem(R.string.override_apn_settings, icon = R.drawable.cell_tower_fill0) { navCtrl.navigate("OverrideAPN") } + FunctionItem(R.string.override_apn_settings, icon = R.drawable.cell_tower_fill0) { onNavigate(OverrideApn) } } } } +@Serializable object NetworkOptions + @Composable -fun NetworkOptions(navCtrl: NavHostController) { +fun NetworkOptionsScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val deviceOwner = context.isDeviceOwner var dialog by remember { mutableIntStateOf(0) } - MyScaffold(R.string.options, 0.dp, navCtrl) { + MyScaffold(R.string.options, 0.dp, onNavigateUp) { if(VERSION.SDK_INT>=30 && (deviceOwner || dpm.isOrgProfile(receiver))) { SwitchItem(R.string.lockdown_admin_configured_network, icon = R.drawable.wifi_password_fill0, getState = { dpm.hasLockdownAdminConfiguredNetworks(receiver) }, onCheckedChange = { dpm.setConfiguredNetworksLockdownState(receiver,it) }, @@ -221,9 +223,11 @@ fun NetworkOptions(navCtrl: NavHostController) { ) } +@Serializable object WiFi + @OptIn(ExperimentalMaterial3Api::class) @Composable -fun Wifi(navCtrl: NavHostController) { +fun WifiScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit, onNavigateToUpdateNetwork: (Bundle) -> Unit) { val context = LocalContext.current val coroutine = rememberCoroutineScope() val pagerState = rememberPagerState { 3 } @@ -233,7 +237,7 @@ fun Wifi(navCtrl: NavHostController) { topBar = { TopAppBar( title = { Text(stringResource(R.string.wifi)) }, - navigationIcon = { NavIcon { navCtrl.navigateUp() } } + navigationIcon = { NavIcon(onNavigateUp) } ) } ) { paddingValues -> @@ -295,14 +299,14 @@ fun Wifi(navCtrl: NavHostController) { FunctionItem(R.string.wifi_mac_address) { wifiMacDialog = true } } if(VERSION.SDK_INT >= 33 && (deviceOwner || orgProfileOwner)) { - FunctionItem(R.string.min_wifi_security_level) { navCtrl.navigate("MinWifiSecurityLevel") } - FunctionItem(R.string.wifi_ssid_policy) { navCtrl.navigate("WifiSsidPolicy") } + FunctionItem(R.string.min_wifi_security_level) { onNavigate(WifiSecurityLevel) } + FunctionItem(R.string.wifi_ssid_policy) { onNavigate(WifiSsidPolicyScreen) } } } } else if(page == 1) { - SavedNetworks(navCtrl) + SavedNetworks(onNavigateToUpdateNetwork) } else { - AddNetwork() + AddNetworkScreen(null) {} } } } @@ -334,7 +338,7 @@ fun Wifi(navCtrl: NavHostController) { @Suppress("DEPRECATION") @OptIn(ExperimentalPermissionsApi::class) @Composable -private fun SavedNetworks(navCtrl: NavHostController) { +private fun SavedNetworks(onNavigateToUpdateNetwork: (Bundle) -> Unit) { val context = LocalContext.current val wm = context.getSystemService(Context.WIFI_SERVICE) as WifiManager val configuredNetworks = remember { mutableStateListOf() } @@ -431,9 +435,7 @@ private fun SavedNetworks(navCtrl: NavHostController) { Button( onClick = { networkDetailsDialog = -1 - val dest = navCtrl.graph.findNode("UpdateNetwork") - if(dest != null) - navCtrl.navigate(dest.id, bundleOf("wifi_configuration" to network)) + onNavigateToUpdateNetwork(bundleOf("wifi_configuration" to network)) }, modifier = Modifier.fillMaxWidth() ) { @@ -461,17 +463,20 @@ private fun SavedNetworks(navCtrl: NavHostController) { ) } +@Serializable +object AddNetwork + @Composable -fun UpdateNetwork(arguments: Bundle, navCtrl: NavHostController) { - MyScaffold(R.string.update_network, 0.dp, navCtrl, false) { - AddNetwork(arguments.getParcelable("wifi_configuration"), navCtrl) +fun AddNetworkScreen(data: Bundle, onNavigateUp: () -> Unit) { + MyScaffold(R.string.update_network, 0.dp, onNavigateUp, false) { + AddNetworkScreen(data.getParcelable("wifi_configuration"), onNavigateUp) } } @Suppress("DEPRECATION") @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostController? = null) { +private fun AddNetworkScreen(wifiConfig: WifiConfiguration? = null, onNavigateUp: () -> Unit) { val context = LocalContext.current var resultDialog by remember { mutableStateOf(false) } var createdNetworkId by remember { mutableIntStateOf(-1) } @@ -721,7 +726,7 @@ private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostCo TextButton( onClick = { resultDialog = false - if(createdNetworkId != -1) navCtrl?.navigateUp() + if(createdNetworkId != -1) onNavigateUp() } ) { Text(stringResource(R.string.confirm)) @@ -732,14 +737,16 @@ private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostCo } } +@Serializable object WifiSecurityLevel + @RequiresApi(33) @Composable -fun WifiSecurityLevel(navCtrl: NavHostController) { +fun WifiSecurityLevelScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() var selectedWifiSecLevel by remember { mutableIntStateOf(0) } LaunchedEffect(Unit) { selectedWifiSecLevel = dpm.minimumRequiredWifiSecurityLevel } - MyScaffold(R.string.min_wifi_security_level, 8.dp, navCtrl) { + MyScaffold(R.string.min_wifi_security_level, 8.dp, onNavigateUp) { RadioButtonItem(R.string.wifi_security_open, selectedWifiSecLevel == WIFI_SECURITY_OPEN) { selectedWifiSecLevel = WIFI_SECURITY_OPEN } RadioButtonItem("WEP, WPA(2)-PSK", selectedWifiSecLevel == WIFI_SECURITY_PERSONAL) { selectedWifiSecLevel = WIFI_SECURITY_PERSONAL } RadioButtonItem("WPA-EAP", selectedWifiSecLevel == WIFI_SECURITY_ENTERPRISE_EAP) { selectedWifiSecLevel = WIFI_SECURITY_ENTERPRISE_EAP } @@ -758,13 +765,15 @@ fun WifiSecurityLevel(navCtrl: NavHostController) { } } +@Serializable object WifiSsidPolicyScreen + @RequiresApi(33) @Composable -fun WifiSsidPolicy(navCtrl: NavHostController) { +fun WifiSsidPolicyScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val focusMgr = LocalFocusManager.current - MyScaffold(R.string.wifi_ssid_policy, 8.dp, navCtrl) { + MyScaffold(R.string.wifi_ssid_policy, 8.dp, onNavigateUp) { var selectedPolicyType by remember { mutableIntStateOf(-1) } val ssidList = remember { mutableStateListOf() } val refreshPolicy = { @@ -862,10 +871,12 @@ fun NetworkStats.toBucketList(): List { return list } +@Serializable object QueryNetworkStats + @OptIn(ExperimentalMaterial3Api::class) @RequiresApi(23) @Composable -fun NetworkStats(navCtrl: NavHostController, vm: MyViewModel) { +fun NetworkStatsScreen(onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkStatsViewer) -> Unit) { val context = LocalContext.current val deviceOwner = context.isDeviceOwner val nsm = context.getSystemService(NetworkStatsManager::class.java) @@ -884,7 +895,7 @@ fun NetworkStats(navCtrl: NavHostController, vm: MyViewModel) { val endTimeTextFieldInteractionSource = remember { MutableInteractionSource() } if(startTimeTextFieldInteractionSource.collectIsPressedAsState().value) activeTextField = NetworkStatsActiveTextField.StartTime if(endTimeTextFieldInteractionSource.collectIsPressedAsState().value) activeTextField = NetworkStatsActiveTextField.EndTime - MyScaffold(R.string.network_stats, 8.dp, navCtrl) { + MyScaffold(R.string.network_stats, 8.dp, onNavigateUp) { ExposedDropdownMenuBox( activeTextField == NetworkStatsActiveTextField.Type, { activeTextField = if(it) NetworkStatsActiveTextField.Type else NetworkStatsActiveTextField.Type } @@ -1028,12 +1039,12 @@ fun NetworkStats(navCtrl: NavHostController, vm: MyViewModel) { var uidText by rememberSaveable { mutableStateOf(context.getString(NetworkStatsUID.All.strRes)) } var readOnly by rememberSaveable { mutableStateOf(true) } if(!readOnly && uidText.toIntOrNull() != null) uid = uidText.toInt() - if(VERSION.SDK_INT >= 24) { - val selectedPackage by vm.selectedPackage.collectAsStateWithLifecycle() - if(readOnly && selectedPackage != "") { + val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { + it ?: return@rememberLauncherForActivityResult + if(VERSION.SDK_INT >= 24 && readOnly) { try { - uid = context.packageManager.getPackageUid(selectedPackage, 0) - uidText = "$selectedPackage ($uid)" + uid = context.packageManager.getPackageUid(it, 0) + uidText = "$it ($uid)" } catch(_: NameNotFoundException) { context.showOperationResultToast(false) } @@ -1066,8 +1077,8 @@ fun NetworkStats(navCtrl: NavHostController, vm: MyViewModel) { text = { Text(stringResource(R.string.choose_an_app)) }, onClick = { readOnly = true - navCtrl.navigate("PackageSelector") activeTextField = NetworkStatsActiveTextField.None + choosePackage.launch(null) } ) DropdownMenuItem( @@ -1186,28 +1197,18 @@ fun NetworkStats(navCtrl: NavHostController, vm: MyViewModel) { } else { val bundle = Bundle() bundle.putInt("size", buckets.size) - buckets.forEachIndexed { index, bucket -> - val subBundle = bundleOf( - "rx_bytes" to bucket.rxBytes, - "rx_packets" to bucket.rxPackets, - "tx_bytes" to bucket.txBytes, - "tx_packets" to bucket.txPackets, - "uid" to bucket.uid, - "state" to bucket.state, - "start_time" to bucket.startTimeStamp, - "end_time" to bucket.endTimeStamp + val stats = buckets.map { + NetworkStatsViewer.Data( + it.rxBytes, it.rxPackets, it.txBytes, it.txPackets, + it.uid, it.state, it.startTimeStamp, it.endTimeStamp, + if(VERSION.SDK_INT >= 24) it.tag else null, + if(VERSION.SDK_INT >= 24) it.roaming else null, + if(VERSION.SDK_INT >= 26) it.metered else null ) - if(VERSION.SDK_INT >= 24) { - subBundle.putInt("tag", bucket.tag) - subBundle.putInt("roaming", bucket.roaming) - } - if(VERSION.SDK_INT >= 26) subBundle.putInt("metered", bucket.metered) - bundle.putBundle(index.toString(), subBundle) } withContext(Dispatchers.Main) { querying = false - val nodeId = navCtrl.graph.findNode("NetworkStatsViewer")?.id - if(nodeId != null) navCtrl.navigate(nodeId, bundle) + onNavigateToViewer(NetworkStatsViewer(stats)) } } } @@ -1245,12 +1246,32 @@ fun NetworkStats(navCtrl: NavHostController, vm: MyViewModel) { } } +@Serializable +data class NetworkStatsViewer( + val stats: List +) { + @Serializable + data class Data( + val rxBytes: Long, + val rxPackets: Long, + val txBytes: Long, + val txPackets: Long, + val uid: Int, + val state: Int, + val startTime: Long, + val endTime: Long, + val tag: Int?, + val roaming: Int?, + val metered: Int? + ) +} + @RequiresApi(23) @Composable -fun NetworkStatsViewer(navCtrl: NavHostController, navArgs: Bundle) { +fun NetworkStatsViewerScreen(nsv: NetworkStatsViewer, onNavigateUp: () -> Unit) { var index by remember { mutableIntStateOf(0) } - val size = navArgs.getInt("size", 1) - MyScaffold(R.string.place_holder, 8.dp, navCtrl, false) { + val size = nsv.stats.size + MyScaffold(R.string.place_holder, 8.dp, onNavigateUp, false) { if(size > 1) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp) @@ -1269,70 +1290,75 @@ fun NetworkStatsViewer(navCtrl: NavHostController, navArgs: Bundle) { Icon(imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight, contentDescription = null) } } - val data = navArgs.getBundle(index.toString())!! + val data = nsv.stats[index] Text( - data.getLong("start_time").humanReadableDate + " ~ " + data.getLong("end_time").humanReadableDate, + data.startTime.humanReadableDate + " ~ " + data.endTime.humanReadableDate, modifier = Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp) ) - val txBytes = data.getLong("tx_bytes") + val txBytes = data.txBytes Text(stringResource(R.string.transmitted), style = typography.titleLarge) Column(modifier = Modifier.padding(start = 8.dp, bottom = 4.dp)) { Text("$txBytes bytes") Text(formatFileSize(txBytes)) - Text(data.getLong("tx_packets").toString() + " packets") + Text(data.txPackets.toString() + " packets") } - val rxBytes = data.getLong("rx_bytes") + val rxBytes = data.rxBytes Text(stringResource(R.string.received), style = typography.titleLarge) Column(modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)) { Text("$rxBytes bytes") Text(formatFileSize(rxBytes)) - Text(data.getLong("rx_packets").toString() + " packets") + Text(data.rxPackets.toString() + " packets") } Row(verticalAlignment = Alignment.CenterVertically) { - val textMap = mapOf( - NetworkStats.Bucket.STATE_ALL to R.string.all, - NetworkStats.Bucket.STATE_DEFAULT to R.string.default_str, - NetworkStats.Bucket.STATE_FOREGROUND to R.string.foreground - ) + val text = when(data.state) { + NetworkStats.Bucket.STATE_ALL -> R.string.all + NetworkStats.Bucket.STATE_DEFAULT -> R.string.default_str + NetworkStats.Bucket.STATE_FOREGROUND -> R.string.foreground + else -> R.string.unknown + } Text(stringResource(R.string.state), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp)) - Text(stringResource(textMap[data.getInt("state")] ?: R.string.unknown)) + Text(stringResource(text)) } if(VERSION.SDK_INT >= 24) { Row(verticalAlignment = Alignment.CenterVertically) { - val tag = data.getInt("tag") + val tag = data.tag Text(stringResource(R.string.tag), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp)) Text(if(tag == NetworkStats.Bucket.TAG_NONE) stringResource(R.string.all) else tag.toString()) } Row(verticalAlignment = Alignment.CenterVertically) { - val textMap = mapOf( - NetworkStats.Bucket.ROAMING_ALL to R.string.all, - NetworkStats.Bucket.ROAMING_YES to R.string.yes, - NetworkStats.Bucket.ROAMING_NO to R.string.no - ) + val text = when(data.roaming) { + NetworkStats.Bucket.ROAMING_ALL -> R.string.all + NetworkStats.Bucket.ROAMING_YES -> R.string.yes + NetworkStats.Bucket.ROAMING_NO -> R.string.no + else -> R.string.unknown + } Text(stringResource(R.string.roaming), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp)) - Text(stringResource(textMap[data.getInt("roaming")] ?: R.string.unknown)) + Text(stringResource(text)) } } if(VERSION.SDK_INT >= 26) Row(verticalAlignment = Alignment.CenterVertically) { - val textMap = mapOf( - NetworkStats.Bucket.METERED_ALL to R.string.all, - NetworkStats.Bucket.METERED_YES to R.string.yes, - NetworkStats.Bucket.METERED_NO to R.string.no - ) + val text = when(data.metered) { + NetworkStats.Bucket.METERED_ALL -> R.string.all + NetworkStats.Bucket.METERED_YES -> R.string.yes + NetworkStats.Bucket.METERED_NO -> R.string.no + else -> R.string.unknown + } Text(stringResource(R.string.metered), style = typography.titleMedium, modifier = Modifier.padding(end = 8.dp)) - Text(stringResource(textMap[data.getInt("metered")] ?: R.string.unknown)) + Text(stringResource(text)) } } } +@Serializable object PrivateDns + @RequiresApi(29) @Composable -fun PrivateDNS(navCtrl: NavHostController) { +fun PrivateDnsScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - MyScaffold(R.string.private_dns, 8.dp, navCtrl) { + MyScaffold(R.string.private_dns, 8.dp, onNavigateUp) { val dnsStatus = mapOf( PRIVATE_DNS_MODE_UNKNOWN to stringResource(R.string.unknown), PRIVATE_DNS_MODE_OFF to stringResource(R.string.disabled), @@ -1397,9 +1423,11 @@ fun PrivateDNS(navCtrl: NavHostController) { } } +@Serializable object AlwaysOnVpnPackage + @RequiresApi(24) @Composable -fun AlwaysOnVPNPackage(navCtrl: NavHostController, vm: MyViewModel) { +fun AlwaysOnVpnPackageScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -1408,12 +1436,8 @@ fun AlwaysOnVPNPackage(navCtrl: NavHostController, vm: MyViewModel) { val focusMgr = LocalFocusManager.current val refresh = { pkgName = dpm.getAlwaysOnVpnPackage(receiver) ?: "" } LaunchedEffect(Unit) { refresh() } - val updatePackage by vm.selectedPackage.collectAsState() - LaunchedEffect(updatePackage) { - if(updatePackage != "") { - pkgName = updatePackage - vm.selectedPackage.value = "" - } + val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { result -> + result?.let { pkgName = it } } val setAlwaysOnVpn: (String?, Boolean)->Boolean = { vpnPkg: String?, lockdownEnabled: Boolean -> try { @@ -1430,7 +1454,7 @@ fun AlwaysOnVPNPackage(navCtrl: NavHostController, vm: MyViewModel) { false } } - MyScaffold(R.string.always_on_vpn, 8.dp, navCtrl) { + MyScaffold(R.string.always_on_vpn, 8.dp, onNavigateUp) { OutlinedTextField( value = pkgName, onValueChange = { pkgName = it }, @@ -1441,10 +1465,7 @@ fun AlwaysOnVPNPackage(navCtrl: NavHostController, vm: MyViewModel) { Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, modifier = Modifier .clip(RoundedCornerShape(50)) - .clickable(onClick = { - focusMgr.clearFocus() - navCtrl.navigate("PackageSelector") - }) + .clickable { choosePackage.launch(null) } .padding(3.dp)) }, modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) @@ -1468,8 +1489,10 @@ fun AlwaysOnVPNPackage(navCtrl: NavHostController, vm: MyViewModel) { } } +@Serializable object RecommendedGlobalProxy + @Composable -fun RecommendedGlobalProxy(navCtrl: NavHostController) { +fun RecommendedGlobalProxyScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -1479,7 +1502,7 @@ fun RecommendedGlobalProxy(navCtrl: NavHostController) { var specifyPort by remember { mutableStateOf(false) } var proxyPort by remember { mutableStateOf("") } var exclList by remember { mutableStateOf("") } - MyScaffold(R.string.recommended_global_proxy, 8.dp, navCtrl) { + MyScaffold(R.string.recommended_global_proxy, 8.dp, onNavigateUp) { RadioButtonItem(R.string.proxy_type_off, proxyType == 0) { proxyType = 0 } RadioButtonItem(R.string.proxy_type_pac, proxyType == 1) { proxyType = 1 } RadioButtonItem(R.string.proxy_type_direct, proxyType == 2) { proxyType = 2 } @@ -1564,9 +1587,11 @@ fun RecommendedGlobalProxy(navCtrl: NavHostController) { } } +@Serializable object NetworkLogging + @RequiresApi(26) @Composable -fun NetworkLogging(navCtrl: NavHostController) { +fun NetworkLoggingScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -1583,7 +1608,7 @@ fun NetworkLogging(navCtrl: NavHostController) { } } } - MyScaffold(R.string.network_logging, 8.dp, navCtrl) { + MyScaffold(R.string.network_logging, 8.dp, onNavigateUp) { SwitchItem( R.string.enable, getState = { dpm.isNetworkLoggingEnabled(receiver) }, @@ -1620,14 +1645,16 @@ fun NetworkLogging(navCtrl: NavHostController) { } } +@Serializable object WifiAuthKeypair + @RequiresApi(31) @Composable -fun WifiAuthKeypair(navCtrl: NavHostController) { +fun WifiAuthKeypairScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val focusMgr = LocalFocusManager.current var keyPair by remember { mutableStateOf("") } - MyScaffold(R.string.wifi_auth_keypair, 8.dp, navCtrl) { + MyScaffold(R.string.wifi_auth_keypair, 8.dp, onNavigateUp) { OutlinedTextField( value = keyPair, label = { Text(stringResource(R.string.alias)) }, @@ -1662,9 +1689,11 @@ fun WifiAuthKeypair(navCtrl: NavHostController) { } } +@Serializable object PreferentialNetworkService + @RequiresApi(33) @Composable -fun PreferentialNetworkService(navCtrl: NavHostController) { +fun PreferentialNetworkServiceScreen(onNavigateUp: () -> Unit) { val focusMgr = LocalFocusManager.current val context = LocalContext.current val dpm = context.getDPM() @@ -1703,7 +1732,7 @@ fun PreferentialNetworkService(navCtrl: NavHostController) { refresh() } LaunchedEffect(Unit) { initialize() } - MyScaffold(R.string.preferential_network_service, 8.dp, navCtrl) { + MyScaffold(R.string.preferential_network_service, 8.dp, onNavigateUp) { SwitchItem(R.string.enabled, state = masterEnabled, onCheckedChange = { masterEnabled = it }, padding = false) Row( horizontalArrangement = Arrangement.SpaceAround, @@ -1814,9 +1843,11 @@ fun PreferentialNetworkService(navCtrl: NavHostController) { } } +@Serializable object OverrideApn + @RequiresApi(28) @Composable -fun OverrideAPN(navCtrl: NavHostController) { +fun OverrideApnScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -1825,7 +1856,7 @@ fun OverrideAPN(navCtrl: NavHostController) { var inputNum by remember { mutableStateOf("0") } var nextStep by remember { mutableStateOf(false) } val builder = Builder() - MyScaffold(R.string.override_apn_settings, 8.dp, navCtrl) { + MyScaffold(R.string.override_apn_settings, 8.dp, onNavigateUp) { Text(text = stringResource(id = R.string.developing)) Spacer(Modifier.padding(vertical = 5.dp)) SwitchItem( diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt index 7dc106f..102bf9f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt @@ -78,30 +78,33 @@ import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.RadioButtonItem import com.bintianqi.owndroid.yesOrNo +import kotlinx.serialization.Serializable + +@Serializable object Password @SuppressLint("NewApi") @Composable -fun Password(navCtrl: NavHostController) { +fun PasswordScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { val context = LocalContext.current val deviceAdmin = context.isDeviceAdmin val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner var dialog by remember { mutableIntStateOf(0) } - MyScaffold(R.string.password_and_keyguard, 0.dp, navCtrl) { - FunctionItem(R.string.password_info, icon = R.drawable.info_fill0) { navCtrl.navigate("PasswordInfo") } + MyScaffold(R.string.password_and_keyguard, 0.dp, onNavigateUp) { + FunctionItem(R.string.password_info, icon = R.drawable.info_fill0) { onNavigate(PasswordInfo) } if(SharedPrefs(context).displayDangerousFeatures) { if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.reset_password_token, icon = R.drawable.key_vertical_fill0) { navCtrl.navigate("ResetPasswordToken") } + FunctionItem(R.string.reset_password_token, icon = R.drawable.key_vertical_fill0) { onNavigate(ResetPasswordToken) } } if(deviceAdmin || deviceOwner || profileOwner) { - FunctionItem(R.string.reset_password, icon = R.drawable.lock_reset_fill0) { navCtrl.navigate("ResetPassword") } + FunctionItem(R.string.reset_password, icon = R.drawable.lock_reset_fill0) { onNavigate(ResetPassword) } } } if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.required_password_complexity, icon = R.drawable.password_fill0) { navCtrl.navigate("RequirePasswordComplexity") } + FunctionItem(R.string.required_password_complexity, icon = R.drawable.password_fill0) { onNavigate(RequiredPasswordComplexity) } } if(deviceAdmin) { - FunctionItem(R.string.disable_keyguard_features, icon = R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("DisableKeyguardFeatures") } + FunctionItem(R.string.disable_keyguard_features, icon = R.drawable.screen_lock_portrait_fill0) { onNavigate(KeyguardDisabledFeatures) } } if(deviceOwner) { FunctionItem(R.string.max_time_to_lock, icon = R.drawable.schedule_fill0) { dialog = 1 } @@ -115,7 +118,7 @@ fun Password(navCtrl: NavHostController) { FunctionItem(R.string.pwd_history, icon = R.drawable.history_fill0) { dialog = 5 } } if(VERSION.SDK_INT < 31 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.required_password_quality, icon = R.drawable.password_fill0) { navCtrl.navigate("RequirePasswordQuality") } + FunctionItem(R.string.required_password_quality, icon = R.drawable.password_fill0) { onNavigate(RequiredPasswordQuality) } } } if(dialog != 0) { @@ -207,14 +210,16 @@ fun Password(navCtrl: NavHostController) { } } +@Serializable object PasswordInfo + @Composable -fun PasswordInfo(navCtrl: NavHostController) { +fun PasswordInfoScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner - MyScaffold(R.string.password_info, 8.dp, navCtrl) { + MyScaffold(R.string.password_info, 8.dp, onNavigateUp) { if(VERSION.SDK_INT >= 29) { val passwordComplexity = mapOf( PASSWORD_COMPLEXITY_NONE to R.string.password_complexity_none, @@ -233,16 +238,18 @@ fun PasswordInfo(navCtrl: NavHostController) { } } +@Serializable object ResetPasswordToken + @RequiresApi(26) @Composable -fun ResetPasswordToken(navCtrl: NavHostController) { +fun ResetPasswordTokenScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() var token by remember { mutableStateOf("") } val tokenByteArray = token.toByteArray() val focusMgr = LocalFocusManager.current - MyScaffold(R.string.reset_password_token, 8.dp, navCtrl) { + MyScaffold(R.string.reset_password_token, 8.dp, onNavigateUp) { OutlinedTextField( value = token, onValueChange = { token = it }, label = { Text(stringResource(R.string.token)) }, @@ -295,8 +302,10 @@ fun ResetPasswordToken(navCtrl: NavHostController) { } } +@Serializable object ResetPassword + @Composable -fun ResetPassword(navCtrl: NavHostController) { +fun ResetPasswordScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -307,7 +316,7 @@ fun ResetPassword(navCtrl: NavHostController) { val tokenByteArray = token.toByteArray() var flag by remember { mutableIntStateOf(0) } var confirmDialog by remember { mutableStateOf(false) } - MyScaffold(R.string.reset_password, 8.dp, navCtrl) { + MyScaffold(R.string.reset_password, 8.dp, onNavigateUp) { if(VERSION.SDK_INT >= 26) { OutlinedTextField( value = token, onValueChange = { token = it }, @@ -413,9 +422,11 @@ fun ResetPassword(navCtrl: NavHostController) { } } +@Serializable object RequiredPasswordComplexity + @RequiresApi(31) @Composable -fun PasswordComplexity(navCtrl: NavHostController) { +fun RequiredPasswordComplexityScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val passwordComplexity = mapOf( @@ -426,7 +437,7 @@ fun PasswordComplexity(navCtrl: NavHostController) { ) var selectedItem by remember { mutableIntStateOf(PASSWORD_COMPLEXITY_NONE) } LaunchedEffect(Unit) { selectedItem = dpm.requiredPasswordComplexity } - MyScaffold(R.string.required_password_complexity, 8.dp, navCtrl) { + MyScaffold(R.string.required_password_complexity, 8.dp, onNavigateUp) { passwordComplexity.forEach { RadioButtonItem(it.value, selectedItem == it.key) { selectedItem = it.key } } @@ -450,8 +461,10 @@ fun PasswordComplexity(navCtrl: NavHostController) { } } +@Serializable object KeyguardDisabledFeatures + @Composable -fun DisableKeyguardFeatures(navCtrl: NavHostController) { +fun KeyguardDisabledFeaturesScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -481,7 +494,7 @@ fun DisableKeyguardFeatures(navCtrl: NavHostController) { } LaunchedEffect(mode) { if(mode != 2) flag = dpm.getKeyguardDisabledFeatures(receiver) } LaunchedEffect(Unit) { refresh() } - MyScaffold(R.string.disable_keyguard_features, 8.dp, navCtrl) { + MyScaffold(R.string.disable_keyguard_features, 8.dp, onNavigateUp) { RadioButtonItem(R.string.enable_all, mode == 0) { mode = 0 } RadioButtonItem(R.string.disable_all, mode == 1) { mode = 1 } RadioButtonItem(R.string.custom, mode == 2) { mode = 2 } @@ -509,8 +522,10 @@ fun DisableKeyguardFeatures(navCtrl: NavHostController) { } } +@Serializable object RequiredPasswordQuality + @Composable -fun PasswordQuality(navCtrl: NavHostController) { +fun RequiredPasswordQualityScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -525,7 +540,7 @@ fun PasswordQuality(navCtrl: NavHostController) { ) var selectedItem by remember { mutableIntStateOf(PASSWORD_QUALITY_UNSPECIFIED) } LaunchedEffect(Unit) { selectedItem=dpm.getPasswordQuality(receiver) } - MyScaffold(R.string.required_password_quality, 8.dp, navCtrl) { + MyScaffold(R.string.required_password_quality, 8.dp, onNavigateUp) { passwordQuality.forEach { RadioButtonItem(it.value, selectedItem == it.key) { selectedItem = it.key } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt index d7142f8..8b41727 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -9,10 +9,12 @@ import android.content.Intent import android.content.pm.PackageManager import android.os.Binder import android.os.Build.VERSION +import android.os.Bundle import android.os.IBinder import android.os.RemoteException import android.os.UserManager import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility @@ -41,10 +43,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.core.os.bundleOf -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController -import androidx.navigation.NavOptions -import com.bintianqi.owndroid.MyViewModel +import com.bintianqi.owndroid.ChoosePackageContract import com.bintianqi.owndroid.R import com.bintianqi.owndroid.SharedPrefs import com.bintianqi.owndroid.backToHomeStateFlow @@ -55,12 +54,15 @@ import com.bintianqi.owndroid.yesOrNo import com.rosan.dhizuku.api.Dhizuku import com.rosan.dhizuku.api.DhizukuRequestPermissionListener import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable import rikka.shizuku.Shizuku import rikka.sui.Sui +@Serializable object Permissions + @SuppressLint("NewApi") @Composable -fun Permissions(navCtrl: NavHostController) { +fun PermissionsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit, onNavigateToShizuku: (Bundle) -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -71,7 +73,7 @@ fun Permissions(navCtrl: NavHostController) { var dialog by remember { mutableIntStateOf(0) } var bindingShizuku by remember { mutableStateOf(false) } val enrollmentSpecificId = if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) dpm.enrollmentSpecificId else "" - MyScaffold(R.string.permissions, 0.dp, navCtrl) { + MyScaffold(R.string.permissions, 0.dp, onNavigateUp) { if(!dpm.isDeviceOwnerApp(context.packageName)) { SwitchItem( R.string.dhizuku, @@ -82,18 +84,18 @@ fun Permissions(navCtrl: NavHostController) { } FunctionItem( R.string.device_admin, stringResource(if(deviceAdmin) R.string.activated else R.string.deactivated), - operation = { navCtrl.navigate("DeviceAdmin") } + operation = { onNavigate(DeviceAdmin) } ) if(profileOwner || !userManager.isSystemUser) { FunctionItem( R.string.profile_owner, stringResource(if(profileOwner) R.string.activated else R.string.deactivated), - operation = { navCtrl.navigate("ProfileOwner") } + operation = { onNavigate(ProfileOwner) } ) } if(!profileOwner && userManager.isSystemUser) { FunctionItem( R.string.device_owner, stringResource(if(deviceOwner) R.string.activated else R.string.deactivated), - operation = { navCtrl.navigate("DeviceOwner") } + operation = { onNavigate(DeviceOwner) } ) } FunctionItem(R.string.shizuku) { @@ -101,9 +103,8 @@ fun Permissions(navCtrl: NavHostController) { if(Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { bindingShizuku = true fun onBind(binder: IBinder) { - val destinationId = navCtrl.graph.findNode("Shizuku")!!.id bindingShizuku = false - navCtrl.navigate(destinationId, bundleOf("binder" to binder), NavOptions.Builder().setLaunchSingleTop(true).build()) + onNavigateToShizuku(bundleOf("binder" to binder)) } try { controlShizukuService(context, ::onBind, { bindingShizuku = false }, true) @@ -129,8 +130,8 @@ fun Permissions(navCtrl: NavHostController) { } } if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) - FunctionItem(R.string.delegated_admins) { navCtrl.navigate("DelegatedAdmins") } - FunctionItem(R.string.device_info, icon = R.drawable.perm_device_information_fill0) { navCtrl.navigate("DeviceInfo") } + FunctionItem(R.string.delegated_admins) { onNavigate(DelegatedAdmins) } + FunctionItem(R.string.device_info, icon = R.drawable.perm_device_information_fill0) { onNavigate(DeviceInfo) } if(VERSION.SDK_INT >= 24 && (profileOwner || (VERSION.SDK_INT >= 26 && deviceOwner))) { FunctionItem(R.string.org_name, icon = R.drawable.corporate_fare_fill0) { dialog = 2 } } @@ -141,13 +142,13 @@ fun Permissions(navCtrl: NavHostController) { FunctionItem(R.string.enrollment_specific_id, icon = R.drawable.id_card_fill0) { dialog = 1 } } if(VERSION.SDK_INT >= 24 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.lock_screen_info, icon = R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("LockScreenInfo") } + FunctionItem(R.string.lock_screen_info, icon = R.drawable.screen_lock_portrait_fill0) { onNavigate(LockScreenInfo) } } if(VERSION.SDK_INT >= 24 && deviceAdmin) { - FunctionItem(R.string.support_messages, icon = R.drawable.chat_fill0) { navCtrl.navigate("SupportMessages") } + FunctionItem(R.string.support_messages, icon = R.drawable.chat_fill0) { onNavigate(SupportMessage) } } if(VERSION.SDK_INT >= 28 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.transfer_ownership, icon = R.drawable.admin_panel_settings_fill0) { navCtrl.navigate("TransferOwnership") } + FunctionItem(R.string.transfer_ownership, icon = R.drawable.admin_panel_settings_fill0) { onNavigate(TransferOwnership) } } } if(bindingShizuku) { @@ -265,15 +266,17 @@ private fun toggleDhizukuMode(status: Boolean, context: Context) { } } +@Serializable object LockScreenInfo + @RequiresApi(24) @Composable -fun LockScreenInfo(navCtrl: NavHostController) { +fun LockScreenInfoScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current var infoText by remember { mutableStateOf(dpm.deviceOwnerLockScreenInfo?.toString() ?: "") } - MyScaffold(R.string.lock_screen_info, 8.dp, navCtrl) { + MyScaffold(R.string.lock_screen_info, 8.dp, onNavigateUp) { OutlinedTextField( value = infoText, label = { Text(stringResource(R.string.lock_screen_info)) }, @@ -307,14 +310,16 @@ fun LockScreenInfo(navCtrl: NavHostController) { } } +@Serializable object DeviceAdmin + @Composable -fun DeviceAdmin(navCtrl: NavHostController) { +fun DeviceAdminScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() var deactivateDialog by remember { mutableStateOf(false) } val deviceAdmin = context.isDeviceAdmin - MyScaffold(R.string.device_admin, 8.dp, navCtrl) { + MyScaffold(R.string.device_admin, 8.dp, onNavigateUp) { Text(text = stringResource(if(context.isDeviceAdmin) R.string.activated else R.string.deactivated), style = typography.titleLarge) Spacer(Modifier.padding(vertical = 5.dp)) AnimatedVisibility(deviceAdmin) { @@ -364,14 +369,16 @@ fun DeviceAdmin(navCtrl: NavHostController) { } } +@Serializable object ProfileOwner + @Composable -fun ProfileOwner(navCtrl: NavHostController) { +fun ProfileOwnerScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() var deactivateDialog by remember { mutableStateOf(false) } val profileOwner = context.isProfileOwner - MyScaffold(R.string.profile_owner, 8.dp, navCtrl) { + MyScaffold(R.string.profile_owner, 8.dp, onNavigateUp) { Text(stringResource(if(profileOwner) R.string.activated else R.string.deactivated), style = typography.titleLarge) Spacer(Modifier.padding(vertical = 5.dp)) if(VERSION.SDK_INT >= 24 && profileOwner) { @@ -417,13 +424,15 @@ fun ProfileOwner(navCtrl: NavHostController) { } } +@Serializable object DeviceOwner + @Composable -fun DeviceOwner(navCtrl: NavHostController) { +fun DeviceOwnerScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() var deactivateDialog by remember { mutableStateOf(false) } val deviceOwner = context.isDeviceOwner - MyScaffold(R.string.device_owner, 8.dp, navCtrl) { + MyScaffold(R.string.device_owner, 8.dp, onNavigateUp) { Text(text = stringResource(if(deviceOwner) R.string.activated else R.string.deactivated), style = typography.titleLarge) Spacer(Modifier.padding(vertical = 5.dp)) AnimatedVisibility(deviceOwner) { @@ -502,9 +511,11 @@ private enum class DelegatedScope(val id: String, @StringRes val string: Int, va SecurityLogging(DevicePolicyManager.DELEGATION_SECURITY_LOGGING, R.string.security_logging, 31) } +@Serializable object DelegatedAdmins + @RequiresApi(26) @Composable -fun DelegatedAdmins(navCtrl: NavHostController, vm: MyViewModel) { +fun DelegatedAdminsScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -529,7 +540,7 @@ fun DelegatedAdmins(navCtrl: NavHostController, vm: MyViewModel) { packages.putAll(list) } LaunchedEffect(Unit) { refresh() } - MyScaffold(R.string.delegated_admins, 0.dp, navCtrl) { + MyScaffold(R.string.delegated_admins, 0.dp, onNavigateUp) { packages.forEach { (pkg, scopes) -> Column( modifier = Modifier @@ -558,12 +569,8 @@ fun DelegatedAdmins(navCtrl: NavHostController, vm: MyViewModel) { Text(stringResource(R.string.add_delegated_admin), style = typography.titleLarge) } if(dialog != 0) { - val selectedPackage by vm.selectedPackage.collectAsStateWithLifecycle() - LaunchedEffect(selectedPackage) { - if(selectedPackage != "") { - inputPackageName = selectedPackage - vm.selectedPackage.value = "" - } + val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { result -> + result?.let { inputPackageName = it } } AlertDialog( text = { @@ -573,7 +580,7 @@ fun DelegatedAdmins(navCtrl: NavHostController, vm: MyViewModel) { value = inputPackageName, onValueChange = { inputPackageName = it }, label = { Text(stringResource(R.string.package_name)) }, trailingIcon = { - if(dialog == 2) IconButton({ navCtrl.navigate("PackageSelector") }) { + if(dialog == 2) IconButton({ choosePackage.launch(null) }) { Icon(painterResource(R.drawable.list_fill0), null) } }, @@ -614,13 +621,15 @@ fun DelegatedAdmins(navCtrl: NavHostController, vm: MyViewModel) { } } +@Serializable object DeviceInfo + @Composable -fun DeviceInfo(navCtrl: NavHostController) { +fun DeviceInfoScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() var dialog by remember { mutableIntStateOf(0) } - MyScaffold(R.string.device_info, 8.dp, navCtrl) { + MyScaffold(R.string.device_info, 8.dp, onNavigateUp) { if(VERSION.SDK_INT>=34 && (context.isDeviceOwner || dpm.isOrgProfile(receiver))) { CardItem(R.string.financed_device, dpm.isDeviceFinanced.yesOrNo) } @@ -654,9 +663,11 @@ fun DeviceInfo(navCtrl: NavHostController) { ) } +@Serializable object SupportMessage + @RequiresApi(24) @Composable -fun SupportMessages(navCtrl: NavHostController) { +fun SupportMessageScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -667,7 +678,7 @@ fun SupportMessages(navCtrl: NavHostController) { longMsg = dpm.getLongSupportMessage(receiver)?.toString() ?: "" } LaunchedEffect(Unit) { refreshMsg() } - MyScaffold(R.string.support_messages, 8.dp, navCtrl) { + MyScaffold(R.string.support_messages, 8.dp, onNavigateUp) { OutlinedTextField( value = shortMsg, label = { Text(stringResource(R.string.short_support_msg)) }, @@ -732,15 +743,17 @@ fun SupportMessages(navCtrl: NavHostController) { } } +@Serializable object TransferOwnership + @RequiresApi(28) @Composable -fun TransferOwnership(navCtrl: NavHostController) { +fun TransferOwnershipScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val focusMgr = LocalFocusManager.current var input by remember { mutableStateOf("") } val componentName = ComponentName.unflattenFromString(input) var dialog by remember { mutableStateOf(false) } - MyScaffold(R.string.transfer_ownership, 8.dp, navCtrl) { + MyScaffold(R.string.transfer_ownership, 8.dp, onNavigateUp) { OutlinedTextField( value = input, onValueChange = { input = it }, label = { Text(stringResource(R.string.target_component_name)) }, modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt index 59bc771..aa46716 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt @@ -1,6 +1,5 @@ package com.bintianqi.owndroid.dpm -import android.accounts.Account import android.content.ComponentName import android.content.Context import android.content.ServiceConnection @@ -37,17 +36,19 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController import com.bintianqi.owndroid.IUserService import com.bintianqi.owndroid.R import com.bintianqi.owndroid.ui.MyScaffold import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable import rikka.shizuku.Shizuku +@Serializable object ShizukuScreen + @OptIn(ExperimentalMaterial3Api::class) @Composable -fun Shizuku(navCtrl: NavHostController, navArgs: Bundle) { +fun ShizukuScreen(navArgs: Bundle, onNavigateUp: () -> Unit, onNavigateToAccountsViewer: (Accounts) -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -65,7 +66,7 @@ fun Shizuku(navCtrl: NavHostController, navArgs: Bundle) { null } } - MyScaffold(R.string.shizuku, 0.dp, navCtrl, false) { + MyScaffold(R.string.shizuku, 0.dp, onNavigateUp, false) { Button( onClick = { @@ -93,9 +94,10 @@ fun Shizuku(navCtrl: NavHostController, navArgs: Bundle) { onClick = { Log.d("Shizuku", "List accounts") try { - val accounts = service!!.listAccounts() - val dest = navCtrl.graph.findNode("AccountsViewer")!!.id - navCtrl.navigate(dest, Bundle().apply { putParcelableArray("accounts", accounts) }) + val accounts = service!!.listAccounts().map { + Accounts.Account(it.type, it.name) + } + onNavigateToAccountsViewer(Accounts(accounts)) } catch(_: Exception) { outputText = service!!.execute("dumpsys account") coScope.launch{ @@ -174,11 +176,17 @@ fun controlShizukuService( else Shizuku.unbindUserService(userServiceArgs, userServiceConnection, true) } +@Serializable +data class Accounts( + val list: List +) { + @Serializable data class Account(val type: String, val name: String) +} + @Composable -fun AccountsViewer(navCtrl: NavHostController, navArgs: Bundle) { - val accounts = navArgs.getParcelableArray("accounts") as Array - MyScaffold(R.string.accounts, 8.dp, navCtrl, false) { - accounts.forEach { +fun AccountsScreen(accounts: Accounts, onNavigateUp: () -> Unit) { + MyScaffold(R.string.accounts, 8.dp, onNavigateUp, false) { + accounts.list.forEach { Column( modifier = Modifier .fillMaxWidth().padding(vertical = 4.dp) diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 130780c..c3e9c4f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -119,9 +119,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavHostController -import com.bintianqi.owndroid.MyViewModel +import com.bintianqi.owndroid.ChoosePackageContract import com.bintianqi.owndroid.NotificationUtils import com.bintianqi.owndroid.R import com.bintianqi.owndroid.SharedPrefs @@ -139,6 +137,7 @@ import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.uriToStream import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable import java.io.ByteArrayOutputStream import java.util.Date import java.util.TimeZone @@ -146,8 +145,10 @@ import java.util.concurrent.Executors import kotlin.collections.addAll import kotlin.math.roundToLong +@Serializable object SystemManager + @Composable -fun SystemManage(navCtrl: NavHostController) { +fun SystemManagerScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -156,13 +157,13 @@ fun SystemManage(navCtrl: NavHostController) { val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner var dialog by remember { mutableIntStateOf(0) } - MyScaffold(R.string.system, 0.dp, navCtrl) { + MyScaffold(R.string.system, 0.dp, onNavigateUp) { if(deviceOwner || profileOwner) { - FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { navCtrl.navigate("SystemOptions") } + FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { onNavigate(SystemOptions) } } - FunctionItem(R.string.keyguard, icon = R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("Keyguard") } + FunctionItem(R.string.keyguard, icon = R.drawable.screen_lock_portrait_fill0) { onNavigate(Keyguard) } if(VERSION.SDK_INT >= 24 && deviceOwner && !dhizuku) - FunctionItem(R.string.hardware_monitor, icon = R.drawable.memory_fill0) { navCtrl.navigate("HardwareMonitor") } + FunctionItem(R.string.hardware_monitor, icon = R.drawable.memory_fill0) { onNavigate(HardwareMonitor) } if(VERSION.SDK_INT >= 24 && deviceOwner) { FunctionItem(R.string.reboot, icon = R.drawable.restart_alt_fill0) { dialog = 1 } } @@ -170,45 +171,45 @@ fun SystemManage(navCtrl: NavHostController) { FunctionItem(R.string.bug_report, icon = R.drawable.bug_report_fill0) { dialog = 2 } } if(VERSION.SDK_INT >= 28 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.change_time, icon = R.drawable.schedule_fill0) { navCtrl.navigate("ChangeTime") } - FunctionItem(R.string.change_timezone, icon = R.drawable.schedule_fill0) { navCtrl.navigate("ChangeTimeZone") } + FunctionItem(R.string.change_time, icon = R.drawable.schedule_fill0) { onNavigate(ChangeTime) } + FunctionItem(R.string.change_timezone, icon = R.drawable.schedule_fill0) { onNavigate(ChangeTimeZone) } } /*if(VERSION.SDK_INT >= 28 && (deviceOwner || profileOwner)) FunctionItem(R.string.key_pairs, icon = R.drawable.key_vertical_fill0) { navCtrl.navigate("KeyPairs") }*/ if(VERSION.SDK_INT >= 35 && (deviceOwner || (profileOwner && dpm.isAffiliatedUser))) - FunctionItem(R.string.content_protection_policy, icon = R.drawable.search_fill0) { navCtrl.navigate("ContentProtectionPolicy") } + FunctionItem(R.string.content_protection_policy, icon = R.drawable.search_fill0) { onNavigate(ContentProtectionPolicy) } if(VERSION.SDK_INT >= 23 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.permission_policy, icon = R.drawable.key_fill0) { navCtrl.navigate("PermissionPolicy") } + FunctionItem(R.string.permission_policy, icon = R.drawable.key_fill0) { onNavigate(PermissionPolicy) } } if(VERSION.SDK_INT >= 34 && deviceOwner) { - FunctionItem(R.string.mte_policy, icon = R.drawable.memory_fill0) { navCtrl.navigate("MTEPolicy") } + FunctionItem(R.string.mte_policy, icon = R.drawable.memory_fill0) { onNavigate(MtePolicy) } } if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.nearby_streaming_policy, icon = R.drawable.share_fill0) { navCtrl.navigate("NearbyStreamingPolicy") } + FunctionItem(R.string.nearby_streaming_policy, icon = R.drawable.share_fill0) { onNavigate(NearbyStreamingPolicy) } } if(VERSION.SDK_INT >= 28 && deviceOwner) { - FunctionItem(R.string.lock_task_mode, icon = R.drawable.lock_fill0) { navCtrl.navigate("LockTaskMode") } + FunctionItem(R.string.lock_task_mode, icon = R.drawable.lock_fill0) { onNavigate(LockTaskMode) } } if(deviceOwner || profileOwner) { - FunctionItem(R.string.ca_cert, icon = R.drawable.license_fill0) { navCtrl.navigate("CACert") } + FunctionItem(R.string.ca_cert, icon = R.drawable.license_fill0) { onNavigate(CaCert) } } if(VERSION.SDK_INT >= 26 && !dhizuku && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.security_logging, icon = R.drawable.description_fill0) { navCtrl.navigate("SecurityLogging") } + FunctionItem(R.string.security_logging, icon = R.drawable.description_fill0) { onNavigate(SecurityLogging) } } if(deviceOwner || profileOwner) { - FunctionItem(R.string.disable_account_management, icon = R.drawable.account_circle_fill0) { navCtrl.navigate("DisableAccountManagement") } + FunctionItem(R.string.disable_account_management, icon = R.drawable.account_circle_fill0) { onNavigate(DisableAccountManagement) } } if(VERSION.SDK_INT >= 23 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.system_update_policy, icon = R.drawable.system_update_fill0) { navCtrl.navigate("SystemUpdatePolicy") } + FunctionItem(R.string.system_update_policy, icon = R.drawable.system_update_fill0) { onNavigate(SetSystemUpdatePolicy) } } if(VERSION.SDK_INT >= 29 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.install_system_update, icon = R.drawable.system_update_fill0) { navCtrl.navigate("InstallSystemUpdate") } + FunctionItem(R.string.install_system_update, icon = R.drawable.system_update_fill0) { onNavigate(InstallSystemUpdate) } } if(VERSION.SDK_INT >= 30 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.frp_policy, icon = R.drawable.device_reset_fill0) { navCtrl.navigate("FRPPolicy") } + FunctionItem(R.string.frp_policy, icon = R.drawable.device_reset_fill0) { onNavigate(FrpPolicy) } } if(sp.displayDangerousFeatures && context.isDeviceAdmin && !(VERSION.SDK_INT >= 24 && profileOwner && dpm.isManagedProfile(receiver))) { - FunctionItem(R.string.wipe_data, icon = R.drawable.device_reset_fill0) { navCtrl.navigate("WipeData") } + FunctionItem(R.string.wipe_data, icon = R.drawable.device_reset_fill0) { onNavigate(WipeData) } } } if(dialog != 0 &&VERSION.SDK_INT >= 24) AlertDialog( @@ -238,8 +239,10 @@ fun SystemManage(navCtrl: NavHostController) { ) } +@Serializable object SystemOptions + @Composable -fun SystemOptions(navCtrl: NavHostController) { +fun SystemOptionsScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -247,7 +250,7 @@ fun SystemOptions(navCtrl: NavHostController) { val profileOwner = context.isProfileOwner val um = context.getSystemService(Context.USER_SERVICE) as UserManager var dialog by remember { mutableIntStateOf(0) } - MyScaffold(R.string.options, 0.dp, navCtrl) { + MyScaffold(R.string.options, 0.dp, onNavigateUp) { if(deviceOwner || profileOwner) { SwitchItem(R.string.disable_cam, icon = R.drawable.photo_camera_fill0, getState = { dpm.getCameraDisabled(null) }, onCheckedChange = { dpm.setCameraDisabled(receiver,it) } @@ -323,14 +326,16 @@ fun SystemOptions(navCtrl: NavHostController) { ) } +@Serializable object Keyguard + @Composable -fun Keyguard(navCtrl: NavHostController) { +fun KeyguardScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner - MyScaffold(R.string.keyguard, 8.dp, navCtrl) { + MyScaffold(R.string.keyguard, 8.dp, onNavigateUp) { if(VERSION.SDK_INT >= 23) { Row( horizontalArrangement = Arrangement.SpaceBetween, @@ -379,10 +384,12 @@ fun Keyguard(navCtrl: NavHostController) { } } +@Serializable object HardwareMonitor + @OptIn(ExperimentalMaterial3Api::class) @RequiresApi(24) @Composable -fun HardwareMonitor(navCtrl: NavHostController) { +fun HardwareMonitorScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val hpm = context.getSystemService(HardwarePropertiesManager::class.java) var refreshInterval by remember { mutableFloatStateOf(1F) } @@ -412,7 +419,7 @@ fun HardwareMonitor(navCtrl: NavHostController) { delay(refreshIntervalMs) } } - MyScaffold(R.string.hardware_monitor, 8.dp, navCtrl, false) { + MyScaffold(R.string.hardware_monitor, 8.dp, onNavigateUp, false) { Text(stringResource(R.string.refresh_interval), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp)) Slider(refreshInterval, { refreshInterval = it }, valueRange = 0.5F..2F, steps = 14) Text("${refreshIntervalMs}ms") @@ -460,10 +467,12 @@ fun HardwareMonitor(navCtrl: NavHostController) { } } +@Serializable object ChangeTime + @OptIn(ExperimentalMaterial3Api::class) @RequiresApi(28) @Composable -fun ChangeTime(navCtrl: NavHostController) { +fun ChangeTimeScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -481,7 +490,7 @@ fun ChangeTime(navCtrl: NavHostController) { if(timeInteractionSource.collectIsPressedAsState().value) picker = 2 val isInputLegal = (manualInput && (try { inputTime.toLong() } catch(_: Exception) { -1 }) >= 0) || (!manualInput && datePickerState.selectedDateMillis != null) - MyScaffold(R.string.change_time, 8.dp, navCtrl) { + MyScaffold(R.string.change_time, 8.dp, onNavigateUp) { SingleChoiceSegmentedButtonRow( modifier = Modifier.fillMaxWidth().padding(top = 4.dp) ) { @@ -571,16 +580,18 @@ fun ChangeTime(navCtrl: NavHostController) { ) } +@Serializable object ChangeTimeZone + @RequiresApi(28) @Composable -fun ChangeTimeZone(navCtrl: NavHostController) { +fun ChangeTimeZoneScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val focusMgr = LocalFocusManager.current val receiver = context.getReceiver() var inputTimezone by remember { mutableStateOf("") } var dialog by remember { mutableStateOf(false) } - MyScaffold(R.string.change_timezone, 8.dp, navCtrl) { + MyScaffold(R.string.change_timezone, 8.dp, onNavigateUp) { OutlinedTextField( value = inputTimezone, label = { Text(stringResource(R.string.timezone_id)) }, @@ -814,16 +825,18 @@ fun KeyPairs(navCtrl: NavHostController) { } }*/ +@Serializable object ContentProtectionPolicy + @RequiresApi(35) @Composable -fun ContentProtectionPolicy(navCtrl: NavHostController) { +fun ContentProtectionPolicyScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() var policy by remember { mutableIntStateOf(DevicePolicyManager.CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY) } fun refresh() { policy = dpm.getContentProtectionPolicy(receiver) } LaunchedEffect(Unit) { refresh() } - MyScaffold(R.string.content_protection_policy, 8.dp, navCtrl) { + MyScaffold(R.string.content_protection_policy, 8.dp, onNavigateUp) { mapOf( DevicePolicyManager.CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY to R.string.not_controlled_by_policy, DevicePolicyManager.CONTENT_PROTECTION_ENABLED to R.string.enabled, @@ -845,14 +858,16 @@ fun ContentProtectionPolicy(navCtrl: NavHostController) { } } +@Serializable object PermissionPolicy + @RequiresApi(23) @Composable -fun PermissionPolicy(navCtrl: NavHostController) { +fun PermissionPolicyScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() var selectedPolicy by remember { mutableIntStateOf(dpm.getPermissionPolicy(receiver)) } - MyScaffold(R.string.permission_policy, 8.dp, navCtrl) { + MyScaffold(R.string.permission_policy, 8.dp, onNavigateUp) { RadioButtonItem(R.string.default_stringres, selectedPolicy == PERMISSION_POLICY_PROMPT) { selectedPolicy = PERMISSION_POLICY_PROMPT } RadioButtonItem(R.string.auto_grant, selectedPolicy == PERMISSION_POLICY_AUTO_GRANT) { selectedPolicy = PERMISSION_POLICY_AUTO_GRANT } RadioButtonItem(R.string.auto_deny, selectedPolicy == PERMISSION_POLICY_AUTO_DENY) { selectedPolicy = PERMISSION_POLICY_AUTO_DENY } @@ -870,13 +885,15 @@ fun PermissionPolicy(navCtrl: NavHostController) { } } +@Serializable object MtePolicy + @RequiresApi(34) @Composable -fun MTEPolicy(navCtrl: NavHostController) { +fun MtePolicyScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() var selectedMtePolicy by remember { mutableIntStateOf(dpm.mtePolicy) } - MyScaffold(R.string.mte_policy, 8.dp, navCtrl) { + MyScaffold(R.string.mte_policy, 8.dp, onNavigateUp) { RadioButtonItem(R.string.decide_by_user, selectedMtePolicy == MTE_NOT_CONTROLLED_BY_POLICY) { selectedMtePolicy = MTE_NOT_CONTROLLED_BY_POLICY } RadioButtonItem(R.string.enabled, selectedMtePolicy == MTE_ENABLED) { selectedMtePolicy = MTE_ENABLED } RadioButtonItem(R.string.disabled, selectedMtePolicy == MTE_DISABLED) { selectedMtePolicy = MTE_DISABLED } @@ -898,13 +915,15 @@ fun MTEPolicy(navCtrl: NavHostController) { } } +@Serializable object NearbyStreamingPolicy + @RequiresApi(31) @Composable -fun NearbyStreamingPolicy(navCtrl: NavHostController) { +fun NearbyStreamingPolicyScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() var appPolicy by remember { mutableIntStateOf(dpm.nearbyAppStreamingPolicy) } - MyScaffold(R.string.nearby_streaming_policy, 8.dp, navCtrl) { + MyScaffold(R.string.nearby_streaming_policy, 8.dp, onNavigateUp) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.nearby_app_streaming), style = typography.titleLarge) Spacer(Modifier.padding(vertical = 3.dp)) @@ -963,10 +982,12 @@ fun NearbyStreamingPolicy(navCtrl: NavHostController) { } } +@Serializable object LockTaskMode + @OptIn(ExperimentalMaterial3Api::class) @RequiresApi(28) @Composable -fun LockTaskMode(navCtrl: NavHostController, vm: MyViewModel) { +fun LockTaskModeScreen(onNavigateUp: () -> Unit) { val coroutine = rememberCoroutineScope() val pagerState = rememberPagerState { 3 } var tabIndex by remember { mutableIntStateOf(0) } @@ -975,7 +996,7 @@ fun LockTaskMode(navCtrl: NavHostController, vm: MyViewModel) { topBar = { TopAppBar( title = { Text(stringResource(R.string.lock_task_mode)) }, - navigationIcon = { NavIcon { navCtrl.navigateUp() } } + navigationIcon = { NavIcon(onNavigateUp) } ) } ) { paddingValues -> @@ -1000,8 +1021,8 @@ fun LockTaskMode(navCtrl: NavHostController, vm: MyViewModel) { Column( modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(start = 8.dp, end = 8.dp, bottom = 80.dp) ) { - if(page == 0) StartLockTaskMode(navCtrl, vm) - else if(page == 1) LockTaskPackages(navCtrl, vm) + if(page == 0) StartLockTaskMode() + else if(page == 1) LockTaskPackages() else LockTaskFeatures() } } @@ -1011,19 +1032,15 @@ fun LockTaskMode(navCtrl: NavHostController, vm: MyViewModel) { @RequiresApi(28) @Composable -private fun ColumnScope.StartLockTaskMode(navCtrl: NavHostController, vm: MyViewModel) { +private fun ColumnScope.StartLockTaskMode() { val context = LocalContext.current val dpm = context.getDPM() val focusMgr = LocalFocusManager.current var startLockTaskApp by rememberSaveable { mutableStateOf("") } var startLockTaskActivity by rememberSaveable { mutableStateOf("") } var specifyActivity by rememberSaveable { mutableStateOf(false) } - val updatePackage by vm.selectedPackage.collectAsStateWithLifecycle() - LaunchedEffect(updatePackage) { - if(updatePackage != "") { - startLockTaskApp = updatePackage - vm.selectedPackage.value = "" - } + val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { result -> + result?.let { startLockTaskApp = it } } Spacer(Modifier.padding(vertical = 5.dp)) OutlinedTextField( @@ -1036,10 +1053,7 @@ private fun ColumnScope.StartLockTaskMode(navCtrl: NavHostController, vm: MyView Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, modifier = Modifier .clip(RoundedCornerShape(50)) - .clickable(onClick = { - focusMgr.clearFocus() - navCtrl.navigate("PackageSelector") - }) + .clickable { choosePackage.launch(null) } .padding(3.dp)) }, modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) @@ -1081,19 +1095,15 @@ private fun ColumnScope.StartLockTaskMode(navCtrl: NavHostController, vm: MyView @RequiresApi(26) @Composable -private fun ColumnScope.LockTaskPackages(navCtrl: NavHostController, vm: MyViewModel) { +private fun ColumnScope.LockTaskPackages() { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current val lockTaskPackages = remember { mutableStateListOf() } var input by rememberSaveable { mutableStateOf("") } - val updatePackage by vm.selectedPackage.collectAsStateWithLifecycle() - LaunchedEffect(updatePackage) { - if(updatePackage != "") { - input = updatePackage - vm.selectedPackage.value = "" - } + val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { result -> + result?.let { input = it } } LaunchedEffect(Unit) { lockTaskPackages.addAll(dpm.getLockTaskPackages(receiver)) } Spacer(Modifier.padding(vertical = 5.dp)) @@ -1111,10 +1121,7 @@ private fun ColumnScope.LockTaskPackages(navCtrl: NavHostController, vm: MyViewM Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, modifier = Modifier .clip(RoundedCornerShape(50)) - .clickable(onClick = { - focusMgr.clearFocus() - navCtrl.navigate("PackageSelector") - }) + .clickable { choosePackage.launch(null) } .padding(3.dp)) }, modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) @@ -1221,8 +1228,10 @@ private fun ColumnScope.LockTaskFeatures() { } } +@Serializable object CaCert + @Composable -fun CACert(navCtrl: NavHostController) { +fun CaCertScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -1235,7 +1244,7 @@ fun CACert(navCtrl: NavHostController) { } dialog = true } - MyScaffold(R.string.ca_cert, 8.dp, navCtrl) { + MyScaffold(R.string.ca_cert, 8.dp, onNavigateUp) { Button( onClick = { getFileLauncher.launch("*/*") @@ -1278,9 +1287,11 @@ fun CACert(navCtrl: NavHostController) { } } +@Serializable object SecurityLogging + @RequiresApi(24) @Composable -fun SecurityLogging(navCtrl: NavHostController) { +fun SecurityLoggingScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -1305,7 +1316,7 @@ fun SecurityLogging(navCtrl: NavHostController) { } } } - MyScaffold(R.string.security_logging, 8.dp, navCtrl) { + MyScaffold(R.string.security_logging, 8.dp, onNavigateUp) { SwitchItem( R.string.enable, getState = { dpm.isSecurityLoggingEnabled(receiver) }, onCheckedChange = { dpm.setSecurityLoggingEnabled(receiver, it) }, @@ -1366,13 +1377,15 @@ fun SecurityLogging(navCtrl: NavHostController) { } } +@Serializable object DisableAccountManagement + @Composable -fun DisableAccountManagement(navCtrl: NavHostController) { +fun DisableAccountManagementScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - MyScaffold(R.string.disable_account_management, 8.dp, navCtrl) { + MyScaffold(R.string.disable_account_management, 8.dp, onNavigateUp) { val list = remember { mutableStateListOf() } fun refreshList() { list.clear() @@ -1414,9 +1427,11 @@ fun DisableAccountManagement(navCtrl: NavHostController) { } } +@Serializable object FrpPolicy + @RequiresApi(30) @Composable -fun FRPPolicy(navCtrl: NavHostController) { +fun FrpPolicyScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val focusMgr = LocalFocusManager.current @@ -1442,7 +1457,7 @@ fun FRPPolicy(navCtrl: NavHostController) { } } } - MyScaffold(R.string.frp_policy, 8.dp, navCtrl) { + MyScaffold(R.string.frp_policy, 8.dp, onNavigateUp) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp, vertical = 8.dp) @@ -1506,8 +1521,10 @@ fun FRPPolicy(navCtrl: NavHostController) { } } +@Serializable object WipeData + @Composable -fun WipeData(navCtrl: NavHostController) { +fun WipeDataScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val dpm = context.getDPM() @@ -1517,7 +1534,7 @@ fun WipeData(navCtrl: NavHostController) { var wipeDevice by remember { mutableStateOf(false) } var silent by remember { mutableStateOf(false) } var reason by remember { mutableStateOf("") } - MyScaffold(R.string.wipe_data, 8.dp, navCtrl) { + MyScaffold(R.string.wipe_data, 8.dp, onNavigateUp) { CheckBoxItem(R.string.wipe_external_storage, flag and WIPE_EXTERNAL_STORAGE != 0) { flag = flag xor WIPE_EXTERNAL_STORAGE } if(VERSION.SDK_INT >= 22 && context.isDeviceOwner) CheckBoxItem( R.string.wipe_reset_protection_data, flag and WIPE_RESET_PROTECTION_DATA != 0) { flag = flag xor WIPE_RESET_PROTECTION_DATA } @@ -1612,13 +1629,15 @@ fun WipeData(navCtrl: NavHostController) { } } +@Serializable object SetSystemUpdatePolicy + @Composable -fun SystemUpdatePolicy(navCtrl: NavHostController) { +fun SystemUpdatePolicy(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - MyScaffold(R.string.system_update_policy, 8.dp, navCtrl) { + MyScaffold(R.string.system_update_policy, 8.dp, onNavigateUp) { if(VERSION.SDK_INT >= 23) { Column { var selectedPolicy by remember { mutableStateOf(dpm.systemUpdatePolicy?.policyType) } @@ -1700,9 +1719,11 @@ fun SystemUpdatePolicy(navCtrl: NavHostController) { } } +@Serializable object InstallSystemUpdate + @SuppressLint("NewApi") @Composable -fun InstallSystemUpdate(navCtrl: NavHostController) { +fun InstallSystemUpdateScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -1724,7 +1745,7 @@ fun InstallSystemUpdate(navCtrl: NavHostController) { val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { uri = it.data?.data } - MyScaffold(R.string.install_system_update, 8.dp, navCtrl) { + MyScaffold(R.string.install_system_update, 8.dp, onNavigateUp) { Button( onClick = { val intent = Intent(Intent.ACTION_GET_CONTENT) diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt index 1ab8e24..221ec6c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt @@ -10,18 +10,17 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController -import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.R import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.SwitchItem +import kotlinx.serialization.Serializable +@Serializable data class Restriction( val id: String, @StringRes val name: Int, @@ -29,45 +28,59 @@ data class Restriction( val requiresApi: Int = 0 ) +@Serializable object UserRestriction + @RequiresApi(24) @Composable -fun UserRestriction(navCtrl:NavHostController, vm: MyViewModel) { +fun UserRestrictionScreen(onNavigateUp: () -> Unit, onNavigate: (Int, List) -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - LaunchedEffect(Unit) { - vm.userRestrictions.value = dpm.getUserRestrictions(receiver) - } - MyScaffold(R.string.user_restriction, 0.dp, navCtrl) { + MyScaffold(R.string.user_restriction, 0.dp, onNavigateUp) { Text(text = stringResource(R.string.switch_to_disable_feature), modifier = Modifier.padding(start = 16.dp)) if(context.isProfileOwner) { Text(text = stringResource(R.string.profile_owner_is_restricted), modifier = Modifier.padding(start = 16.dp)) } if(context.isProfileOwner && dpm.isManagedProfile(receiver)) { Text(text = stringResource(R.string.some_features_invalid_in_work_profile), modifier = Modifier.padding(start = 16.dp)) } Spacer(Modifier.padding(vertical = 2.dp)) - FunctionItem(R.string.network_and_internet, icon = R.drawable.wifi_fill0) { navCtrl.navigate("UR-Internet") } - FunctionItem(R.string.connectivity, icon = R.drawable.devices_other_fill0) { navCtrl.navigate("UR-Connectivity") } - FunctionItem(R.string.applications, icon = R.drawable.apps_fill0) { navCtrl.navigate("UR-Applications") } - FunctionItem(R.string.users, icon = R.drawable.account_circle_fill0) { navCtrl.navigate("UR-Users") } - FunctionItem(R.string.media, icon = R.drawable.volume_up_fill0) { navCtrl.navigate("UR-Media") } - FunctionItem(R.string.other, icon = R.drawable.more_horiz_fill0) { navCtrl.navigate("UR-Other") } + FunctionItem(R.string.network, icon = R.drawable.language_fill0) { + onNavigate(R.string.network, RestrictionData.internet) + } + FunctionItem(R.string.connectivity, icon = R.drawable.devices_other_fill0) { + onNavigate(R.string.connectivity, RestrictionData.connectivity) + } + FunctionItem(R.string.applications, icon = R.drawable.apps_fill0) { + onNavigate(R.string.applications, RestrictionData.applications) + } + FunctionItem(R.string.users, icon = R.drawable.account_circle_fill0) { + onNavigate(R.string.users, RestrictionData.users) + } + FunctionItem(R.string.media, icon = R.drawable.volume_up_fill0) { + onNavigate(R.string.media, RestrictionData.media) + } + FunctionItem(R.string.other, icon = R.drawable.more_horiz_fill0) { + onNavigate(R.string.other, RestrictionData.other) + } } } +@Serializable +data class UserRestrictionOptions( + val title: Int, val items: List +) + @RequiresApi(24) @Composable -fun UserRestrictionScreen( - title: Int, items: List, restrictions: Bundle, +fun UserRestrictionOptionsScreen( + data: UserRestrictionOptions, restrictions: Bundle, onRestrictionChange: (String, Boolean) -> Unit, onNavigateUp: () -> Unit ) { - MyScaffold(title, 0.dp, onNavigateUp, false) { - items.filter { Build.VERSION.SDK_INT >= it.requiresApi }.forEach { restriction -> + MyScaffold(data.title, 0.dp, onNavigateUp, false) { + data.items.filter { Build.VERSION.SDK_INT >= it.requiresApi }.forEach { restriction -> SwitchItem( restriction.name, restriction.id, restriction.icon, restrictions.getBoolean(restriction.id), { onRestrictionChange(restriction.id, it) }, padding = true ) - /*Box(modifier = Modifier.padding(start = 22.dp, end = 16.dp)) { - }*/ } } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt index 94704d2..69887bb 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt @@ -69,7 +69,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.navigation.NavHostController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.parseTimestamp import com.bintianqi.owndroid.showOperationResultToast @@ -85,41 +84,44 @@ import com.bintianqi.owndroid.yesOrNo import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable + +@Serializable object Users @Composable -fun Users(navCtrl: NavHostController) { +fun UsersScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner var dialog by remember { mutableIntStateOf(0) } - MyScaffold(R.string.users, 0.dp, navCtrl) { + MyScaffold(R.string.users, 0.dp, onNavigateUp) { if(VERSION.SDK_INT >= 28 && profileOwner && dpm.isAffiliatedUser) { FunctionItem(R.string.logout, icon = R.drawable.logout_fill0) { dialog = 2 } } - FunctionItem(R.string.user_info, icon = R.drawable.person_fill0) { navCtrl.navigate("UserInfo") } + FunctionItem(R.string.user_info, icon = R.drawable.person_fill0) { onNavigate(UserInfo) } if(deviceOwner && VERSION.SDK_INT >= 28) { FunctionItem(R.string.secondary_users, icon = R.drawable.list_fill0) { dialog = 1 } - FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { navCtrl.navigate("UserOptions") } + FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { onNavigate(UsersOptions) } } if(deviceOwner) { - FunctionItem(R.string.user_operation, icon = R.drawable.sync_alt_fill0) { navCtrl.navigate("UserOperation") } + FunctionItem(R.string.user_operation, icon = R.drawable.sync_alt_fill0) { onNavigate(UserOperation) } } if(VERSION.SDK_INT >= 24 && deviceOwner) { - FunctionItem(R.string.create_user, icon = R.drawable.person_add_fill0) { navCtrl.navigate("CreateUser") } + FunctionItem(R.string.create_user, icon = R.drawable.person_add_fill0) { onNavigate(CreateUser) } } if(deviceOwner || profileOwner) { - FunctionItem(R.string.change_username, icon = R.drawable.edit_fill0) { navCtrl.navigate("ChangeUsername") } + FunctionItem(R.string.change_username, icon = R.drawable.edit_fill0) { onNavigate(ChangeUsername) } } if(VERSION.SDK_INT >= 23 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.change_user_icon, icon = R.drawable.account_circle_fill0) { navCtrl.navigate("ChangeUserIcon") } + FunctionItem(R.string.change_user_icon, icon = R.drawable.account_circle_fill0) { onNavigate(ChangeUserIcon) } } if(VERSION.SDK_INT >= 28 && deviceOwner) { - FunctionItem(R.string.user_session_msg, icon = R.drawable.notifications_fill0) { navCtrl.navigate("UserSessionMessage") } + FunctionItem(R.string.user_session_msg, icon = R.drawable.notifications_fill0) { onNavigate(UserSessionMessage) } } if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.affiliation_id, icon = R.drawable.id_card_fill0) { navCtrl.navigate("AffiliationID") } + FunctionItem(R.string.affiliation_id, icon = R.drawable.id_card_fill0) { onNavigate(AffiliationId) } } } if(dialog != 0 && VERSION.SDK_INT >= 28) AlertDialog( @@ -159,27 +161,31 @@ fun Users(navCtrl: NavHostController) { ) } +@Serializable object UsersOptions + @Composable -fun UserOptions(navCtrl: NavHostController) { +fun UsersOptionsScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - MyScaffold(R.string.options, 0.dp, navCtrl) { + MyScaffold(R.string.options, 0.dp, onNavigateUp) { if(VERSION.SDK_INT >= 28) { SwitchItem(R.string.enable_logout, getState = { dpm.isLogoutEnabled }, onCheckedChange = { dpm.setLogoutEnabled(receiver, it) }) } } } +@Serializable object UserInfo + @Composable -fun CurrentUserInfo(navCtrl: NavHostController) { +fun UserInfoScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val user = Process.myUserHandle() var infoDialog by remember { mutableIntStateOf(0) } - MyScaffold(R.string.user_info, 8.dp, navCtrl) { + MyScaffold(R.string.user_info, 8.dp, onNavigateUp) { if(VERSION.SDK_INT >= 24) CardItem(R.string.support_multiuser, UserManager.supportsMultipleUsers().yesOrNo) if(VERSION.SDK_INT >= 31) CardItem(R.string.headless_system_user_mode, UserManager.isHeadlessSystemUserMode().yesOrNo) { infoDialog = 1 } Spacer(Modifier.padding(vertical = 8.dp)) @@ -208,8 +214,10 @@ fun CurrentUserInfo(navCtrl: NavHostController) { ) } +@Serializable object UserOperation + @Composable -fun UserOperation(navCtrl: NavHostController) { +fun UserOperationScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val dpm = context.getDPM() @@ -230,7 +238,7 @@ fun UserOperation(navCtrl: NavHostController) { } } val legalInput = input.toIntOrNull() != null - MyScaffold(R.string.user_operation, 8.dp, navCtrl) { + MyScaffold(R.string.user_operation, 8.dp, onNavigateUp) { if(VERSION.SDK_INT >= 24) SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { SegmentedButton(!useUserId, { useUserId = false }, SegmentedButtonDefaults.itemShape(0, 2)) { Text(stringResource(R.string.serial_number)) @@ -311,9 +319,11 @@ fun UserOperation(navCtrl: NavHostController) { } } +@Serializable object CreateUser + @RequiresApi(24) @Composable -fun CreateUser(navCtrl: NavHostController) { +fun CreateUserScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val dpm = context.getDPM() @@ -324,7 +334,7 @@ fun CreateUser(navCtrl: NavHostController) { var createdUserSerialNumber by remember { mutableLongStateOf(-1) } var flag by remember { mutableIntStateOf(0) } val coroutine = rememberCoroutineScope() - MyScaffold(R.string.create_user, 8.dp, navCtrl) { + MyScaffold(R.string.create_user, 8.dp, onNavigateUp) { OutlinedTextField( value = userName, onValueChange = { userName= it }, @@ -383,9 +393,11 @@ fun CreateUser(navCtrl: NavHostController) { } } +@Serializable object AffiliationId + @RequiresApi(26) @Composable -fun AffiliationID(navCtrl: NavHostController) { +fun AffiliationIdScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -397,7 +409,7 @@ fun AffiliationID(navCtrl: NavHostController) { list.addAll(dpm.getAffiliationIds(receiver)) } LaunchedEffect(Unit) { refreshIds() } - MyScaffold(R.string.affiliation_id, 8.dp, navCtrl) { + MyScaffold(R.string.affiliation_id, 8.dp, onNavigateUp) { Column(modifier = Modifier.animateContentSize()) { if(list.isEmpty()) Text(stringResource(R.string.none)) for(i in list) { @@ -439,14 +451,16 @@ fun AffiliationID(navCtrl: NavHostController) { } } +@Serializable object ChangeUsername + @Composable -fun ChangeUsername(navCtrl: NavHostController) { +fun ChangeUsernameScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current var inputUsername by remember { mutableStateOf("") } - MyScaffold(R.string.change_username, 8.dp, navCtrl) { + MyScaffold(R.string.change_username, 8.dp, onNavigateUp) { OutlinedTextField( value = inputUsername, onValueChange = { inputUsername= it }, @@ -474,9 +488,11 @@ fun ChangeUsername(navCtrl: NavHostController) { } } +@Serializable object UserSessionMessage + @RequiresApi(28) @Composable -fun UserSessionMessage(navCtrl: NavHostController) { +fun UserSessionMessageScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -488,7 +504,7 @@ fun UserSessionMessage(navCtrl: NavHostController) { end = dpm.getEndUserSessionMessage(receiver)?.toString() ?: "" } LaunchedEffect(Unit) { refreshMsg() } - MyScaffold(R.string.user_session_msg, 8.dp, navCtrl) { + MyScaffold(R.string.user_session_msg, 8.dp, onNavigateUp) { OutlinedTextField( value = start, onValueChange = { start= it }, @@ -552,9 +568,11 @@ fun UserSessionMessage(navCtrl: NavHostController) { } } +@Serializable object ChangeUserIcon + @RequiresApi(23) @Composable -fun ChangeUserIcon(navCtrl: NavHostController) { +fun ChangeUserIconScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -567,7 +585,7 @@ fun ChangeUserIcon(navCtrl: NavHostController) { } } } - MyScaffold(R.string.change_user_icon, 8.dp, navCtrl) { + MyScaffold(R.string.change_user_icon, 8.dp, onNavigateUp) { CheckBoxItem(R.string.file_picker_instead_gallery, getContent) { getContent = it } Spacer(Modifier.padding(vertical = 5.dp)) Button( diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt similarity index 91% rename from app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt rename to app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt index 0d45c2b..f24a933 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt @@ -53,7 +53,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CardItem @@ -64,39 +63,44 @@ import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.yesOrNo +import kotlinx.serialization.Serializable + +@Serializable object WorkProfile @Composable -fun WorkProfile(navCtrl: NavHostController) { +fun WorkProfileScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val profileOwner = context.isProfileOwner - MyScaffold(R.string.work_profile, 0.dp, navCtrl) { + MyScaffold(R.string.work_profile, 0.dp, onNavigateUp) { if(VERSION.SDK_INT >= 30 && profileOwner && dpm.isManagedProfile(receiver)) { - FunctionItem(R.string.org_owned_work_profile, icon = R.drawable.corporate_fare_fill0) { navCtrl.navigate("OrgOwnedWorkProfile") } + FunctionItem(R.string.org_owned_work_profile, icon = R.drawable.corporate_fare_fill0) { onNavigate(OrganizationOwnedProfile) } } if(VERSION.SDK_INT < 24 || dpm.isProvisioningAllowed(ACTION_PROVISION_MANAGED_PROFILE)) { - FunctionItem(R.string.create_work_profile, icon = R.drawable.work_fill0) { navCtrl.navigate("CreateWorkProfile") } + FunctionItem(R.string.create_work_profile, icon = R.drawable.work_fill0) { onNavigate(CreateWorkProfile) } } if(dpm.isOrgProfile(receiver)) { - FunctionItem(R.string.suspend_personal_app, icon = R.drawable.block_fill0) { navCtrl.navigate("SuspendPersonalApp") } + FunctionItem(R.string.suspend_personal_app, icon = R.drawable.block_fill0) { onNavigate(SuspendPersonalApp) } } if(profileOwner && (VERSION.SDK_INT < 24 || dpm.isManagedProfile(receiver))) { - FunctionItem(R.string.intent_filter, icon = R.drawable.filter_alt_fill0) { navCtrl.navigate("IntentFilter") } + FunctionItem(R.string.intent_filter, icon = R.drawable.filter_alt_fill0) { onNavigate(CrossProfileIntentFilter) } } if(profileOwner && (VERSION.SDK_INT < 24 || dpm.isManagedProfile(receiver))) { - FunctionItem(R.string.delete_work_profile, icon = R.drawable.delete_forever_fill0) { navCtrl.navigate("DeleteWorkProfile") } + FunctionItem(R.string.delete_work_profile, icon = R.drawable.delete_forever_fill0) { onNavigate(DeleteWorkProfile) } } } } +@Serializable object CreateWorkProfile + @Composable -fun CreateWorkProfile(navCtrl: NavHostController) { +fun CreateWorkProfileScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { } - MyScaffold(R.string.create_work_profile, 8.dp, navCtrl) { + MyScaffold(R.string.create_work_profile, 8.dp, onNavigateUp) { var skipEncrypt by remember { mutableStateOf(false) } var offlineProvisioning by remember { mutableStateOf(true) } var migrateAccount by remember { mutableStateOf(false) } @@ -160,12 +164,14 @@ fun CreateWorkProfile(navCtrl: NavHostController) { } } +@Serializable object OrganizationOwnedProfile + @RequiresApi(30) @Composable -fun OrgOwnedProfile(navCtrl: NavHostController) { +fun OrganizationOwnedProfileScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() - MyScaffold(R.string.org_owned_work_profile, 8.dp, navCtrl, false) { + MyScaffold(R.string.org_owned_work_profile, 8.dp, onNavigateUp, false) { CardItem(R.string.org_owned_work_profile, dpm.isOrganizationOwnedDeviceWithManagedProfile.yesOrNo) Spacer(Modifier.padding(vertical = 5.dp)) if(!dpm.isOrganizationOwnedDeviceWithManagedProfile) { @@ -180,15 +186,17 @@ fun OrgOwnedProfile(navCtrl: NavHostController) { } } +@Serializable object SuspendPersonalApp + @RequiresApi(30) @Composable -fun SuspendPersonalApp(navCtrl: NavHostController) { +fun SuspendPersonalAppScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current var suspend by remember { mutableStateOf(dpm.getPersonalAppsSuspendedReasons(receiver) != PERSONAL_APPS_NOT_SUSPENDED) } - MyScaffold(R.string.suspend_personal_app, 8.dp, navCtrl) { + MyScaffold(R.string.suspend_personal_app, 8.dp, onNavigateUp) { SwitchItem(R.string.suspend_personal_app, state = suspend, onCheckedChange = { dpm.setPersonalAppsSuspended(receiver,it) @@ -226,13 +234,15 @@ fun SuspendPersonalApp(navCtrl: NavHostController) { } } +@Serializable object CrossProfileIntentFilter + @Composable -fun IntentFilter(navCtrl: NavHostController) { +fun CrossProfileIntentFilterScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - MyScaffold(R.string.intent_filter, 8.dp, navCtrl) { + MyScaffold(R.string.intent_filter, 8.dp, onNavigateUp) { var action by remember { mutableStateOf("") } OutlinedTextField( value = action, onValueChange = { action = it }, @@ -274,8 +284,10 @@ fun IntentFilter(navCtrl: NavHostController) { } } +@Serializable object DeleteWorkProfile + @Composable -fun DeleteWorkProfile(navCtrl: NavHostController) { +fun DeleteWorkProfileScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val focusMgr = LocalFocusManager.current @@ -283,7 +295,7 @@ fun DeleteWorkProfile(navCtrl: NavHostController) { var warning by remember { mutableStateOf(false) } var silent by remember { mutableStateOf(false) } var reason by remember { mutableStateOf("") } - MyScaffold(R.string.delete_work_profile, 8.dp, navCtrl) { + MyScaffold(R.string.delete_work_profile, 8.dp, onNavigateUp) { CheckBoxItem(R.string.wipe_external_storage, flag and WIPE_EXTERNAL_STORAGE != 0) { flag = flag xor WIPE_EXTERNAL_STORAGE } if(VERSION.SDK_INT >= 28) CheckBoxItem(R.string.wipe_euicc, flag and WIPE_EUICC != 0) { flag = flag xor WIPE_EUICC } CheckBoxItem(R.string.wipe_silently, silent) { silent = it } diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt index 741d5d6..e469dff 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt @@ -30,7 +30,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.navigation.NavHostController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.writeClipBoard import com.bintianqi.owndroid.zhCN @@ -263,22 +262,13 @@ fun InfoCard(@StringRes strID: Int) { } } -@Composable -fun MyScaffold( - @StringRes title: Int, - horizonPadding: Dp, - navCtrl: NavHostController, - displayTitle: Boolean = true, - content: @Composable ColumnScope.() -> Unit -) = MyScaffold(title, horizonPadding, { navCtrl.navigateUp() }, displayTitle, content) - @OptIn(ExperimentalMaterial3Api::class) @Composable fun MyScaffold( @StringRes title: Int, horizonPadding: Dp, onNavIconClicked: () -> Unit, - displayTitle: Boolean, + displayTitle: Boolean = true, content: @Composable ColumnScope.() -> Unit ) { val scrollState = rememberScrollState() diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/theme/Theme.kt b/app/src/main/java/com/bintianqi/owndroid/ui/theme/Theme.kt index 0e1c8e0..02bac15 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/theme/Theme.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/theme/Theme.kt @@ -6,14 +6,12 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.getValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.bintianqi.owndroid.MyViewModel +import com.bintianqi.owndroid.ThemeSettings private val lightScheme = lightColorScheme( primary = primaryLight, @@ -93,10 +91,9 @@ private val darkScheme = darkColorScheme( @Composable fun OwnDroidTheme( - vm: MyViewModel, + theme: ThemeSettings, content: @Composable () -> Unit ) { - val theme by vm.theme.collectAsStateWithLifecycle() val darkTheme = theme.darkTheme == 1 || (theme.darkTheme == -1 && isSystemInDarkTheme()) val context = LocalContext.current var colorScheme = when { diff --git a/app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt deleted file mode 100644 index 716e55f..0000000 --- a/app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 deleted file mode 100644 index b932e11..0000000 --- a/app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 deleted file mode 100644 index f178e45..0000000 --- a/app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 598a85c..e949d81 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -369,8 +369,6 @@ Приложения - Выбор пакета - Загрузка Показывать пользовательские приложения Показывать системные приложения Приостановить @@ -419,7 +417,6 @@ Владелец профиля может использовать ограниченные функции Включите переключатель, чтобы отключить эту функцию. Функции в рабочем профиле ограничены. - Сеть Другие подключения Медиа Другое @@ -646,6 +643,8 @@ Доступ к датчикам тела в фоновом режиме Распознавание активности + Package chooser + Установщик приложений Режим Полная установка diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 0ed2fed..431c5a1 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -373,8 +373,6 @@ Uygulamalar - Paket seçici - Yükleniyor Kullanıcı uygulamalarını göster Sistem uygulamalarını göster Askıya al @@ -424,7 +422,6 @@ Profil sahibi sınırlı işlev kullanabilir Bu işlevi devre dışı bırakmak için bir anahtar açın. İş profilindeki işlevler sınırlıdır. - Diğer bağlantı Medya Diğer @@ -648,6 +645,8 @@ Arka planda vücut sensörlerine eriş Aktivite tanıma + Package chooser + App installer Mode diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d461170..fe761ca 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -362,8 +362,6 @@ 应用程序 - 应用选择器 - 加载中 显示用户应用 显示系统应用 挂起 @@ -411,7 +409,6 @@ Profile owner无法使用部分功能 打开开关后会禁用对应的功能 工作资料中部分功能无效 - 网络和互联网 更多连接 媒体 其他 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9774d6b..dc1e3ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -398,8 +398,6 @@ Applications - Package selector - Loading Show user apps Show system apps Suspend @@ -448,7 +446,6 @@ Profile owner can use limited function Turn on a switch to disable that function. Functions in work profile is limited. - Network Other connection Media Other @@ -673,6 +670,8 @@ Access body sensors in background Activity recognition + Package chooser + App installer Mode Full install