diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 44bc16f..41478fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,10 @@ on: push: paths-ignore: - '**.md' + tags-ignore: + - '**' + branches-ignore: + - 'master' jobs: build: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f53e97e..1b01983 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.cc) } var keyPassword: String? = null @@ -26,8 +27,8 @@ android { applicationId = "com.bintianqi.owndroid" minSdk = 21 targetSdk = 34 - versionCode = 30 - versionName = "5.5" + versionCode = 31 + versionName = "5.6" multiDexEnabled = false } @@ -56,9 +57,6 @@ android { compose = true aidl = true } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.13" - } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 93b4c89..0506601 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,7 @@ + @@ -51,13 +52,24 @@ android:windowSoftInputMode="adjustResize|stateHidden" android:theme="@style/Theme.OwnDroid"> + + + + + + android:theme="@style/Theme.Transparent"> @@ -86,9 +98,20 @@ + android:name=".PackageInstallerReceiver" + android:description="@string/app_name" + android:permission="android.permission.BIND_DEVICE_ADMIN"> + + + + + + + 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/AutomationActivity.kt b/app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt new file mode 100644 index 0000000..2245e64 --- /dev/null +++ b/app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt @@ -0,0 +1,27 @@ +package com.bintianqi.owndroid + +import android.app.AlertDialog +import android.content.Context +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.ui.platform.LocalContext + +class AutomationActivity: ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val result = handleTask(applicationContext, this.intent) + val sharedPrefs = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE) + if(sharedPrefs.getBoolean("automation_debug", false)) { + setContent { + AlertDialog.Builder(LocalContext.current) + .setMessage(result) + .setOnDismissListener { finish() } + .setPositiveButton(R.string.confirm) { _, _ -> finish() } + .show() + } + } else { + finish() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt b/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt new file mode 100644 index 0000000..ee32652 --- /dev/null +++ b/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt @@ -0,0 +1,48 @@ +package com.bintianqi.owndroid + +import android.annotation.SuppressLint +import android.app.admin.DevicePolicyManager +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import androidx.activity.ComponentActivity + +class AutomationReceiver: BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + handleTask(context, intent) + } +} + +@SuppressLint("NewApi") +fun handleTask(context: Context, intent: Intent): String { + val sharedPrefs = context.getSharedPreferences("data", Context.MODE_PRIVATE) + val key = sharedPrefs.getString("automation_key", "") ?: "" + if(key.length < 6) { + return "Key length must longer than 6" + } + if(key != intent.getStringExtra("key")) { + return "Wrong key" + } + val operation = intent.getStringExtra("operation") + val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val receiver = ComponentName(context,Receiver::class.java) + val app = intent.getStringExtra("app") + val restriction = intent.getStringExtra("restriction") + try { + when(operation) { + "suspend" -> dpm.setPackagesSuspended(receiver, arrayOf(app), true) + "unsuspend" -> dpm.setPackagesSuspended(receiver, arrayOf(app), false) + "hide" -> dpm.setApplicationHidden(receiver, app, true) + "unhide" -> dpm.setApplicationHidden(receiver, app, false) + "lock" -> dpm.lockNow() + "reboot" -> dpm.reboot(receiver) + "addUserRestriction" -> dpm.addUserRestriction(receiver, restriction) + "clearUserRestriction" -> dpm.clearUserRestriction(receiver, restriction) + else -> return "Operation not defined" + } + } catch(e: Exception) { + return e.message ?: "Failed to get error message" + } + return "No error, or error is unhandled" +} diff --git a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt index 84f8802..f1bdef1 100644 --- a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt @@ -8,7 +8,9 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Text @@ -17,13 +19,21 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import androidx.core.view.WindowCompat import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import com.bintianqi.owndroid.dpm.installPackage import com.bintianqi.owndroid.ui.theme.OwnDroidTheme +import com.github.fishb1.apkinfo.ApkInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.FileInputStream class InstallAppActivity: FragmentActivity() { override fun onNewIntent(intent: Intent?) { @@ -34,37 +44,64 @@ class InstallAppActivity: FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + WindowCompat.setDecorFitsSystemWindows(window, false) + val context = applicationContext val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE) window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + val uri = this.intent.data!! + var apkInfoText by mutableStateOf(context.getString(R.string.parsing_apk_info)) + var status by mutableStateOf("parsing") + this.lifecycleScope.launch(Dispatchers.IO) { + val fd = applicationContext.contentResolver.openFileDescriptor(uri, "r") + val apkInfo = ApkInfo.fromInputStream( + FileInputStream(fd?.fileDescriptor) + ) + fd?.close() + withContext(Dispatchers.Main) { + status = "waiting" + apkInfoText = "${context.getString(R.string.package_name)}: ${apkInfo.packageName}\n" + apkInfoText += "${context.getString(R.string.version_name)}: ${apkInfo.versionName}\n" + apkInfoText += "${context.getString(R.string.version_code)}: ${apkInfo.versionCode}" + } + } setContent { - var installing by remember { mutableStateOf(false) } OwnDroidTheme( sharedPref.getBoolean("material_you", true), sharedPref.getBoolean("black_theme", false) ) { AlertDialog( + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), title = { Text(stringResource(R.string.install_app)) }, onDismissRequest = { - finish() + if(status != "installing") finish() }, text = { - AnimatedVisibility(installing) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + Column { + AnimatedVisibility(status != "waiting") { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + Text(text = apkInfoText, modifier = Modifier.padding(top = 4.dp)) } }, dismissButton = { - TextButton(onClick = { finish() }) { Text(stringResource(R.string.cancel)) } + TextButton( + onClick = { finish() }, + enabled = status != "installing" + ) { + Text(stringResource(R.string.cancel)) + } }, confirmButton = { TextButton( onClick = { - installing = true + status = "installing" uriToStream(applicationContext, this.intent.data) { stream -> installPackage(applicationContext, stream) } - } + }, + enabled = status != "installing" ) { - Text(stringResource(R.string.confirm)) + Text(stringResource(R.string.install)) } }, modifier = Modifier.fillMaxWidth() @@ -72,7 +109,10 @@ class InstallAppActivity: FragmentActivity() { } val installDone by installAppDone.collectAsState() LaunchedEffect(installDone) { - if(installDone) finish() + if(installDone) { + installAppDone.value = false + finish() + } } } } diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index d4e613d..a180ab2 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -1,6 +1,5 @@ package com.bintianqi.owndroid -import android.annotation.SuppressLint import android.app.admin.DevicePolicyManager import android.content.ComponentName import android.content.Context @@ -41,7 +40,6 @@ import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.dpm.* import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.theme.OwnDroidTheme -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import java.util.Locale @@ -50,7 +48,6 @@ var backToHomeStateFlow = MutableStateFlow(false) class MainActivity : FragmentActivity() { private val showAuth = mutableStateOf(false) - @SuppressLint("UnrememberedMutableState") override fun onCreate(savedInstanceState: Bundle?) { registerActivityResult(this) enableEdgeToEdge() @@ -63,8 +60,8 @@ class MainActivity : FragmentActivity() { val locale = applicationContext.resources?.configuration?.locale zhCN = locale == Locale.SIMPLIFIED_CHINESE || locale == Locale.CHINESE || locale == Locale.CHINA setContent { - val materialYou = mutableStateOf(sharedPref.getBoolean("material_you", true)) - val blackTheme = mutableStateOf(sharedPref.getBoolean("black_theme", false)) + val materialYou = remember { mutableStateOf(sharedPref.getBoolean("material_you", true)) } + val blackTheme = remember { mutableStateOf(sharedPref.getBoolean("black_theme", false)) } OwnDroidTheme(materialYou.value, blackTheme.value) { Home(materialYou, blackTheme) if(showAuth.value) { @@ -87,7 +84,6 @@ class MainActivity : FragmentActivity() { } -@SuppressLint("UnrememberedMutableState") @ExperimentalMaterial3Api @Composable fun Home(materialYou:MutableState, blackTheme:MutableState) { @@ -97,10 +93,10 @@ fun Home(materialYou:MutableState, blackTheme:MutableState) { val receiver = ComponentName(context,Receiver::class.java) val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) val focusMgr = LocalFocusManager.current - val pkgName = mutableStateOf("") - val dialogStatus = mutableIntStateOf(0) + val pkgName = remember { mutableStateOf("") } + val dialogStatus = remember { mutableIntStateOf(0) } val backToHome by backToHomeStateFlow.collectAsState() - LaunchedEffect(Unit) { + LaunchedEffect(backToHome) { if(backToHome) { navCtrl.navigateUp(); backToHomeStateFlow.value = false } } NavHost( diff --git a/app/src/main/java/com/bintianqi/owndroid/PermissionPicker.kt b/app/src/main/java/com/bintianqi/owndroid/PermissionPicker.kt index c3e01a5..b7134a7 100644 --- a/app/src/main/java/com/bintianqi/owndroid/PermissionPicker.kt +++ b/app/src/main/java/com/bintianqi/owndroid/PermissionPicker.kt @@ -2,8 +2,11 @@ package com.bintianqi.owndroid import android.Manifest import android.os.Build.VERSION +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes 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 @@ -11,14 +14,17 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme 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.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController @@ -41,17 +47,25 @@ fun PermissionPicker(navCtrl: NavHostController) { modifier = Modifier.fillMaxSize().padding(top = paddingValues.calculateTopPadding()) ) { items(permissionList()) { - Column( + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .clickable{ - selectedPermission.value = it.first + selectedPermission.value = it.permission navCtrl.navigateUp() } - .padding(vertical = 6.dp, horizontal = 8.dp) + .padding(vertical = 8.dp, horizontal = 8.dp) ) { - Text(text = it.first) - Text(text = stringResource(it.second), modifier = Modifier.alpha(0.8F)) + Icon( + painter = painterResource(it.icon), + contentDescription = stringResource(it.label), + modifier = Modifier.padding(start = 8.dp, end = 10.dp) + ) + Column { + Text(text = stringResource(it.label)) + Text(text = it.permission, modifier = Modifier.alpha(0.8F), style = MaterialTheme.typography.bodyMedium) + } } } items(1) { Spacer(Modifier.padding(vertical = 30.dp)) } @@ -59,43 +73,48 @@ fun PermissionPicker(navCtrl: NavHostController) { } } -private fun permissionList():List>{ - val list = mutableListOf>() - list.add(Pair(Manifest.permission.READ_EXTERNAL_STORAGE, R.string.permission_READ_EXTERNAL_STORAGE)) - list.add(Pair(Manifest.permission.WRITE_EXTERNAL_STORAGE, R.string.permission_WRITE_EXTERNAL_STORAGE)) +private data class PermissionPickerItem( + val permission: String, + @StringRes val label: Int, + @DrawableRes val icon: Int +) + +private fun permissionList(): List{ + val list = mutableListOf() + list.add(PermissionPickerItem(Manifest.permission.READ_EXTERNAL_STORAGE, R.string.permission_READ_EXTERNAL_STORAGE, R.drawable.folder_fill0)) + list.add(PermissionPickerItem(Manifest.permission.WRITE_EXTERNAL_STORAGE, R.string.permission_WRITE_EXTERNAL_STORAGE, R.drawable.folder_fill0)) if(VERSION.SDK_INT >= 33) { - list.add(Pair(Manifest.permission.READ_MEDIA_AUDIO, R.string.permission_READ_MEDIA_AUDIO)) - list.add(Pair(Manifest.permission.READ_MEDIA_VIDEO, R.string.permission_READ_MEDIA_VIDEO)) - list.add(Pair(Manifest.permission.READ_MEDIA_IMAGES, R.string.permission_READ_MEDIA_IMAGES)) + list.add(PermissionPickerItem(Manifest.permission.READ_MEDIA_AUDIO, R.string.permission_READ_MEDIA_AUDIO, R.drawable.music_note_fill0)) + list.add(PermissionPickerItem(Manifest.permission.READ_MEDIA_VIDEO, R.string.permission_READ_MEDIA_VIDEO, R.drawable.movie_fill0)) + list.add(PermissionPickerItem(Manifest.permission.READ_MEDIA_IMAGES, R.string.permission_READ_MEDIA_IMAGES, R.drawable.image_fill0)) } - list.add(Pair(Manifest.permission.CAMERA, R.string.permission_CAMERA)) - list.add(Pair(Manifest.permission.RECORD_AUDIO, R.string.permission_RECORD_AUDIO)) - list.add(Pair(Manifest.permission.ACCESS_COARSE_LOCATION, R.string.permission_ACCESS_COARSE_LOCATION)) - list.add(Pair(Manifest.permission.ACCESS_FINE_LOCATION, R.string.permission_ACCESS_FINE_LOCATION)) + list.add(PermissionPickerItem(Manifest.permission.CAMERA, R.string.permission_CAMERA, R.drawable.photo_camera_fill0)) + list.add(PermissionPickerItem(Manifest.permission.RECORD_AUDIO, R.string.permission_RECORD_AUDIO, R.drawable.mic_fill0)) + list.add(PermissionPickerItem(Manifest.permission.ACCESS_COARSE_LOCATION, R.string.permission_ACCESS_COARSE_LOCATION, R.drawable.location_on_fill0)) + list.add(PermissionPickerItem(Manifest.permission.ACCESS_FINE_LOCATION, R.string.permission_ACCESS_FINE_LOCATION, R.drawable.location_on_fill0)) if(VERSION.SDK_INT >= 29) { - list.add(Pair(Manifest.permission.ACCESS_BACKGROUND_LOCATION, R.string.permission_ACCESS_BACKGROUND_LOCATION)) + list.add(PermissionPickerItem(Manifest.permission.ACCESS_BACKGROUND_LOCATION, R.string.permission_ACCESS_BACKGROUND_LOCATION, R.drawable.location_on_fill0)) } - list.add(Pair(Manifest.permission.READ_CONTACTS, R.string.permission_READ_CONTACTS)) - list.add(Pair(Manifest.permission.WRITE_CONTACTS, R.string.permission_WRITE_CONTACTS)) - list.add(Pair(Manifest.permission.READ_CALENDAR, R.string.permission_READ_CALENDAR)) - list.add(Pair(Manifest.permission.WRITE_CALENDAR, R.string.permission_WRITE_CALENDAR)) - list.add(Pair(Manifest.permission.CALL_PHONE, R.string.permission_CALL_PHONE)) - list.add(Pair(Manifest.permission.READ_PHONE_STATE, R.string.permission_READ_PHONE_STATE)) - list.add(Pair(Manifest.permission.READ_SMS, R.string.permission_READ_SMS)) - list.add(Pair(Manifest.permission.RECEIVE_SMS, R.string.permission_RECEIVE_SMS)) - list.add(Pair(Manifest.permission.SEND_SMS, R.string.permission_SEND_SMS)) - list.add(Pair(Manifest.permission.READ_CALL_LOG, R.string.permission_READ_CALL_LOG)) - list.add(Pair(Manifest.permission.WRITE_CALL_LOG, R.string.permission_WRITE_CALL_LOG)) - list.add(Pair(Manifest.permission.BODY_SENSORS, R.string.permission_BODY_SENSORS)) + list.add(PermissionPickerItem(Manifest.permission.READ_CONTACTS, R.string.permission_READ_CONTACTS, R.drawable.contacts_fill0)) + list.add(PermissionPickerItem(Manifest.permission.WRITE_CONTACTS, R.string.permission_WRITE_CONTACTS, R.drawable.contacts_fill0)) + list.add(PermissionPickerItem(Manifest.permission.READ_CALENDAR, R.string.permission_READ_CALENDAR, R.drawable.calendar_month_fill0)) + list.add(PermissionPickerItem(Manifest.permission.WRITE_CALENDAR, R.string.permission_WRITE_CALENDAR, R.drawable.calendar_month_fill0)) + list.add(PermissionPickerItem(Manifest.permission.CALL_PHONE, R.string.permission_CALL_PHONE, R.drawable.call_fill0)) + list.add(PermissionPickerItem(Manifest.permission.READ_PHONE_STATE, R.string.permission_READ_PHONE_STATE, R.drawable.mobile_phone_fill0)) + list.add(PermissionPickerItem(Manifest.permission.READ_SMS, R.string.permission_READ_SMS, R.drawable.sms_fill0)) + list.add(PermissionPickerItem(Manifest.permission.RECEIVE_SMS, R.string.permission_RECEIVE_SMS, R.drawable.sms_fill0)) + list.add(PermissionPickerItem(Manifest.permission.SEND_SMS, R.string.permission_SEND_SMS, R.drawable.sms_fill0)) + list.add(PermissionPickerItem(Manifest.permission.READ_CALL_LOG, R.string.permission_READ_CALL_LOG, R.drawable.call_log_fill0)) + list.add(PermissionPickerItem(Manifest.permission.WRITE_CALL_LOG, R.string.permission_WRITE_CALL_LOG, R.drawable.call_log_fill0)) + list.add(PermissionPickerItem(Manifest.permission.BODY_SENSORS, R.string.permission_BODY_SENSORS, R.drawable.sensors_fill0)) if(VERSION.SDK_INT >= 33) { - list.add(Pair(Manifest.permission.BODY_SENSORS_BACKGROUND, R.string.permission_BODY_SENSORS_BACKGROUND)) + list.add(PermissionPickerItem(Manifest.permission.BODY_SENSORS_BACKGROUND, R.string.permission_BODY_SENSORS_BACKGROUND, R.drawable.sensors_fill0)) } if(VERSION.SDK_INT > 29) { - list.add(Pair(Manifest.permission.ACTIVITY_RECOGNITION, R.string.permission_ACTIVITY_RECOGNITION)) + list.add(PermissionPickerItem(Manifest.permission.ACTIVITY_RECOGNITION, R.string.permission_ACTIVITY_RECOGNITION, R.drawable.history_fill0)) } if(VERSION.SDK_INT >= 33) { - list.add(Pair(Manifest.permission.POST_NOTIFICATIONS, R.string.permission_POST_NOTIFICATIONS)) + list.add(PermissionPickerItem(Manifest.permission.POST_NOTIFICATIONS, R.string.permission_POST_NOTIFICATIONS, R.drawable.notifications_fill0)) } - //list.add(Pair(Manifest.permission., R.string.)) return list } diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt index 07e3597..5bb2b60 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt @@ -1,5 +1,7 @@ package com.bintianqi.owndroid +import android.annotation.SuppressLint +import android.app.NotificationManager import android.app.admin.DeviceAdminReceiver import android.app.admin.DevicePolicyManager import android.content.BroadcastReceiver @@ -10,6 +12,7 @@ import android.content.pm.PackageInstaller.* import android.util.Log import android.widget.Toast import androidx.activity.ComponentActivity +import androidx.compose.ui.platform.LocalContext import com.bintianqi.owndroid.dpm.isDeviceOwner import com.bintianqi.owndroid.dpm.isProfileOwner import kotlinx.coroutines.flow.MutableStateFlow @@ -61,3 +64,16 @@ class PackageInstallerReceiver:BroadcastReceiver(){ } } } + +class StopLockTaskModeReceiver: BroadcastReceiver() { + @SuppressLint("NewApi") + override fun onReceive(context: Context, intent: Intent) { + val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val receiver = ComponentName(context,Receiver::class.java) + val packages = dpm.getLockTaskPackages(receiver) + dpm.setLockTaskPackages(receiver, arrayOf()) + dpm.setLockTaskPackages(receiver, packages) + val nm = context.getSystemService(ComponentActivity.NOTIFICATION_SERVICE) as NotificationManager + nm.cancel(1) + } +} diff --git a/app/src/main/java/com/bintianqi/owndroid/Setting.kt b/app/src/main/java/com/bintianqi/owndroid/Setting.kt index a5ef2f9..c9da38a 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Setting.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Setting.kt @@ -4,13 +4,16 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build.VERSION +import android.widget.Toast import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -41,8 +44,10 @@ fun AppSetting(navCtrl:NavHostController, materialYou: MutableState, bl modifier = Modifier.padding(top = it.calculateTopPadding()) ) { composable(route = "Home") { Home(localNavCtrl) } + composable(route = "Options") { Options() } composable(route = "Theme") { ThemeSettings(materialYou, blackTheme) } composable(route = "Auth") { AuthSettings() } + composable(route = "Automation") { Automation() } composable(route = "About") { About() } } } @@ -51,12 +56,26 @@ fun AppSetting(navCtrl:NavHostController, materialYou: MutableState, bl @Composable private fun Home(navCtrl: NavHostController) { Column(modifier = Modifier.fillMaxSize()) { + SubPageItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("Options") } SubPageItem(R.string.theme, "", R.drawable.format_paint_fill0) { navCtrl.navigate("Theme") } SubPageItem(R.string.security, "", R.drawable.lock_fill0) { navCtrl.navigate("Auth") } + SubPageItem(R.string.automation_api, "", R.drawable.apps_fill0) { navCtrl.navigate("Automation") } SubPageItem(R.string.about, "", R.drawable.info_fill0) { navCtrl.navigate("About") } } } +@Composable +private fun Options() { + val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) + Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { + SwitchItem( + R.string.show_dangerous_features, "", R.drawable.warning_fill0, + { sharedPref.getBoolean("dangerous_features", false) }, + { sharedPref.edit().putBoolean("dangerous_features", it).apply() } + ) + } +} + @Composable private fun ThemeSettings(materialYou:MutableState, blackTheme:MutableState) { val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) @@ -122,6 +141,36 @@ private fun AuthSettings() { } } +@Composable +private fun Automation() { + val context = LocalContext.current + val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) + Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { + Spacer(Modifier.padding(vertical = 10.dp)) + Text(text = stringResource(R.string.automation_api), style = typography.headlineLarge) + Spacer(Modifier.padding(vertical = 5.dp)) + var key by remember { mutableStateOf("") } + TextField( + value = key, onValueChange = { key = it }, label = { Text("Key")}, + modifier = Modifier.fillMaxWidth() + ) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + sharedPref.edit().putString("automation_key", key).apply() + Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + } + ) { + Text(stringResource(R.string.apply)) + } + SwitchItem( + R.string.automation_debug, "", null, + { sharedPref.getBoolean("automation_debug", false) }, + { sharedPref.edit().putBoolean("automation_debug", it).apply() } + ) + } +} + @Composable private fun About() { val context = LocalContext.current diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 9085970..d966696 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -1,8 +1,11 @@ package com.bintianqi.owndroid +import android.Manifest import android.app.admin.DevicePolicyManager import android.content.* +import android.content.pm.PackageManager import android.net.Uri +import android.os.Build.VERSION import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher @@ -55,6 +58,10 @@ fun Set.toText(): String{ return output } +fun MutableList.toggle(status: Boolean, element: Int) { + if(status) add(element) else remove(element) +} + fun writeClipBoard(context: Context, string: String):Boolean{ val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager try { @@ -65,6 +72,7 @@ fun writeClipBoard(context: Context, string: String):Boolean{ return true } +lateinit var requestPermission: ActivityResultLauncher fun registerActivityResult(context: ComponentActivity){ getFile = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> @@ -83,4 +91,20 @@ fun registerActivityResult(context: ComponentActivity){ backToHomeStateFlow.value = true } } + requestPermission = context.registerForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted.value = it } +} + +val permissionGranted = MutableStateFlow(null) + +suspend fun prepareForNotification(context: Context, action: ()->Unit) { + if(VERSION.SDK_INT >= 33) { + if(context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + action() + } else { + requestPermission.launch(Manifest.permission.POST_NOTIFICATIONS) + permissionGranted.collect { if(it == true) action() } + } + } else { + action() + } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/ApplicationManage.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/ApplicationManage.kt index 8922642..93ee438 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/ApplicationManage.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/ApplicationManage.kt @@ -158,6 +158,7 @@ fun ApplicationManage(navCtrl:NavHostController, pkgName: MutableState, composable(route = "Home") { Home(localNavCtrl, pkgName.value, dialogStatus, clearAppDataDialog, defaultDialerAppDialog, enableSystemAppDialog) } + composable(route = "AlwaysOnVpn") { AlwaysOnVPNPackage(pkgName.value) } composable(route = "UserControlDisabled") { UserCtrlDisabledPkg(pkgName.value) } composable(route = "PermissionManage") { PermissionManage(pkgName.value, navCtrl) } composable(route = "CrossProfilePackage") { CrossProfilePkg(pkgName.value) } @@ -213,8 +214,8 @@ private fun Home( if(VERSION.SDK_INT>=24 && (isDeviceOwner(dpm) || isProfileOwner(dpm))) { val getSuspendStatus = { try{ dpm.isPackageSuspended(receiver, pkgName) } - catch(e:NameNotFoundException) { false } - catch(e:IllegalArgumentException) { false } + catch(e:NameNotFoundException) { false } + catch(e:IllegalArgumentException) { false } } SwitchItem( title = R.string.suspend, desc = "", icon = R.drawable.block_fill0, @@ -255,26 +256,7 @@ private fun Home( ) } if(VERSION.SDK_INT>=24 && (isDeviceOwner(dpm) || isProfileOwner(dpm))) { - val setAlwaysOnVpn: (Boolean)->Unit = { - try { - dpm.setAlwaysOnVpnPackage(receiver, pkgName, it) - } catch(e: UnsupportedOperationException) { - Toast.makeText(context, R.string.unsupported, Toast.LENGTH_SHORT).show() - } catch(e: NameNotFoundException) { - Toast.makeText(context, R.string.not_installed, Toast.LENGTH_SHORT).show() - } - } - SwitchItem( - title = R.string.always_on_vpn, desc = "", icon = R.drawable.vpn_key_fill0, - getState = { pkgName == dpm.getAlwaysOnVpnPackage(receiver) }, - onCheckedChange = setAlwaysOnVpn, - onClickBlank = { - dialogGetStatus = { pkgName == dpm.getAlwaysOnVpnPackage(receiver) } - dialogConfirmButtonAction = { setAlwaysOnVpn(true) } - dialogDismissButtonAction = { setAlwaysOnVpn(false) } - dialogStatus.intValue = 4 - } - ) + SubPageItem(R.string.always_on_vpn, "", R.drawable.vpn_key_fill0) { navCtrl.navigate("AlwaysOnVpn") } } if((VERSION.SDK_INT>=33&&isProfileOwner(dpm))||(VERSION.SDK_INT>=30&&isDeviceOwner(dpm))) { SubPageItem(R.string.ucd, "", R.drawable.do_not_touch_fill0) { navCtrl.navigate("UserControlDisabled") } @@ -318,6 +300,50 @@ private fun Home( } } +@SuppressLint("NewApi") +@Composable +fun AlwaysOnVPNPackage(pkgName: String) { + val context = LocalContext.current + val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val receiver = ComponentName(context,Receiver::class.java) + var lockdown by remember { mutableStateOf(false) } + var pkg by remember { mutableStateOf("") } + val refresh = { pkg = dpm.getAlwaysOnVpnPackage(receiver) } + LaunchedEffect(Unit) { refresh() } + val setAlwaysOnVpn: (String?, Boolean)->Unit = { vpnPkg: String?, lockdownEnabled: Boolean -> + try { + dpm.setAlwaysOnVpnPackage(receiver, vpnPkg, lockdownEnabled) + Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + } catch(e: UnsupportedOperationException) { + Toast.makeText(context, R.string.unsupported, Toast.LENGTH_SHORT).show() + } catch(e: NameNotFoundException) { + Toast.makeText(context, R.string.not_installed, Toast.LENGTH_SHORT).show() + } + } + Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { + Spacer(Modifier.padding(vertical = 10.dp)) + Text(text = stringResource(R.string.always_on_vpn), style = typography.headlineLarge, modifier = Modifier.padding(8.dp)) + Spacer(Modifier.padding(vertical = 5.dp)) + Text(text = stringResource(R.string.current_app_is) + pkg, modifier = Modifier.padding(8.dp)) + SwitchItem(R.string.enable_lockdown, "", null, { lockdown }, { lockdown = it }) + Spacer(Modifier.padding(vertical = 5.dp)) + Button( + onClick = { setAlwaysOnVpn(pkgName, lockdown); refresh() }, + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp) + ) { + Text(stringResource(R.string.apply)) + } + Spacer(Modifier.padding(vertical = 5.dp)) + Button( + onClick = { setAlwaysOnVpn(null, false); refresh() }, + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp) + ) { + Text(stringResource(R.string.clear_current_config)) + } + Spacer(Modifier.padding(vertical = 30.dp)) + } +} + @SuppressLint("NewApi") @Composable private fun UserCtrlDisabledPkg(pkgName:String) { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt index 39a5ae8..82ac61e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt @@ -11,11 +11,15 @@ import android.app.admin.DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT import android.app.admin.DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED import android.app.admin.DevicePolicyManager.PERSONAL_APPS_NOT_SUSPENDED import android.app.admin.DevicePolicyManager.PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT +import android.app.admin.DevicePolicyManager.WIPE_EUICC +import android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE +import android.app.admin.DevicePolicyManager.WIPE_SILENTLY import android.content.* import android.os.Binder import android.os.Build.VERSION import android.widget.Toast import androidx.activity.ComponentActivity +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -26,13 +30,17 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults 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.TextButton 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 @@ -80,6 +88,7 @@ fun ManagedProfile(navCtrl: NavHostController) { composable(route = "CreateWorkProfile") { CreateWorkProfile() } composable(route = "SuspendPersonalApp") { SuspendPersonalApp() } composable(route = "IntentFilter") { IntentFilter() } + composable(route = "DeleteWorkProfile") { DeleteWorkProfile() } } } } @@ -97,7 +106,7 @@ private fun Home(navCtrl: NavHostController) { style = typography.headlineLarge, modifier = Modifier.padding(top = 8.dp, bottom = 5.dp, start = 15.dp) ) - if(VERSION.SDK_INT >= 30&&isProfileOwner(dpm) && dpm.isManagedProfile(receiver)) { + if(VERSION.SDK_INT >= 30 && isProfileOwner(dpm) && dpm.isManagedProfile(receiver)) { SubPageItem(R.string.org_owned_work_profile, "", R.drawable.corporate_fare_fill0) { navCtrl.navigate("OrgOwnedWorkProfile") } } if(VERSION.SDK_INT<24 || (VERSION.SDK_INT>=24 && dpm.isProvisioningAllowed(ACTION_PROVISION_MANAGED_PROFILE))) { @@ -106,9 +115,12 @@ private fun Home(navCtrl: NavHostController) { if(dpm.isOrgProfile(receiver)) { SubPageItem(R.string.suspend_personal_app, "", R.drawable.block_fill0) { navCtrl.navigate("SuspendPersonalApp") } } - if(isProfileOwner(dpm) && (VERSION.SDK_INT<24 || (VERSION.SDK_INT>=24 && dpm.isManagedProfile(receiver)))) { + if(isProfileOwner(dpm) && (VERSION.SDK_INT < 24 || (VERSION.SDK_INT >= 24 && dpm.isManagedProfile(receiver)))) { SubPageItem(R.string.intent_filter, "", R.drawable.filter_alt_fill0) { navCtrl.navigate("IntentFilter") } } + if(isProfileOwner(dpm) && (VERSION.SDK_INT < 24 || (VERSION.SDK_INT >= 24 && dpm.isManagedProfile(receiver)))) { + SubPageItem(R.string.delete_work_profile, "", R.drawable.delete_forever_fill0) { navCtrl.navigate("DeleteWorkProfile") } + } Spacer(Modifier.padding(vertical = 30.dp)) } } @@ -265,3 +277,81 @@ private fun IntentFilter() { } } } + +@Composable +private fun DeleteWorkProfile() { + val context = LocalContext.current + val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val focusMgr = LocalFocusManager.current + var warning by remember { mutableStateOf(false) } + var externalStorage by remember { mutableStateOf(false) } + var euicc by remember { mutableStateOf(false) } + var silent by remember { mutableStateOf(false) } + var reason by remember { mutableStateOf("") } + Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { + Spacer(Modifier.padding(vertical = 10.dp)) + Text( + text = stringResource(R.string.delete_work_profile), + style = typography.headlineLarge, + modifier = Modifier.padding(6.dp),color = colorScheme.error + ) + Spacer(Modifier.padding(vertical = 5.dp)) + CheckBoxItem(stringResource(R.string.wipe_external_storage), externalStorage, { externalStorage = it }) + if(VERSION.SDK_INT >= 28) { CheckBoxItem(stringResource(R.string.wipe_euicc), euicc, { euicc = it }) } + if(VERSION.SDK_INT >= 29) { CheckBoxItem(stringResource(R.string.wipe_silently), silent, { silent = it }) } + AnimatedVisibility(!silent && VERSION.SDK_INT >= 28) { + OutlinedTextField( + value = reason, onValueChange = { reason = it }, + label = { Text(stringResource(R.string.reason)) }, + modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) + ) + } + Spacer(Modifier.padding(vertical = 5.dp)) + Button( + onClick = { + focusMgr.clearFocus() + warning = true + }, + colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError), + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.delete)) + } + Spacer(Modifier.padding(vertical = 30.dp)) + } + if(warning) { + LaunchedEffect(Unit) { silent = reason == "" } + AlertDialog( + title = { + Text(text = stringResource(R.string.warning), color = colorScheme.error) + }, + text = { + Text(text = stringResource(R.string.wipe_work_profile_warning), color = colorScheme.error) + }, + onDismissRequest = { warning = false }, + confirmButton = { + TextButton( + onClick = { + var flag = 0 + if(externalStorage) { flag += WIPE_EXTERNAL_STORAGE } + if(euicc && VERSION.SDK_INT >= 28) { flag += WIPE_EUICC } + if(silent && VERSION.SDK_INT >= 29) { flag += WIPE_SILENTLY } + if(VERSION.SDK_INT >= 28 && !silent) { + dpm.wipeData(flag, reason) + } else { + dpm.wipeData(flag) + } + }, + colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) + ) { + Text(stringResource(R.string.confirm)) + } + }, + dismissButton = { + TextButton(onClick = { warning = false }) { + Text(stringResource(R.string.cancel)) + } + } + ) + } +} 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 222773e..7b4c38f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -36,6 +36,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.Receiver +import com.bintianqi.owndroid.backToHomeStateFlow import com.bintianqi.owndroid.ui.* import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -119,7 +120,7 @@ private fun Home(localNavCtrl:NavHostController,listScrollState:ScrollState) { SubPageItem(R.string.enrollment_specific_id, "", R.drawable.id_card_fill0) { localNavCtrl.navigate("SpecificID") } } if(isDeviceOwner(dpm) || isProfileOwner(dpm)) { - SubPageItem(R.string.disable_account_management, "", R.drawable.account_circle_fill0) { localNavCtrl.navigate("NoManagementAccount") } + SubPageItem(R.string.disable_account_management, "", R.drawable.account_circle_fill0) { localNavCtrl.navigate("DisableAccountManagement") } } if(VERSION.SDK_INT >= 24 && (isDeviceOwner(dpm) || dpm.isOrgProfile(receiver))) { SubPageItem(R.string.device_owner_lock_screen_info, "", R.drawable.screen_lock_portrait_fill0) { localNavCtrl.navigate("LockScreenInfo") } @@ -178,8 +179,8 @@ private fun DeviceAdmin() { val context = LocalContext.current val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager val receiver = ComponentName(context,Receiver::class.java) - val co = rememberCoroutineScope() var showDeactivateButton by remember { mutableStateOf(dpm.isAdminActive(receiver)) } + var deactivateDialog by remember { mutableStateOf(false) } Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.device_admin), style = typography.headlineLarge) @@ -187,10 +188,7 @@ private fun DeviceAdmin() { Spacer(Modifier.padding(vertical = 5.dp)) AnimatedVisibility(showDeactivateButton) { Button( - onClick = { - dpm.removeActiveAdmin(receiver) - co.launch{ delay(400); showDeactivateButton = dpm.isAdminActive(receiver) } - }, + onClick = { deactivateDialog = true }, enabled = !isProfileOwner(dpm) && !isDeviceOwner(dpm), colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError) ) { @@ -210,6 +208,35 @@ private fun DeviceAdmin() { } } } + if(deactivateDialog) { + val co = rememberCoroutineScope() + AlertDialog( + title = { Text(stringResource(R.string.deactivate)) }, + onDismissRequest = { deactivateDialog = false }, + dismissButton = { + TextButton( + onClick = { deactivateDialog = false } + ) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + dpm.removeActiveAdmin(receiver) + co.launch{ + delay(300) + deactivateDialog = false + showDeactivateButton = dpm.isAdminActive(receiver) + backToHomeStateFlow.value = !dpm.isAdminActive(receiver) + } + } + ) { + Text(stringResource(R.string.confirm)) + } + } + ) + } } @Composable @@ -218,19 +245,16 @@ private fun ProfileOwner() { val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager val receiver = ComponentName(context,Receiver::class.java) var showDeactivateButton by remember { mutableStateOf(isProfileOwner(dpm)) } + var deactivateDialog by remember { mutableStateOf(false) } Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.profile_owner), style = typography.headlineLarge) Text(stringResource(if(isProfileOwner(dpm)) R.string.activated else R.string.deactivated), style = typography.titleLarge) Spacer(Modifier.padding(vertical = 5.dp)) - if(VERSION.SDK_INT>=24) { + if(VERSION.SDK_INT >= 24) { AnimatedVisibility(showDeactivateButton) { - val co = rememberCoroutineScope() Button( - onClick = { - dpm.clearProfileOwner(receiver) - co.launch { delay(400); showDeactivateButton=isProfileOwner(dpm) } - }, + onClick = { deactivateDialog = true }, enabled = !dpm.isManagedProfile(receiver), colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError) ) { @@ -247,14 +271,43 @@ private fun ProfileOwner() { } } } + if(deactivateDialog && VERSION.SDK_INT >= 24) { + val co = rememberCoroutineScope() + AlertDialog( + title = { Text(stringResource(R.string.deactivate)) }, + onDismissRequest = { deactivateDialog = false }, + dismissButton = { + TextButton( + onClick = { deactivateDialog = false } + ) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + dpm.clearProfileOwner(receiver) + co.launch{ + delay(300) + deactivateDialog = false + showDeactivateButton = isProfileOwner(dpm) + backToHomeStateFlow.value = !isProfileOwner(dpm) + } + } + ) { + Text(stringResource(R.string.confirm)) + } + } + ) + } } @Composable private fun DeviceOwner() { val context = LocalContext.current val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val co = rememberCoroutineScope() var showDeactivateButton by remember { mutableStateOf(isDeviceOwner(dpm)) } + var deactivateDialog by remember { mutableStateOf(false) } Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.device_owner), style = typography.headlineLarge) @@ -262,10 +315,7 @@ private fun DeviceOwner() { Spacer(Modifier.padding(vertical = 5.dp)) AnimatedVisibility(showDeactivateButton) { Button( - onClick = { - dpm.clearDeviceOwnerApp(context.packageName) - co.launch{ delay(400); showDeactivateButton=isDeviceOwner(dpm) } - }, + onClick = { deactivateDialog = true }, colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError) ) { Text(text = stringResource(R.string.deactivate)) @@ -280,6 +330,35 @@ private fun DeviceOwner() { } } } + if(deactivateDialog) { + val co = rememberCoroutineScope() + AlertDialog( + title = { Text(stringResource(R.string.deactivate)) }, + onDismissRequest = { deactivateDialog = false }, + dismissButton = { + TextButton( + onClick = { deactivateDialog = false } + ) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + dpm.clearDeviceOwnerApp(context.packageName) + co.launch{ + delay(300) + deactivateDialog = false + showDeactivateButton = isDeviceOwner(dpm) + backToHomeStateFlow.value = !isDeviceOwner(dpm) + } + } + ) { + Text(stringResource(R.string.confirm)) + } + } + ) + } } @Composable diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/SystemManager.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/SystemManager.kt index 12facc7..0045898 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/SystemManager.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/SystemManager.kt @@ -1,6 +1,11 @@ package com.bintianqi.owndroid.dpm import android.annotation.SuppressLint +import android.app.ActivityOptions +import android.app.AlertDialog +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyManager.FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY import android.app.admin.DevicePolicyManager.InstallSystemUpdateCallback @@ -8,7 +13,6 @@ import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_BLOCK_ACTIVITY_ST import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_GLOBAL_ACTIONS import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_HOME import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_KEYGUARD -import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_NONE import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_SYSTEM_INFO @@ -36,7 +40,6 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.net.Uri -import android.os.Binder import android.os.Build.VERSION import android.os.UserManager import android.util.Log @@ -77,6 +80,7 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf 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 @@ -87,6 +91,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.core.app.NotificationCompat import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -94,9 +99,12 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.Receiver +import com.bintianqi.owndroid.StopLockTaskModeReceiver import com.bintianqi.owndroid.fileUriFlow import com.bintianqi.owndroid.getFile +import com.bintianqi.owndroid.prepareForNotification import com.bintianqi.owndroid.toText +import com.bintianqi.owndroid.toggle import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.Information @@ -105,9 +113,11 @@ import com.bintianqi.owndroid.ui.SubPageItem import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.ui.TopBar import com.bintianqi.owndroid.uriToStream +import kotlinx.coroutines.launch import java.util.Date import java.util.TimeZone import java.util.concurrent.Executors +import kotlin.math.pow @Composable fun SystemManage(navCtrl:NavHostController) { @@ -144,7 +154,7 @@ fun SystemManage(navCtrl:NavHostController) { composable(route = "PermissionPolicy") { PermissionPolicy() } composable(route = "MTEPolicy") { MTEPolicy() } composable(route = "NearbyStreamingPolicy") { NearbyStreamingPolicy() } - composable(route = "LockTaskFeatures") { LockTaskFeatures() } + composable(route = "LockTaskMode") { LockTaskMode() } composable(route = "CaCert") { CaCert() } composable(route = "SecurityLogs") { SecurityLogs() } composable(route = "SystemUpdatePolicy") { SysUpdatePolicy() } @@ -166,6 +176,8 @@ private fun Home(navCtrl: NavHostController, scrollState: ScrollState, rebootDia val context = LocalContext.current val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager val receiver = ComponentName(context, Receiver::class.java) + val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) + val dangerousFeatures = sharedPref.getBoolean("dangerous_features", false) Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) { Text( text = stringResource(R.string.system_manage), @@ -196,7 +208,7 @@ private fun Home(navCtrl: NavHostController, scrollState: ScrollState, rebootDia SubPageItem(R.string.nearby_streaming_policy, "", R.drawable.share_fill0) { navCtrl.navigate("NearbyStreamingPolicy") } } if(VERSION.SDK_INT >= 28 && isDeviceOwner(dpm)) { - SubPageItem(R.string.lock_task_feature, "", R.drawable.lock_fill0) { navCtrl.navigate("LockTaskFeatures") } + SubPageItem(R.string.lock_task_mode, "", R.drawable.lock_fill0) { navCtrl.navigate("LockTaskMode") } } if(isDeviceOwner(dpm) || isProfileOwner(dpm)) { SubPageItem(R.string.ca_cert, "", R.drawable.license_fill0) { navCtrl.navigate("CaCert") } @@ -213,8 +225,8 @@ private fun Home(navCtrl: NavHostController, scrollState: ScrollState, rebootDia if(VERSION.SDK_INT >= 30 && (isDeviceOwner(dpm) || dpm.isOrgProfile(receiver))) { SubPageItem(R.string.frp_policy, "", R.drawable.device_reset_fill0) { navCtrl.navigate("FRP") } } - if(dpm.isAdminActive(receiver)) { - SubPageItem(R.string.wipe_data, "", R.drawable.warning_fill0) { navCtrl.navigate("WipeData") } + if(dangerousFeatures && dpm.isAdminActive(receiver) && !(VERSION.SDK_INT >= 24 && isProfileOwner(dpm) && dpm.isManagedProfile(receiver))) { + SubPageItem(R.string.wipe_data, "", R.drawable.device_reset_fill0) { navCtrl.navigate("WipeData") } } Spacer(Modifier.padding(vertical = 30.dp)) LaunchedEffect(Unit) { fileUriFlow.value = Uri.parse("") } @@ -642,41 +654,30 @@ private fun NearbyStreamingPolicy() { @SuppressLint("NewApi") @Composable -private fun LockTaskFeatures() { +private fun LockTaskMode() { val context = LocalContext.current val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager val receiver = ComponentName(context,Receiver::class.java) val focusMgr = LocalFocusManager.current + val coroutine = rememberCoroutineScope() Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - val lockTaskPolicyList = mutableListOf( - LOCK_TASK_FEATURE_NONE, - LOCK_TASK_FEATURE_SYSTEM_INFO, - LOCK_TASK_FEATURE_NOTIFICATIONS, - LOCK_TASK_FEATURE_HOME, - LOCK_TASK_FEATURE_OVERVIEW, - LOCK_TASK_FEATURE_GLOBAL_ACTIONS, - LOCK_TASK_FEATURE_KEYGUARD - ) - var sysInfo by remember { mutableStateOf(false) } - var notifications by remember { mutableStateOf(false) } - var home by remember { mutableStateOf(false) } - var overview by remember { mutableStateOf(false) } - var globalAction by remember { mutableStateOf(false) } - var keyGuard by remember { mutableStateOf(false) } - var blockAct by remember { mutableStateOf(false) } - if(VERSION.SDK_INT >= 30) { lockTaskPolicyList.add(LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK) } - var inited by remember { mutableStateOf(false) } + val lockTaskFeatures = remember { mutableStateListOf() } var custom by remember { mutableStateOf(false) } val refreshFeature = { var calculate = dpm.getLockTaskFeatures(receiver) - if(calculate!=0) { - if(VERSION.SDK_INT >= 30 && calculate-lockTaskPolicyList[7] >= 0) { blockAct= true; calculate -= lockTaskPolicyList[7] } - if(calculate-lockTaskPolicyList[6] >= 0) { keyGuard = true; calculate -= lockTaskPolicyList[6] } - if(calculate-lockTaskPolicyList[5] >= 0) { globalAction= true; calculate -= lockTaskPolicyList[5] } - if(calculate-lockTaskPolicyList[4] >= 0) { overview= true; calculate -= lockTaskPolicyList[4] } - if(calculate-lockTaskPolicyList[3] >= 0) { home= true; calculate -= lockTaskPolicyList[3] } - if(calculate-lockTaskPolicyList[2] >= 0) { notifications= true; calculate -= lockTaskPolicyList[2] } - if(calculate-lockTaskPolicyList[1] >= 0) { sysInfo= true; calculate -= lockTaskPolicyList[1] } + lockTaskFeatures.clear() + if(calculate != 0) { + var sq = 10 + while(sq >= 1) { + val current = (2).toDouble().pow(sq.toDouble()).toInt() + if(calculate - current >= 0) { + lockTaskFeatures += current + calculate -= current + } + sq-- + } + if(calculate - 1 >= 0) { lockTaskFeatures += 1 } + custom = true }else{ custom = false } @@ -684,57 +685,87 @@ private fun LockTaskFeatures() { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.lock_task_feature), style = typography.headlineLarge) Spacer(Modifier.padding(vertical = 5.dp)) - if(!inited) { refreshFeature(); custom=dpm.getLockTaskFeatures(receiver)!=0; inited= true } + LaunchedEffect(Unit) { refreshFeature() } RadioButtonItem(stringResource(R.string.disable_all), !custom, { custom = false }) RadioButtonItem(stringResource(R.string.custom), custom, { custom = true }) AnimatedVisibility(custom) { Column { - CheckBoxItem(stringResource(R.string.ltf_sys_info), sysInfo, { sysInfo = it }) - CheckBoxItem(stringResource(R.string.ltf_notifications), notifications, { notifications = it }) - CheckBoxItem(stringResource(R.string.ltf_home), home, { home = it }) - CheckBoxItem(stringResource(R.string.ltf_overview), overview, { overview = it }) - CheckBoxItem(stringResource(R.string.ltf_global_actions), globalAction, { globalAction = it }) - CheckBoxItem(stringResource(R.string.ltf_keyguard), keyGuard, { keyGuard = it }) - if(VERSION.SDK_INT >= 30) { CheckBoxItem(stringResource(R.string.ltf_block_activity_start_in_task), blockAct, { blockAct = it }) } + CheckBoxItem( + stringResource(R.string.ltf_sys_info), + LOCK_TASK_FEATURE_SYSTEM_INFO in lockTaskFeatures, + { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_SYSTEM_INFO) } + ) + CheckBoxItem( + stringResource(R.string.ltf_notifications), + LOCK_TASK_FEATURE_NOTIFICATIONS in lockTaskFeatures, + { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_NOTIFICATIONS) } + ) + CheckBoxItem( + stringResource(R.string.ltf_home), + LOCK_TASK_FEATURE_HOME in lockTaskFeatures, + { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_HOME) } + ) + CheckBoxItem( + stringResource(R.string.ltf_overview), + LOCK_TASK_FEATURE_OVERVIEW in lockTaskFeatures, + { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_OVERVIEW) } + ) + CheckBoxItem( + stringResource(R.string.ltf_global_actions), + LOCK_TASK_FEATURE_GLOBAL_ACTIONS in lockTaskFeatures, + { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_GLOBAL_ACTIONS) } + ) + CheckBoxItem( + stringResource(R.string.ltf_keyguard), + LOCK_TASK_FEATURE_KEYGUARD in lockTaskFeatures, + { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_KEYGUARD) } + ) + if(VERSION.SDK_INT >= 30) { + CheckBoxItem( + stringResource(R.string.ltf_block_activity_start_in_task), + LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK in lockTaskFeatures, + { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK) } + ) + } } } Button( modifier = Modifier.fillMaxWidth(), onClick = { - var result = lockTaskPolicyList[0] + var result = 0 if(custom) { - if(blockAct&&VERSION.SDK_INT >= 30) { result += lockTaskPolicyList[7] } - if(keyGuard) { result += lockTaskPolicyList[6] } - if(globalAction) { result += lockTaskPolicyList[5] } - if(overview) { result += lockTaskPolicyList[4] } - if(home) { result += lockTaskPolicyList[3] } - if(notifications) { result += lockTaskPolicyList[2] } - if(sysInfo) { result += lockTaskPolicyList[1] } + lockTaskFeatures.forEach { result += it } + } + try { + dpm.setLockTaskFeatures(receiver,result) + Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + } catch (e: IllegalArgumentException) { + AlertDialog.Builder(context) + .setTitle("Error") + .setMessage(e.message) + .setPositiveButton(R.string.confirm) { dialog, _ -> dialog.dismiss() } + .show() } - dpm.setLockTaskFeatures(receiver,result) refreshFeature() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() } ) { Text(stringResource(R.string.apply)) } + + val lockTaskPackages = remember { mutableStateListOf() } + var inputLockTaskPkg by remember { mutableStateOf("") } + LaunchedEffect(Unit) { lockTaskPackages.addAll(dpm.getLockTaskPackages(receiver)) } + Spacer(Modifier.padding(vertical = 10.dp)) + Text(text = stringResource(R.string.lock_task_packages), style = typography.headlineLarge) Spacer(Modifier.padding(vertical = 5.dp)) - val whitelist = dpm.getLockTaskPackages(receiver).toMutableList() - var listText by remember { mutableStateOf("") } - var inputPkg by remember { mutableStateOf("") } - val refreshWhitelist = { - inputPkg = "" - listText = "" - listText = whitelist.toText() - } - LaunchedEffect(Unit) { refreshWhitelist() } - Text(text = stringResource(R.string.whitelist_app), style = typography.titleLarge) SelectionContainer(modifier = Modifier.animateContentSize()) { - Text(text = if(listText == "") stringResource(R.string.none) else listText) + var listText = "" + lockTaskPackages.forEach { listText += "\n" + it } + Text(text = stringResource(R.string.app_list_is) + if(listText == "") stringResource(R.string.none) else listText) } OutlinedTextField( - value = inputPkg, - onValueChange = { inputPkg = it }, + value = inputLockTaskPkg, + onValueChange = { inputLockTaskPkg = it }, label = { Text(stringResource(R.string.package_name)) }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), @@ -742,34 +773,64 @@ private fun LockTaskFeatures() { ) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Button( - onClick = { - focusMgr.clearFocus() - whitelist.add(inputPkg) - dpm.setLockTaskPackages(receiver,whitelist.toTypedArray()) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() - refreshWhitelist() - }, + onClick = { lockTaskPackages.add(inputLockTaskPkg) }, modifier = Modifier.fillMaxWidth(0.49F) ) { Text(stringResource(R.string.add)) } Button( - onClick = { - focusMgr.clearFocus() - if(inputPkg in whitelist) { - whitelist.remove(inputPkg) - dpm.setLockTaskPackages(receiver,whitelist.toTypedArray()) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(context, R.string.not_exist, Toast.LENGTH_SHORT).show() - } - refreshWhitelist() - }, + onClick = { lockTaskPackages.remove(inputLockTaskPkg) }, modifier = Modifier.fillMaxWidth(0.96F) ) { Text(stringResource(R.string.remove)) } } + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + dpm.setLockTaskPackages(receiver, lockTaskPackages.toTypedArray()) + Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + } + ) { + Text(stringResource(R.string.apply)) + } + var startLockTaskApp by remember { mutableStateOf("") } + Spacer(Modifier.padding(vertical = 10.dp)) + Text(text = stringResource(R.string.start_lock_task_mode), style = typography.headlineLarge) + Spacer(Modifier.padding(vertical = 5.dp)) + OutlinedTextField( + value = startLockTaskApp, + onValueChange = { startLockTaskApp = it }, + label = { Text(stringResource(R.string.package_name)) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), + modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) + ) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + if(!dpm.getLockTaskPackages(receiver).contains(startLockTaskApp)) { + Toast.makeText(context, R.string.app_not_allowed, Toast.LENGTH_SHORT).show() + return@Button + } + val options = ActivityOptions.makeBasic().setLockTaskEnabled(true) + val packageManager = context.packageManager + val launchIntent = packageManager.getLaunchIntentForPackage(startLockTaskApp) + if (launchIntent != null) { + coroutine.launch { + prepareForNotification(context) { + sendStopLockTaskNotification(context) + context.startActivity(launchIntent, options.toBundle()) + Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + } + } + } else { + Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() + } + } + ) { + Text(stringResource(R.string.start)) + } Spacer(Modifier.padding(vertical = 30.dp)) } } @@ -1004,6 +1065,7 @@ fun FactoryResetProtection() { } } +@SuppressLint("NewApi") @Composable private fun WipeData() { val context = LocalContext.current @@ -1011,14 +1073,14 @@ private fun WipeData() { val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager val receiver = ComponentName(context,Receiver::class.java) val focusMgr = LocalFocusManager.current + var warning by remember { mutableStateOf(false) } + var wipeDevice by remember { mutableStateOf(false) } + var externalStorage by remember { mutableStateOf(false) } + var protectionData by remember { mutableStateOf(false) } + var euicc by remember { mutableStateOf(false) } + var silent by remember { mutableStateOf(false) } + var reason by remember { mutableStateOf("") } Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - var flag by remember { mutableIntStateOf(0) } - var confirmed by remember { mutableStateOf(false) } - var externalStorage by remember { mutableStateOf(false) } - var protectionData by remember { mutableStateOf(false) } - var euicc by remember { mutableStateOf(false) } - var silent by remember { mutableStateOf(false) } - var reason by remember { mutableStateOf("") } Spacer(Modifier.padding(vertical = 10.dp)) Text( text = stringResource(R.string.wipe_data), @@ -1028,76 +1090,91 @@ private fun WipeData() { Spacer(Modifier.padding(vertical = 5.dp)) CheckBoxItem( stringResource(R.string.wipe_external_storage), - externalStorage, { externalStorage = it; confirmed = false } + externalStorage, { externalStorage = it } ) if(VERSION.SDK_INT >= 22 && isDeviceOwner(dpm)) { CheckBoxItem(stringResource(R.string.wipe_reset_protection_data), - protectionData, { protectionData = it; confirmed = false} + protectionData, { protectionData = it } ) } - if(VERSION.SDK_INT >= 28) { CheckBoxItem(stringResource(R.string.wipe_euicc), euicc, { euicc = it; confirmed = false }) } - if(VERSION.SDK_INT >= 29) { CheckBoxItem(stringResource(R.string.wipe_silently), silent, { silent = it; confirmed = false }) } + if(VERSION.SDK_INT >= 28) { CheckBoxItem(stringResource(R.string.wipe_euicc), euicc, { euicc = it }) } + if(VERSION.SDK_INT >= 29) { CheckBoxItem(stringResource(R.string.wipe_silently), silent, { silent = it }) } AnimatedVisibility(!silent && VERSION.SDK_INT >= 28) { OutlinedTextField( value = reason, onValueChange = { reason = it }, label = { Text(stringResource(R.string.reason)) }, - enabled = !confirmed, modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) ) } Spacer(Modifier.padding(vertical = 5.dp)) - Button( - onClick = { - focusMgr.clearFocus() - flag = 0 - if(externalStorage) { flag += WIPE_EXTERNAL_STORAGE } - if(protectionData && VERSION.SDK_INT >= 22) { flag += WIPE_RESET_PROTECTION_DATA } - if(euicc && VERSION.SDK_INT >= 28) { flag += WIPE_EUICC } - if(reason == "") { silent = true } - if(silent && VERSION.SDK_INT >= 29) { flag += WIPE_SILENTLY } - confirmed = !confirmed - }, - colors = ButtonDefaults.buttonColors( - containerColor = if(confirmed) colorScheme.primary else colorScheme.error , - contentColor = if(confirmed) colorScheme.onPrimary else colorScheme.onError - ), - modifier = Modifier.fillMaxWidth() - ) { - Text(text = stringResource(if(confirmed) R.string.cancel else R.string.confirm)) - } - Button( - onClick = { - if(VERSION.SDK_INT >= 28 && reason != "") { - dpm.wipeData(flag, reason) - }else{ - dpm.wipeData(flag) - } - }, - colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError), - enabled = confirmed && (VERSION.SDK_INT < 34 || (VERSION.SDK_INT >= 34 && !userManager.isSystemUser)), - modifier = Modifier.fillMaxWidth() - ) { - Text("WipeData") + if(VERSION.SDK_INT < 34 || (VERSION.SDK_INT >= 34 && !userManager.isSystemUser)) { + Button( + onClick = { + focusMgr.clearFocus() + wipeDevice = false + warning = true + }, + colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError), + modifier = Modifier.fillMaxWidth() + ) { + Text("WipeData") + } } if (VERSION.SDK_INT >= 34 && (isDeviceOwner(dpm) || dpm.isOrgProfile(receiver))) { Button( - onClick = { dpm.wipeDevice(flag) }, + onClick = { + focusMgr.clearFocus() + wipeDevice = true + warning = true + }, colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError), - enabled = confirmed, modifier = Modifier.fillMaxWidth() ) { Text("WipeDevice") } } - Spacer(Modifier.padding(vertical = 5.dp)) - if(VERSION.SDK_INT >= 24 && isProfileOwner(dpm) && dpm.isManagedProfile(receiver)) { - Information{ Text(text = stringResource(R.string.will_delete_work_profile)) } - } - if(VERSION.SDK_INT >= 34 && Binder.getCallingUid()/100000 == 0) { - Information{ Text(text = stringResource(R.string.api34_or_above_wipedata_cannot_in_system_user)) } - } Spacer(Modifier.padding(vertical = 30.dp)) } + if(warning) { + LaunchedEffect(Unit) { silent = reason == "" } + AlertDialog( + title = { + Text(text = stringResource(R.string.warning), color = colorScheme.error) + }, + text = { + Text(text = stringResource(R.string.wipe_data_warning), color = colorScheme.error) + }, + onDismissRequest = { warning = false }, + confirmButton = { + TextButton( + onClick = { + var flag = 0 + if(externalStorage) { flag += WIPE_EXTERNAL_STORAGE } + if(protectionData && VERSION.SDK_INT >= 22) { flag += WIPE_RESET_PROTECTION_DATA } + if(euicc && VERSION.SDK_INT >= 28) { flag += WIPE_EUICC } + if(silent && VERSION.SDK_INT >= 29) { flag += WIPE_SILENTLY } + if(wipeDevice) { + dpm.wipeDevice(flag) + } else { + if(VERSION.SDK_INT >= 28 && reason != "") { + dpm.wipeData(flag, reason) + } else { + dpm.wipeData(flag) + } + } + }, + colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) + ) { + Text(stringResource(R.string.confirm)) + } + }, + dismissButton = { + TextButton(onClick = { warning = false }) { + Text(stringResource(R.string.cancel)) + } + } + ) + } } @Composable @@ -1252,3 +1329,23 @@ fun InstallSystemUpdate() { Spacer(Modifier.padding(vertical = 30.dp)) } } + +@SuppressLint("NewApi") +private fun sendStopLockTaskNotification(context: Context) { + val nm = context.getSystemService(ComponentActivity.NOTIFICATION_SERVICE) as NotificationManager + if (VERSION.SDK_INT >= 26) { + val channel = NotificationChannel("LockTaskMode", context.getString(R.string.lock_task_mode), NotificationManager.IMPORTANCE_HIGH).apply { + description = "Notification channel for stop lock task mode" + setShowBadge(false) + } + nm.createNotificationChannel(channel) + } + val intent = Intent(context, StopLockTaskModeReceiver::class.java) + val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val builder = NotificationCompat.Builder(context, "LockTaskMode") + .setContentTitle(context.getText(R.string.lock_task_mode)) + .setSmallIcon(R.drawable.lock_fill0) + .addAction(NotificationCompat.Action.Builder(R.drawable.lock_fill0, context.getText(R.string.stop), pendingIntent).build()) + .setPriority(NotificationCompat.PRIORITY_HIGH) + nm.notify(1, builder.build()) +} diff --git a/app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt new file mode 100644 index 0000000..716e55f --- /dev/null +++ b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 fishbone + * + * This code is licensed under MIT license (see LICENSE file for details) + */ + +package com.github.fishb1.apkinfo + +import com.android.apksig.internal.apk.AndroidBinXmlParser +import com.android.apksig.internal.apk.AndroidBinXmlParser.XmlParserException +import java.io.InputStream +import java.nio.ByteBuffer +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +data class ApkInfo( + val compileSdkVersion: Int = 0, + val compileSdkVersionCodename: String = "", + val installLocation: String = "", + val packageName: String = "", + val platformBuildVersionCode: Int = 0, + val platformBuildVersionName: String = "", + val versionCode: Int = 0, + val versionName: String = "", +) { + + companion object { + + private val EMPTY = ApkInfo() + + private const val MANIFEST_FILE_NAME = "AndroidManifest.xml" + + fun fromInputStream(stream: InputStream): ApkInfo { + ZipInputStream(stream).use { zip -> + var entry: ZipEntry? + do { + entry = zip.nextEntry + if (entry?.name == MANIFEST_FILE_NAME) { + val data = ByteBuffer.wrap(zip.readBytes()) + return try { + val parser = AndroidBinXmlParser(data) + ManifestUtils.readApkInfo(parser) + } catch (e: XmlParserException) { + EMPTY + } + } + } while (entry != null) + } + return EMPTY + } + } +} diff --git a/app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt new file mode 100644 index 0000000..b932e11 --- /dev/null +++ b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2022 fishbone + * + * This code is licensed under MIT license (see LICENSE file for details) + */ + +package com.github.fishb1.apkinfo + +internal class ApkInfoBuilder { + + private var compileSdkVersion: Int = 0 + private var compileSdkVersionCodename: String = "" + private var installLocation: String = "" + private var packageName: String = "" + private var platformBuildVersionCode: Int = 0 + private var platformBuildVersionName: String = "" + private var versionCode: Int = 0 + private var versionName: String = "" + + fun compileSdkVersion(value: Int) = apply { + compileSdkVersion = value + } + + fun compileSdkVersionCodename(value: String) = apply { + compileSdkVersionCodename = value + } + + fun installLocation(value: String) = apply { + installLocation = value + } + + fun packageName(value: String) = apply { + packageName = value + } + + fun platformBuildVersionCode(value: Int) = apply { + platformBuildVersionCode = value + } + + fun platformBuildVersionName(value: String) = apply { + platformBuildVersionName = value + } + + fun versionCode(value: Int) = apply { + versionCode = value + } + + fun versionName(value: String) = apply { + versionName = value + } + + fun build(): ApkInfo { + return ApkInfo( + compileSdkVersion = compileSdkVersion, + compileSdkVersionCodename =compileSdkVersionCodename, + installLocation = installLocation, + packageName = packageName, + platformBuildVersionCode = platformBuildVersionCode, + platformBuildVersionName = platformBuildVersionName, + versionCode = versionCode, + versionName = versionName, + ) + } +} diff --git a/app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt b/app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt new file mode 100644 index 0000000..f178e45 --- /dev/null +++ b/app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 fishbone + * + * This code is licensed under MIT license (see LICENSE file for details) + */ + +package com.github.fishb1.apkinfo + +import com.android.apksig.internal.apk.AndroidBinXmlParser + +internal object ManifestUtils { + + private const val TAG_MANIFEST = "manifest" + + private const val ATTR_COMPILE_SDK_VERSION = "compileSdkVersion" + private const val ATTR_COMPILE_SDK_VERSION_CODENAME = "compileSdkVersionCodename" + private const val ATTR_INSTALL_LOCATION = "installLocation" + private const val ATTR_PACKAGE = "package" + private const val ATTR_PLATFORM_BUILD_VERSION_CODE = "platformBuildVersionCode" + private const val ATTR_PLATFORM_BUILD_VERSION_NAME = "platformBuildVersionName" + private const val ATTR_VERSION_CODE = "versionCode" + private const val ATTR_VERSION_NAME = "versionName" + + fun readApkInfo(parser: AndroidBinXmlParser): ApkInfo { + val builder = ApkInfoBuilder() + var eventType = parser.eventType + while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { + if (eventType == AndroidBinXmlParser.EVENT_START_ELEMENT && parser.name == TAG_MANIFEST) { + + for (i in 0 until parser.attributeCount) { + when (parser.getAttributeName(i)) { + ATTR_COMPILE_SDK_VERSION -> + builder.compileSdkVersion(parser.getAttributeIntValue(i)) + ATTR_COMPILE_SDK_VERSION_CODENAME -> + builder.compileSdkVersionCodename(parser.getAttributeStringValue(i)) + ATTR_INSTALL_LOCATION -> + builder.installLocation(parser.getAttributeStringValue(i)) + ATTR_PACKAGE -> + builder.packageName(parser.getAttributeStringValue(i)) + ATTR_PLATFORM_BUILD_VERSION_CODE -> + builder.platformBuildVersionCode(parser.getAttributeIntValue(i)) + ATTR_PLATFORM_BUILD_VERSION_NAME -> + builder.platformBuildVersionName(parser.getAttributeStringValue(i)) + ATTR_VERSION_CODE -> + builder.versionCode(parser.getAttributeIntValue(i)) + ATTR_VERSION_NAME -> + builder.versionName(parser.getAttributeStringValue(i)) + } + } + } + eventType = parser.next() + } + return builder.build() + } +} diff --git a/app/src/main/res/drawable/calendar_month_fill0.xml b/app/src/main/res/drawable/calendar_month_fill0.xml new file mode 100644 index 0000000..9422871 --- /dev/null +++ b/app/src/main/res/drawable/calendar_month_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/call_log_fill0.xml b/app/src/main/res/drawable/call_log_fill0.xml new file mode 100644 index 0000000..aa443c0 --- /dev/null +++ b/app/src/main/res/drawable/call_log_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/contacts_fill0.xml b/app/src/main/res/drawable/contacts_fill0.xml new file mode 100644 index 0000000..215abbf --- /dev/null +++ b/app/src/main/res/drawable/contacts_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/folder_fill0.xml b/app/src/main/res/drawable/folder_fill0.xml new file mode 100644 index 0000000..5cfe915 --- /dev/null +++ b/app/src/main/res/drawable/folder_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/image_fill0.xml b/app/src/main/res/drawable/image_fill0.xml new file mode 100644 index 0000000..c158978 --- /dev/null +++ b/app/src/main/res/drawable/image_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/movie_fill0.xml b/app/src/main/res/drawable/movie_fill0.xml new file mode 100644 index 0000000..4ab4df0 --- /dev/null +++ b/app/src/main/res/drawable/movie_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/music_note_fill0.xml b/app/src/main/res/drawable/music_note_fill0.xml new file mode 100644 index 0000000..13f1432 --- /dev/null +++ b/app/src/main/res/drawable/music_note_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/sensors_fill0.xml b/app/src/main/res/drawable/sensors_fill0.xml new file mode 100644 index 0000000..dac0a7b --- /dev/null +++ b/app/src/main/res/drawable/sensors_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 42d07e5..f502e9d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -50,10 +50,12 @@ G/Ç Hatası Mevcut Durum: Başlat - Bilinmeyen Hata + Stop Tümünü İzin Ver Politika Kullan Hesap + Warning + Delete Etkinleştirmek İçin Tıklayın @@ -151,7 +153,11 @@ Yakındaki akış politikası Yakındaki bildirim akış politikası Yalnızca aynı yönetilen hesap + Lock task mode Görev kilitleme özelliği + Lock task packages + Start lock task mode + App not allowed Hepsini devre dışı bırak @@ -162,7 +168,6 @@ Küresel eylemlere izin ver Ekran kilidine izin ver Görevde etkinlik başlatmayı engelle - Beyaz listeye alınan uygulama CA sertifikası Lütfen bir sertifika seçin Yüklenen sertifika: %1$s @@ -175,8 +180,8 @@ Korumalı verileri sil eUICC (eSIM) sil Sessizce sil - Çalışma profili silinecek - System user\'da WipeData kullanamazsınız + All data on your device will be ERASED + Your work profile will be DELETED Şifreleme durumu: FRP politikası Fabrika ayarlarına sıfırlama koruma politikası @@ -215,7 +220,6 @@ SSID listesi: Boş olamaz Zaten mevcut - Lütfen bir politika seçin Özel DNS Ana bilgisayar adı sağlayın Ana bilgisayar hizmet vermiyor @@ -272,6 +276,7 @@ Kuruluş Kimliği Uzunluk 6 ile 64 karakter arasında olmalıdır Bunu ayarladıktan sonra cihaz spesifik Kimlik alabilirsiniz. + Delete work profile Uygulama yöneticisi @@ -286,6 +291,9 @@ Gizle Mevcut olmayan uygulamalar gizlidir Her zaman açık VPN + Enable lockdown + Current app: + Clear current config İzin Kapsam: iş profili Uygulama bilgisi @@ -520,6 +528,7 @@ Ayarlar + Show dangerous features Material You rengi Android 12+ Hakkında @@ -545,6 +554,9 @@ Depolamayı temizle Depolama başarıyla temizlendi\nUygulama kapanacak + Automation API + Debug mode + Harici depolamayı oku Harici depolamaya yaz @@ -571,5 +583,8 @@ Arka planda vücut sensörlerine eriş Aktivite tanıma Bildirim gönder - + + Version name + Version code + Parsing APK info... diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 05a71bb..1faecb5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -47,10 +47,12 @@ IO异常 当前状态: 开始 - 未知错误 + 停止 允许全部 使用策略 账户 + 警告 + 删除 点击以激活 @@ -146,7 +148,11 @@ 附近流式传输策略 附近通知传输 在足够安全时启用 + 锁定任务模式 锁定任务功能 + 锁定任务应用 + 启动锁定任务模式 + 应用未被允许 禁用全部 允许状态栏信息 允许通知 @@ -155,7 +161,6 @@ 允许全局行为(比如长按电源键对话框) 允许锁屏 阻止启动未允许的应用 - 白名单应用 包名 不存在 Ca证书 @@ -170,8 +175,8 @@ 清除受保护的数据 清除eUICC(eSIM) 静默清除 - 将会删除工作资料 - API34或以上将不能在系统用户中使用WipeData + 你的设备上的所有数据将会被清除 + 你的工作资料将会被删除 加密状态: FRP策略 恢复出厂设置保护策略 @@ -210,7 +215,6 @@ SSID列表: 不能为空 已经存在 - 请选择策略 私人DNS 指定主机名 主机不支持 @@ -267,6 +271,7 @@ 组织ID 长度应在6~64个字符之间 设置组织ID后才能获取设备唯一标识码 + 删除工作资料 应用管理 @@ -281,6 +286,9 @@ 隐藏 如果隐藏,有可能是没安装 VPN保持打开 + 启用锁定 + 当前应用: + 清除当前配置 权限 作用域: 工作资料 应用详情 @@ -511,6 +519,7 @@ 设置 + 显示危险功能 Material you 颜色 安卓12+ 关于 @@ -536,6 +545,9 @@ 清除存储空间 清除存储空间成功\n应用即将退出 + 自动化API + 调试模式 + 读取外部存储 写入外部存储 @@ -563,4 +575,7 @@ 查看使用情况 发送通知 + 版本名 + 版本号 + 解析APK信息中... diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da159d2..fa3c1ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,10 +50,12 @@ IO Exception Current status: Start - Unknown error + Stop Allow all Use policy Account + Warning + Delete Click to activate @@ -109,8 +111,8 @@ dpm set-active-admin com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver Permission granted (Shell) Permission granted (Root) - Activate profile owner - Activate device owner + Activate Profile owner + Activate Device owner Activate organization-owned work profile Shizuku service disconnected Invalid binder @@ -157,7 +159,11 @@ Nearby streaming policy Nearby notification streaming policy Same managed account only + Lock task mode Lock task feature + Lock task packages + Start lock task mode + App not allowed Disable all Allow system info @@ -167,7 +173,6 @@ Allow global actions Allow keyguard Block activity start in task - Whitelisted app Ca certification Please select a certification Cert installed: %1$s @@ -180,8 +185,8 @@ Wipe protected data Wipe eUICC(eSIM) Wipe silently - Work profile will be deleted. - You cannot use WipeData in system user. + All data on your device will be ERASED + Your work profile will be DELETED Encrypt status: FRP policy Factory reset protection policy @@ -221,7 +226,6 @@ SSID list: Cannot be empty Already exist - Please select a policy PrivateDNS Provide hostname Host not serving @@ -281,6 +285,7 @@ Organization ID The length should be between 6~64 characters You can get device specific ID after set this. + Delete work profile App manager @@ -295,6 +300,9 @@ Hide Non-existent apps is hidden Always-on VPN + Enable lockdown + Current app: + Clear current config Permission Scope: work profile App info @@ -527,6 +535,7 @@ Settings + Show dangerous features Material you color Android 12+ About @@ -552,6 +561,9 @@ Clear storage Clear storage success\nApplication will exit + Automation API + Debug mode + Read external storage Write external storage @@ -579,4 +591,7 @@ Activity recognition Post notifications + Version name + Version code + Parsing APK info... diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 83f2a43..7ed11ac 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -4,7 +4,7 @@ #FFFFFF #FFFFFF -