mirror of
https://github.com/topjohnwu/Magisk.git
synced 2026-03-16 06:48:49 -07:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4f4239f5c | ||
|
|
debf1800d8 | ||
|
|
7008c563e8 | ||
|
|
62c6ab8c0a | ||
|
|
6242a605f5 | ||
|
|
2a5ff26e22 | ||
|
|
4bfb9d820f | ||
|
|
b02f52f283 | ||
|
|
6771f1141b | ||
|
|
c4ddebfa73 | ||
|
|
a211541fc3 | ||
|
|
4d758f871b | ||
|
|
162b84661b | ||
|
|
b087cb2876 | ||
|
|
299350cb7b | ||
|
|
ab93c22750 | ||
|
|
24b16e3609 | ||
|
|
a6f0cb20af | ||
|
|
f4dd504eeb | ||
|
|
bcffc941bb | ||
|
|
41059e8d69 | ||
|
|
b89589ee0c | ||
|
|
741eff8c50 | ||
|
|
bc3f6b1a99 | ||
|
|
6a9bd4531f | ||
|
|
2ce0fdebe6 | ||
|
|
6e9e12e4b2 | ||
|
|
73c3cfc7f3 | ||
|
|
3b73a0ea2c | ||
|
|
d8d64c8a75 | ||
|
|
125d2caffd | ||
|
|
2fb7030527 | ||
|
|
08d412d43c | ||
|
|
fb79eddc73 | ||
|
|
0279882309 | ||
|
|
be63d2bb36 | ||
|
|
1f74953b84 | ||
|
|
5f01d29204 | ||
|
|
e12e3f907e | ||
|
|
8946938987 | ||
|
|
6f4e4743a1 | ||
|
|
3e3d45d2dc | ||
|
|
44a31234d3 | ||
|
|
d7301154a5 | ||
|
|
d7ec7f826b | ||
|
|
2cab7d6c7b | ||
|
|
e000607d71 | ||
|
|
08d0b9be27 | ||
|
|
45c7cf19c5 | ||
|
|
4204fc56d4 | ||
|
|
390d529168 | ||
|
|
a757758e10 | ||
|
|
7a6c8508ce | ||
|
|
ffdb5cb511 | ||
|
|
47f1a33585 | ||
|
|
20d6d70ff6 | ||
|
|
397a906905 | ||
|
|
a9ade79617 | ||
|
|
70a0dafb21 | ||
|
|
e6c4268694 | ||
|
|
9107bb6af3 | ||
|
|
925ed577e7 | ||
|
|
1d4fbd9e75 | ||
|
|
7845da2943 | ||
|
|
6bb48df712 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,3 +12,7 @@ native/out
|
||||
# Android Studio
|
||||
*.iml
|
||||
.idea
|
||||
.cursor
|
||||
ramdisk.img
|
||||
app/core/src/debug
|
||||
app/core/src/release
|
||||
|
||||
61
app/apk-ng/build.gradle.kts
Normal file
61
app/apk-ng/build.gradle.kts
Normal file
@@ -0,0 +1,61 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("plugin.parcelize")
|
||||
kotlin("plugin.compose")
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
|
||||
setupMainApk()
|
||||
|
||||
android {
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
excludes += "lib/*/libandroidx.graphics.path.so"
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
proguardFile("proguard-rules.pro")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core"))
|
||||
coreLibraryDesugaring(libs.jdk.libs)
|
||||
|
||||
implementation(libs.appcompat)
|
||||
implementation(libs.material)
|
||||
|
||||
// Compose
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
debugImplementation(libs.compose.ui.tooling)
|
||||
implementation(libs.activity.compose)
|
||||
implementation(libs.lifecycle.runtime.compose)
|
||||
implementation(libs.lifecycle.viewmodel.compose)
|
||||
implementation(libs.miuix)
|
||||
implementation(libs.miuix.icons)
|
||||
implementation(libs.miuix.navigation3.ui)
|
||||
|
||||
// Navigation3
|
||||
implementation(libs.navigation3.runtime)
|
||||
implementation(libs.navigationevent.compose)
|
||||
implementation(libs.lifecycle.viewmodel.navigation3)
|
||||
|
||||
}
|
||||
3
app/apk-ng/proguard-rules.pro
vendored
Normal file
3
app/apk-ng/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Excessive obfuscation
|
||||
-flattenpackagehierarchy
|
||||
-allowaccessmodification
|
||||
34
app/apk-ng/src/main/AndroidManifest.xml
Normal file
34
app/apk-ng/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application android:localeConfig="@xml/locale_config">
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/SplashTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.surequest.SuRequestActivity"
|
||||
android:directBootAware="true"
|
||||
android:exported="false"
|
||||
android:taskAffinity=""
|
||||
tools:ignore="AppLinkUrlError">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class AsyncLoadViewModel : BaseViewModel() {
|
||||
|
||||
private var loadingJob: Job? = null
|
||||
|
||||
@MainThread
|
||||
fun startLoading() {
|
||||
if (loadingJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
loadingJob = viewModelScope.launch { doLoadWork() }
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun reload() {
|
||||
loadingJob?.cancel()
|
||||
loadingJob = viewModelScope.launch { doLoadWork() }
|
||||
}
|
||||
|
||||
protected abstract suspend fun doLoadWork()
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
abstract class BaseViewModel : ViewModel() {
|
||||
|
||||
private val _navEvents = MutableSharedFlow<Route>(extraBufferCapacity = 1)
|
||||
val navEvents: SharedFlow<Route> = _navEvents
|
||||
|
||||
open fun onSaveState(state: Bundle) {}
|
||||
open fun onRestoreState(state: Bundle) {}
|
||||
|
||||
fun showSnackbar(@StringRes resId: Int) {
|
||||
AppContext.toast(resId, Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
||||
fun showSnackbar(msg: String) {
|
||||
AppContext.toast(msg, Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
||||
fun navigateTo(route: Route) {
|
||||
_navEvents.tryEmit(route)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
|
||||
|
||||
object VMFactory : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return when (modelClass) {
|
||||
HomeViewModel::class.java -> HomeViewModel(ServiceLocator.networkService)
|
||||
LogViewModel::class.java -> LogViewModel(ServiceLocator.logRepo)
|
||||
SuperuserViewModel::class.java -> SuperuserViewModel(ServiceLocator.policyDB)
|
||||
InstallViewModel::class.java ->
|
||||
InstallViewModel(ServiceLocator.networkService)
|
||||
SuRequestViewModel::class.java ->
|
||||
SuRequestViewModel(ServiceLocator.policyDB, ServiceLocator.timeoutPrefs)
|
||||
else -> modelClass.newInstance()
|
||||
} as T
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
import java.util.Arrays
|
||||
|
||||
/**
|
||||
* A circular buffer of [TerminalRow]s which keeps notes about what is visible on a logical screen and the scroll
|
||||
* history.
|
||||
*
|
||||
* See [externalToInternalRow] for how to map from logical screen rows to array indices.
|
||||
*/
|
||||
class TerminalBuffer(columns: Int, totalRows: Int, screenRows: Int) {
|
||||
|
||||
var lines: Array<TerminalRow?>
|
||||
|
||||
/** The length of [lines]. */
|
||||
var totalRows: Int = totalRows
|
||||
private set
|
||||
|
||||
/** The number of rows and columns visible on the screen. */
|
||||
var screenRows: Int = screenRows
|
||||
|
||||
var columns: Int = columns
|
||||
|
||||
/** The number of rows kept in history. */
|
||||
var activeTranscriptRows: Int = 0
|
||||
private set
|
||||
|
||||
/** The index in the circular buffer where the visible screen starts. */
|
||||
private var screenFirstRow = 0
|
||||
|
||||
init {
|
||||
lines = arrayOfNulls(totalRows)
|
||||
blockSet(0, 0, columns, screenRows, ' '.code, TextStyle.NORMAL)
|
||||
}
|
||||
|
||||
val transcriptText: String
|
||||
get() = getSelectedText(0, -activeTranscriptRows, columns, screenRows).trim()
|
||||
|
||||
val transcriptTextWithoutJoinedLines: String
|
||||
get() = getSelectedText(0, -activeTranscriptRows, columns, screenRows, false).trim()
|
||||
|
||||
val transcriptTextWithFullLinesJoined: String
|
||||
get() = getSelectedText(0, -activeTranscriptRows, columns, screenRows, joinBackLines = true, joinFullLines = true).trim()
|
||||
|
||||
fun getSelectedText(selX1: Int, selY1: Int, selX2: Int, selY2: Int, joinBackLines: Boolean = true, joinFullLines: Boolean = false): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
var y1 = selY1
|
||||
var y2 = selY2
|
||||
if (y1 < -activeTranscriptRows) y1 = -activeTranscriptRows
|
||||
if (y2 >= screenRows) y2 = screenRows - 1
|
||||
|
||||
for (row in y1..y2) {
|
||||
val x1 = if (row == y1) selX1 else 0
|
||||
var x2: Int
|
||||
if (row == y2) {
|
||||
x2 = selX2 + 1
|
||||
if (x2 > columns) x2 = columns
|
||||
} else {
|
||||
x2 = columns
|
||||
}
|
||||
val lineObject = lines[externalToInternalRow(row)]!!
|
||||
val x1Index = lineObject.findStartOfColumn(x1)
|
||||
var x2Index = if (x2 < columns) lineObject.findStartOfColumn(x2) else lineObject.spaceUsed
|
||||
if (x2Index == x1Index) {
|
||||
x2Index = lineObject.findStartOfColumn(x2 + 1)
|
||||
}
|
||||
val line = lineObject.text
|
||||
var lastPrintingCharIndex = -1
|
||||
val rowLineWrap = getLineWrap(row)
|
||||
if (rowLineWrap && x2 == columns) {
|
||||
lastPrintingCharIndex = x2Index - 1
|
||||
} else {
|
||||
for (i in x1Index until x2Index) {
|
||||
val c = line[i]
|
||||
if (c != ' ') lastPrintingCharIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
val len = lastPrintingCharIndex - x1Index + 1
|
||||
if (lastPrintingCharIndex != -1 && len > 0)
|
||||
builder.append(line, x1Index, len)
|
||||
|
||||
val lineFillsWidth = lastPrintingCharIndex == x2Index - 1
|
||||
if ((!joinBackLines || !rowLineWrap) && (!joinFullLines || !lineFillsWidth)
|
||||
&& row < y2 && row < screenRows - 1) builder.append('\n')
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun getWordAtLocation(x: Int, y: Int): String {
|
||||
var y1 = y
|
||||
var y2 = y
|
||||
while (y1 > 0 && !getSelectedText(0, y1 - 1, columns, y, joinBackLines = true, joinFullLines = true).contains("\n")) {
|
||||
y1--
|
||||
}
|
||||
while (y2 < screenRows && !getSelectedText(0, y, columns, y2 + 1, joinBackLines = true, joinFullLines = true).contains("\n")) {
|
||||
y2++
|
||||
}
|
||||
|
||||
val text = getSelectedText(0, y1, columns, y2, joinBackLines = true, joinFullLines = true)
|
||||
val textOffset = (y - y1) * columns + x
|
||||
|
||||
if (textOffset >= text.length) {
|
||||
return ""
|
||||
}
|
||||
|
||||
val x1 = text.lastIndexOf(' ', textOffset)
|
||||
var x2 = text.indexOf(' ', textOffset)
|
||||
if (x2 == -1) {
|
||||
x2 = text.length
|
||||
}
|
||||
|
||||
if (x1 == x2) {
|
||||
return ""
|
||||
}
|
||||
return text.substring(x1 + 1, x2)
|
||||
}
|
||||
|
||||
val activeRows: Int get() = activeTranscriptRows + screenRows
|
||||
|
||||
/**
|
||||
* Convert a row value from the public external coordinate system to our internal private coordinate system.
|
||||
*
|
||||
* ```
|
||||
* - External coordinate system: -activeTranscriptRows to screenRows-1, with the screen being 0..screenRows-1.
|
||||
* - Internal coordinate system: the screenRows lines starting at screenFirstRow comprise the screen, while the
|
||||
* activeTranscriptRows lines ending at screenFirstRow-1 form the transcript (as a circular buffer).
|
||||
*
|
||||
* External <-> Internal:
|
||||
*
|
||||
* [ ... ] [ ... ]
|
||||
* [ -activeTranscriptRows ] [ screenFirstRow - activeTranscriptRows ]
|
||||
* [ ... ] [ ... ]
|
||||
* [ 0 (visible screen starts here) ] <-> [ screenFirstRow ]
|
||||
* [ ... ] [ ... ]
|
||||
* [ screenRows-1 ] [ screenFirstRow + screenRows-1 ]
|
||||
* ```
|
||||
*
|
||||
* @param externalRow a row in the external coordinate system.
|
||||
* @return The row corresponding to the input argument in the private coordinate system.
|
||||
*/
|
||||
fun externalToInternalRow(externalRow: Int): Int {
|
||||
if (externalRow < -activeTranscriptRows || externalRow > screenRows)
|
||||
throw IllegalArgumentException("extRow=$externalRow, screenRows=$screenRows, activeTranscriptRows=$activeTranscriptRows")
|
||||
val internalRow = screenFirstRow + externalRow
|
||||
return if (internalRow < 0) (totalRows + internalRow) else (internalRow % totalRows)
|
||||
}
|
||||
|
||||
fun setLineWrap(row: Int) {
|
||||
lines[externalToInternalRow(row)]!!.lineWrap = true
|
||||
}
|
||||
|
||||
fun getLineWrap(row: Int): Boolean {
|
||||
return lines[externalToInternalRow(row)]!!.lineWrap
|
||||
}
|
||||
|
||||
fun clearLineWrap(row: Int) {
|
||||
lines[externalToInternalRow(row)]!!.lineWrap = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
|
||||
* change or the rows expand (that is, it only works when shrinking the number of rows).
|
||||
*
|
||||
* @param newColumns The number of columns the screen should have.
|
||||
* @param newRows The number of rows the screen should have.
|
||||
* @param cursor An int[2] containing the (column, row) cursor location.
|
||||
*/
|
||||
fun resize(newColumns: Int, newRows: Int, newTotalRows: Int, cursor: IntArray, currentStyle: Long, altScreen: Boolean) {
|
||||
// newRows > totalRows should not normally happen since totalRows is TRANSCRIPT_ROWS (10000):
|
||||
if (newColumns == columns && newRows <= totalRows) {
|
||||
// Fast resize where just the rows changed.
|
||||
var shiftDownOfTopRow = screenRows - newRows
|
||||
if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < screenRows) {
|
||||
// Shrinking. Check if we can skip blank rows at bottom below cursor.
|
||||
for (i in screenRows - 1 downTo 1) {
|
||||
if (cursor[1] >= i) break
|
||||
val r = externalToInternalRow(i)
|
||||
if (lines[r] == null || lines[r]!!.isBlank()) {
|
||||
if (--shiftDownOfTopRow == 0) break
|
||||
}
|
||||
}
|
||||
} else if (shiftDownOfTopRow < 0) {
|
||||
// Negative shift down = expanding. Only move screen up if there is transcript to show:
|
||||
val actualShift = maxOf(shiftDownOfTopRow, -activeTranscriptRows)
|
||||
if (shiftDownOfTopRow != actualShift) {
|
||||
for (i in 0 until actualShift - shiftDownOfTopRow)
|
||||
allocateFullLineIfNecessary((screenFirstRow + screenRows + i) % totalRows).clear(currentStyle)
|
||||
shiftDownOfTopRow = actualShift
|
||||
}
|
||||
}
|
||||
screenFirstRow += shiftDownOfTopRow
|
||||
screenFirstRow = if (screenFirstRow < 0) (screenFirstRow + totalRows) else (screenFirstRow % totalRows)
|
||||
totalRows = newTotalRows
|
||||
activeTranscriptRows = if (altScreen) 0 else maxOf(0, activeTranscriptRows + shiftDownOfTopRow)
|
||||
cursor[1] -= shiftDownOfTopRow
|
||||
screenRows = newRows
|
||||
} else {
|
||||
// Copy away old state and update new:
|
||||
val oldLines = lines
|
||||
lines = arrayOfNulls(newTotalRows)
|
||||
for (i in 0 until newTotalRows)
|
||||
lines[i] = TerminalRow(newColumns, currentStyle)
|
||||
|
||||
val oldActiveTranscriptRows = activeTranscriptRows
|
||||
val oldScreenFirstRow = screenFirstRow
|
||||
val oldScreenRows = screenRows
|
||||
val oldTotalRows = totalRows
|
||||
totalRows = newTotalRows
|
||||
screenRows = newRows
|
||||
activeTranscriptRows = 0
|
||||
screenFirstRow = 0
|
||||
columns = newColumns
|
||||
|
||||
var newCursorRow = -1
|
||||
var newCursorColumn = -1
|
||||
val oldCursorRow = cursor[1]
|
||||
val oldCursorColumn = cursor[0]
|
||||
var newCursorPlaced = false
|
||||
|
||||
var currentOutputExternalRow = 0
|
||||
var currentOutputExternalColumn = 0
|
||||
|
||||
var skippedBlankLines = 0
|
||||
for (externalOldRow in -oldActiveTranscriptRows until oldScreenRows) {
|
||||
var internalOldRow = oldScreenFirstRow + externalOldRow
|
||||
internalOldRow = if (internalOldRow < 0) (oldTotalRows + internalOldRow) else (internalOldRow % oldTotalRows)
|
||||
|
||||
val oldLine = oldLines[internalOldRow]
|
||||
val cursorAtThisRow = externalOldRow == oldCursorRow
|
||||
if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) {
|
||||
skippedBlankLines++
|
||||
continue
|
||||
} else if (skippedBlankLines > 0) {
|
||||
for (i in 0 until skippedBlankLines) {
|
||||
if (currentOutputExternalRow == screenRows - 1) {
|
||||
scrollDownOneLine(0, screenRows, currentStyle)
|
||||
} else {
|
||||
currentOutputExternalRow++
|
||||
}
|
||||
currentOutputExternalColumn = 0
|
||||
}
|
||||
skippedBlankLines = 0
|
||||
}
|
||||
|
||||
var lastNonSpaceIndex = 0
|
||||
var justToCursor = false
|
||||
if (cursorAtThisRow || oldLine.lineWrap) {
|
||||
lastNonSpaceIndex = oldLine.spaceUsed
|
||||
if (cursorAtThisRow) justToCursor = true
|
||||
} else {
|
||||
for (i in 0 until oldLine.spaceUsed)
|
||||
// NEWLY INTRODUCED BUG! Should not index oldLine.styles with char indices
|
||||
if (oldLine.text[i] != ' '/* || oldLine.styles[i] != currentStyle */)
|
||||
lastNonSpaceIndex = i + 1
|
||||
}
|
||||
|
||||
var currentOldCol = 0
|
||||
var styleAtCol = 0L
|
||||
var i = 0
|
||||
while (i < lastNonSpaceIndex) {
|
||||
val c = oldLine.text[i]
|
||||
val codePoint: Int
|
||||
if (Character.isHighSurrogate(c)) {
|
||||
i++
|
||||
codePoint = Character.toCodePoint(c, oldLine.text[i])
|
||||
} else {
|
||||
codePoint = c.code
|
||||
}
|
||||
val displayWidth = WcWidth.width(codePoint)
|
||||
if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol)
|
||||
|
||||
if (currentOutputExternalColumn + displayWidth > columns) {
|
||||
setLineWrap(currentOutputExternalRow)
|
||||
if (currentOutputExternalRow == screenRows - 1) {
|
||||
if (newCursorPlaced) newCursorRow--
|
||||
scrollDownOneLine(0, screenRows, currentStyle)
|
||||
} else {
|
||||
currentOutputExternalRow++
|
||||
}
|
||||
currentOutputExternalColumn = 0
|
||||
}
|
||||
|
||||
val offsetDueToCombiningChar = if (displayWidth <= 0 && currentOutputExternalColumn > 0) 1 else 0
|
||||
val outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar
|
||||
setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol)
|
||||
|
||||
if (displayWidth > 0) {
|
||||
if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) {
|
||||
newCursorColumn = currentOutputExternalColumn
|
||||
newCursorRow = currentOutputExternalRow
|
||||
newCursorPlaced = true
|
||||
}
|
||||
currentOldCol += displayWidth
|
||||
currentOutputExternalColumn += displayWidth
|
||||
if (justToCursor && newCursorPlaced) break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (externalOldRow != (oldScreenRows - 1) && !oldLine.lineWrap) {
|
||||
if (currentOutputExternalRow == screenRows - 1) {
|
||||
if (newCursorPlaced) newCursorRow--
|
||||
scrollDownOneLine(0, screenRows, currentStyle)
|
||||
} else {
|
||||
currentOutputExternalRow++
|
||||
}
|
||||
currentOutputExternalColumn = 0
|
||||
}
|
||||
}
|
||||
|
||||
cursor[0] = newCursorColumn
|
||||
cursor[1] = newCursorRow
|
||||
}
|
||||
|
||||
// Handle cursor scrolling off screen:
|
||||
if (cursor[0] < 0 || cursor[1] < 0) {
|
||||
cursor[0] = 0
|
||||
cursor[1] = 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound
|
||||
* into account.
|
||||
*
|
||||
* @param srcInternal The first line to be copied.
|
||||
* @param len The number of lines to be copied.
|
||||
*/
|
||||
private fun blockCopyLinesDown(srcInternal: Int, len: Int) {
|
||||
if (len == 0) return
|
||||
|
||||
val start = len - 1
|
||||
val lineToBeOverWritten = lines[(srcInternal + start + 1) % totalRows]
|
||||
for (i in start downTo 0)
|
||||
lines[(srcInternal + i + 1) % totalRows] = lines[(srcInternal + i) % totalRows]
|
||||
lines[srcInternal % totalRows] = lineToBeOverWritten
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24).
|
||||
*
|
||||
* @param topMargin First line that is scrolled.
|
||||
* @param bottomMargin One line after the last line that is scrolled.
|
||||
* @param style the style for the newly exposed line.
|
||||
*/
|
||||
fun scrollDownOneLine(topMargin: Int, bottomMargin: Int, style: Long) {
|
||||
if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > screenRows)
|
||||
throw IllegalArgumentException("topMargin=$topMargin, bottomMargin=$bottomMargin, screenRows=$screenRows")
|
||||
|
||||
blockCopyLinesDown(screenFirstRow, topMargin)
|
||||
blockCopyLinesDown(externalToInternalRow(bottomMargin), screenRows - bottomMargin)
|
||||
|
||||
screenFirstRow = (screenFirstRow + 1) % totalRows
|
||||
if (activeTranscriptRows < totalRows - screenRows) activeTranscriptRows++
|
||||
|
||||
val blankRow = externalToInternalRow(bottomMargin - 1)
|
||||
if (lines[blankRow] == null) {
|
||||
lines[blankRow] = TerminalRow(columns, style)
|
||||
} else {
|
||||
lines[blankRow]!!.clear(style)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block copy characters from one position in the screen to another. The two positions can overlap. All characters
|
||||
* of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will
|
||||
* be thrown.
|
||||
*
|
||||
* @param sx source X coordinate
|
||||
* @param sy source Y coordinate
|
||||
* @param w width
|
||||
* @param h height
|
||||
* @param dx destination X coordinate
|
||||
* @param dy destination Y coordinate
|
||||
*/
|
||||
fun blockCopy(sx: Int, sy: Int, w: Int, h: Int, dx: Int, dy: Int) {
|
||||
if (w == 0) return
|
||||
if (sx < 0 || sx + w > columns || sy < 0 || sy + h > screenRows || dx < 0 || dx + w > columns || dy < 0 || dy + h > screenRows)
|
||||
throw IllegalArgumentException()
|
||||
val copyingUp = sy > dy
|
||||
for (y in 0 until h) {
|
||||
val y2 = if (copyingUp) y else (h - (y + 1))
|
||||
val sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2))
|
||||
allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Block set characters. All characters must be within the bounds of the screen, or else an
|
||||
* InvalidParameterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
|
||||
* of characters.
|
||||
*/
|
||||
fun blockSet(sx: Int, sy: Int, w: Int, h: Int, `val`: Int, style: Long) {
|
||||
if (sx < 0 || sx + w > columns || sy < 0 || sy + h > screenRows) {
|
||||
throw IllegalArgumentException(
|
||||
"Illegal arguments! blockSet($sx, $sy, $w, $h, $`val`, $columns, $screenRows)")
|
||||
}
|
||||
for (y in 0 until h)
|
||||
for (x in 0 until w)
|
||||
setChar(sx + x, sy + y, `val`, style)
|
||||
}
|
||||
|
||||
fun allocateFullLineIfNecessary(row: Int): TerminalRow {
|
||||
return lines[row] ?: TerminalRow(columns, 0).also { lines[row] = it }
|
||||
}
|
||||
|
||||
fun setChar(column: Int, row: Int, codePoint: Int, style: Long) {
|
||||
if (row < 0 || row >= screenRows || column < 0 || column >= columns)
|
||||
throw IllegalArgumentException("TerminalBuffer.setChar(): row=$row, column=$column, screenRows=$screenRows, columns=$columns")
|
||||
val internalRow = externalToInternalRow(row)
|
||||
allocateFullLineIfNecessary(internalRow).setChar(column, codePoint, style)
|
||||
}
|
||||
|
||||
fun getStyleAt(externalRow: Int, column: Int): Long {
|
||||
return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column)
|
||||
}
|
||||
|
||||
/** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */
|
||||
fun setOrClearEffect(bits: Int, setOrClear: Boolean, reverse: Boolean, rectangular: Boolean, leftMargin: Int, rightMargin: Int, top: Int, left: Int,
|
||||
bottom: Int, right: Int) {
|
||||
for (y in top until bottom) {
|
||||
val line = lines[externalToInternalRow(y)]!!
|
||||
val startOfLine = if (rectangular || y == top) left else leftMargin
|
||||
val endOfLine = if (rectangular || y + 1 == bottom) right else rightMargin
|
||||
for (x in startOfLine until endOfLine) {
|
||||
val currentStyle = line.getStyle(x)
|
||||
val foreColor = TextStyle.decodeForeColor(currentStyle)
|
||||
val backColor = TextStyle.decodeBackColor(currentStyle)
|
||||
var effect = TextStyle.decodeEffect(currentStyle)
|
||||
if (reverse) {
|
||||
effect = (effect and bits.inv()) or (bits and effect.inv())
|
||||
} else if (setOrClear) {
|
||||
effect = effect or bits
|
||||
} else {
|
||||
effect = effect and bits.inv()
|
||||
}
|
||||
line.styles[x] = TextStyle.encode(foreColor, backColor, effect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearTranscript() {
|
||||
if (screenFirstRow < activeTranscriptRows) {
|
||||
Arrays.fill(lines, totalRows + screenFirstRow - activeTranscriptRows, totalRows, null)
|
||||
Arrays.fill(lines, 0, screenFirstRow, null)
|
||||
} else {
|
||||
Arrays.fill(lines, screenFirstRow - activeTranscriptRows, screenFirstRow, null)
|
||||
}
|
||||
activeTranscriptRows = 0
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
|
||||
private val busyboxPath: String by lazy {
|
||||
Shell.cmd("readlink /proc/self/exe").exec().out.firstOrNull()
|
||||
?: "/data/adb/magisk/busybox"
|
||||
}
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
fun TerminalEmulator.appendOnMain(bytes: ByteArray, len: Int) {
|
||||
mainHandler.post {
|
||||
append(bytes, len)
|
||||
onScreenUpdate?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
fun TerminalEmulator.appendLineOnMain(line: String) {
|
||||
val bytes = "$line\r\n".toByteArray(Charsets.UTF_8)
|
||||
appendOnMain(bytes, bytes.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a command as root inside a PTY (via busybox script).
|
||||
* Reads raw bytes from the process and feeds them to the terminal emulator.
|
||||
* Must be called from a background thread.
|
||||
* Returns true if the process exits with code 0.
|
||||
*/
|
||||
fun runSuCommand(emulator: TerminalEmulator, command: String): Boolean {
|
||||
return try {
|
||||
val cols = emulator.mColumns
|
||||
val rows = emulator.mRows
|
||||
val wrappedCmd = "export TERM=xterm-256color; stty cols $cols rows $rows 2>/dev/null; $command"
|
||||
val escapedCmd = wrappedCmd.replace("'", "'\\''")
|
||||
|
||||
val process = ProcessBuilder(
|
||||
"su", "-c",
|
||||
"$busyboxPath script -q -c '$escapedCmd' /dev/null"
|
||||
).redirectErrorStream(true).start()
|
||||
|
||||
process.outputStream.close()
|
||||
|
||||
val buffer = ByteArray(4096)
|
||||
process.inputStream.use { input ->
|
||||
while (true) {
|
||||
val n = input.read(buffer)
|
||||
if (n == -1) break
|
||||
emulator.appendOnMain(buffer.copyOf(n), n)
|
||||
}
|
||||
}
|
||||
|
||||
process.waitFor() == 0
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "runSuCommand failed")
|
||||
emulator.appendLineOnMain("! Error: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
import java.util.Arrays
|
||||
|
||||
/**
|
||||
* A row in a terminal, composed of a fixed number of cells.
|
||||
*
|
||||
* The text in the row is stored in a char[] array, [text], for quick access during rendering.
|
||||
*/
|
||||
class TerminalRow(private val columns: Int, style: Long) {
|
||||
|
||||
/**
|
||||
* Max combining characters that can exist in a column, that are separate from the base character
|
||||
* itself. Any additional combining characters will be ignored and not added to the column.
|
||||
*
|
||||
* There does not seem to be limit in unicode standard for max number of combination characters
|
||||
* that can be combined but such characters are primarily under 10.
|
||||
*
|
||||
* "Section 3.6 Combination" of unicode standard contains combining characters info.
|
||||
* - https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf
|
||||
* - https://en.wikipedia.org/wiki/Combining_character#Unicode_ranges
|
||||
* - https://stackoverflow.com/questions/71237212/what-is-the-maximum-number-of-unicode-combined-characters-that-may-be-needed-to
|
||||
*
|
||||
* UAX15-D3 Stream-Safe Text Format limits to max 30 combining characters.
|
||||
* > The value of 30 is chosen to be significantly beyond what is required for any linguistic or technical usage.
|
||||
* > While it would have been feasible to chose a smaller number, this value provides a very wide margin,
|
||||
* > yet is well within the buffer size limits of practical implementations.
|
||||
* - https://unicode.org/reports/tr15/#Stream_Safe_Text_Format
|
||||
* - https://stackoverflow.com/a/11983435/14686958
|
||||
*
|
||||
* We choose the value 15 because it should be enough for terminal based applications and keep
|
||||
* the memory usage low for a terminal row, won't affect performance or cause terminal to
|
||||
* lag or hang, and will keep malicious applications from causing harm. The value can be
|
||||
* increased if ever needed for legitimate applications.
|
||||
*/
|
||||
companion object {
|
||||
private const val SPARE_CAPACITY_FACTOR = 1.5f
|
||||
private const val MAX_COMBINING_CHARACTERS_PER_COLUMN = 15
|
||||
}
|
||||
|
||||
/** The text filling this terminal row. */
|
||||
var text: CharArray = CharArray((SPARE_CAPACITY_FACTOR * columns).toInt())
|
||||
|
||||
/** The number of java chars used in [text]. */
|
||||
private var _spaceUsed: Short = 0
|
||||
|
||||
/** If this row has been line wrapped due to text output at the end of line. */
|
||||
var lineWrap: Boolean = false
|
||||
|
||||
/** The style bits of each cell in the row. See [TextStyle]. */
|
||||
val styles: LongArray = LongArray(columns)
|
||||
|
||||
/** If this row might contain chars with width != 1, used for deactivating fast path */
|
||||
var hasNonOneWidthOrSurrogateChars: Boolean = false
|
||||
|
||||
init {
|
||||
clear(style)
|
||||
}
|
||||
|
||||
/** NOTE: The sourceX2 is exclusive. */
|
||||
fun copyInterval(line: TerminalRow, sourceX1: Int, sourceX2: Int, destinationX: Int) {
|
||||
hasNonOneWidthOrSurrogateChars = hasNonOneWidthOrSurrogateChars or line.hasNonOneWidthOrSurrogateChars
|
||||
val x1 = line.findStartOfColumn(sourceX1)
|
||||
val x2 = line.findStartOfColumn(sourceX2)
|
||||
var startingFromSecondHalfOfWideChar = sourceX1 > 0 && line.wideDisplayCharacterStartingAt(sourceX1 - 1)
|
||||
val sourceChars = if (this === line) line.text.copyOf() else line.text
|
||||
var latestNonCombiningWidth = 0
|
||||
var destX = destinationX
|
||||
var srcX1 = sourceX1
|
||||
var i = x1
|
||||
while (i < x2) {
|
||||
val sourceChar = sourceChars[i]
|
||||
var codePoint: Int
|
||||
if (Character.isHighSurrogate(sourceChar)) {
|
||||
i++
|
||||
codePoint = Character.toCodePoint(sourceChar, sourceChars[i])
|
||||
} else {
|
||||
codePoint = sourceChar.code
|
||||
}
|
||||
if (startingFromSecondHalfOfWideChar) {
|
||||
codePoint = ' '.code
|
||||
startingFromSecondHalfOfWideChar = false
|
||||
}
|
||||
val w = WcWidth.width(codePoint)
|
||||
if (w > 0) {
|
||||
destX += latestNonCombiningWidth
|
||||
srcX1 += latestNonCombiningWidth
|
||||
latestNonCombiningWidth = w
|
||||
}
|
||||
setChar(destX, codePoint, line.getStyle(srcX1))
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
val spaceUsed: Int get() = _spaceUsed.toInt()
|
||||
|
||||
/** Note that the column may end of second half of wide character. */
|
||||
fun findStartOfColumn(column: Int): Int {
|
||||
if (column == columns) return spaceUsed
|
||||
|
||||
var currentColumn = 0
|
||||
var currentCharIndex = 0
|
||||
while (true) {
|
||||
var newCharIndex = currentCharIndex
|
||||
val c = text[newCharIndex++]
|
||||
val isHigh = Character.isHighSurrogate(c)
|
||||
val codePoint = if (isHigh) Character.toCodePoint(c, text[newCharIndex++]) else c.code
|
||||
val wcwidth = WcWidth.width(codePoint)
|
||||
if (wcwidth > 0) {
|
||||
currentColumn += wcwidth
|
||||
if (currentColumn == column) {
|
||||
while (newCharIndex < _spaceUsed) {
|
||||
if (Character.isHighSurrogate(text[newCharIndex])) {
|
||||
if (WcWidth.width(Character.toCodePoint(text[newCharIndex], text[newCharIndex + 1])) <= 0) {
|
||||
newCharIndex += 2
|
||||
} else {
|
||||
break
|
||||
}
|
||||
} else if (WcWidth.width(text[newCharIndex].code) <= 0) {
|
||||
newCharIndex++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return newCharIndex
|
||||
} else if (currentColumn > column) {
|
||||
return currentCharIndex
|
||||
}
|
||||
}
|
||||
currentCharIndex = newCharIndex
|
||||
}
|
||||
}
|
||||
|
||||
private fun wideDisplayCharacterStartingAt(column: Int): Boolean {
|
||||
var currentCharIndex = 0
|
||||
var currentColumn = 0
|
||||
while (currentCharIndex < _spaceUsed) {
|
||||
val c = text[currentCharIndex++]
|
||||
val codePoint = if (Character.isHighSurrogate(c)) Character.toCodePoint(c, text[currentCharIndex++]) else c.code
|
||||
val wcwidth = WcWidth.width(codePoint)
|
||||
if (wcwidth > 0) {
|
||||
if (currentColumn == column && wcwidth == 2) return true
|
||||
currentColumn += wcwidth
|
||||
if (currentColumn > column) return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun clear(style: Long) {
|
||||
Arrays.fill(text, ' ')
|
||||
Arrays.fill(styles, style)
|
||||
_spaceUsed = columns.toShort()
|
||||
hasNonOneWidthOrSurrogateChars = false
|
||||
}
|
||||
|
||||
// https://github.com/steven676/Android-Terminal-Emulator/commit/9a47042620bec87617f0b4f5d50568535668fe26
|
||||
fun setChar(columnToSet: Int, codePoint: Int, style: Long) {
|
||||
if (columnToSet < 0 || columnToSet >= styles.size)
|
||||
throw IllegalArgumentException("TerminalRow.setChar(): columnToSet=$columnToSet, codePoint=$codePoint, style=$style")
|
||||
|
||||
styles[columnToSet] = style
|
||||
|
||||
val newCodePointDisplayWidth = WcWidth.width(codePoint)
|
||||
|
||||
// Fast path when we don't have any chars with width != 1
|
||||
if (!hasNonOneWidthOrSurrogateChars) {
|
||||
if (codePoint >= Character.MIN_SUPPLEMENTARY_CODE_POINT || newCodePointDisplayWidth != 1) {
|
||||
hasNonOneWidthOrSurrogateChars = true
|
||||
} else {
|
||||
text[columnToSet] = codePoint.toChar()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val newIsCombining = newCodePointDisplayWidth <= 0
|
||||
|
||||
val wasExtraColForWideChar = columnToSet > 0 && wideDisplayCharacterStartingAt(columnToSet - 1)
|
||||
|
||||
var col = columnToSet
|
||||
if (newIsCombining) {
|
||||
if (wasExtraColForWideChar) col--
|
||||
} else {
|
||||
if (wasExtraColForWideChar) setChar(col - 1, ' '.code, style)
|
||||
val overwritingWideCharInNextColumn = newCodePointDisplayWidth == 2 && wideDisplayCharacterStartingAt(col + 1)
|
||||
if (overwritingWideCharInNextColumn) setChar(col + 1, ' '.code, style)
|
||||
}
|
||||
|
||||
var textArray = text
|
||||
val oldStartOfColumnIndex = findStartOfColumn(col)
|
||||
val oldCodePointDisplayWidth = WcWidth.width(textArray, oldStartOfColumnIndex)
|
||||
|
||||
val oldCharactersUsedForColumn: Int
|
||||
if (col + oldCodePointDisplayWidth < columns) {
|
||||
val oldEndOfColumnIndex = findStartOfColumn(col + oldCodePointDisplayWidth)
|
||||
oldCharactersUsedForColumn = oldEndOfColumnIndex - oldStartOfColumnIndex
|
||||
} else {
|
||||
oldCharactersUsedForColumn = _spaceUsed - oldStartOfColumnIndex
|
||||
}
|
||||
|
||||
if (newIsCombining) {
|
||||
val combiningCharsCount = WcWidth.zeroWidthCharsCount(textArray, oldStartOfColumnIndex, oldStartOfColumnIndex + oldCharactersUsedForColumn)
|
||||
if (combiningCharsCount >= MAX_COMBINING_CHARACTERS_PER_COLUMN)
|
||||
return
|
||||
}
|
||||
|
||||
var newCharactersUsedForColumn = Character.charCount(codePoint)
|
||||
if (newIsCombining) {
|
||||
newCharactersUsedForColumn += oldCharactersUsedForColumn
|
||||
}
|
||||
|
||||
val oldNextColumnIndex = oldStartOfColumnIndex + oldCharactersUsedForColumn
|
||||
val newNextColumnIndex = oldStartOfColumnIndex + newCharactersUsedForColumn
|
||||
|
||||
val javaCharDifference = newCharactersUsedForColumn - oldCharactersUsedForColumn
|
||||
if (javaCharDifference > 0) {
|
||||
val oldCharactersAfterColumn = _spaceUsed - oldNextColumnIndex
|
||||
if (_spaceUsed + javaCharDifference > textArray.size) {
|
||||
val newText = CharArray(textArray.size + columns)
|
||||
System.arraycopy(textArray, 0, newText, 0, oldNextColumnIndex)
|
||||
System.arraycopy(textArray, oldNextColumnIndex, newText, newNextColumnIndex, oldCharactersAfterColumn)
|
||||
text = newText
|
||||
textArray = newText
|
||||
} else {
|
||||
System.arraycopy(textArray, oldNextColumnIndex, textArray, newNextColumnIndex, oldCharactersAfterColumn)
|
||||
}
|
||||
} else if (javaCharDifference < 0) {
|
||||
System.arraycopy(textArray, oldNextColumnIndex, textArray, newNextColumnIndex, _spaceUsed - oldNextColumnIndex)
|
||||
}
|
||||
_spaceUsed = (_spaceUsed + javaCharDifference).toShort()
|
||||
|
||||
Character.toChars(codePoint, textArray, oldStartOfColumnIndex + if (newIsCombining) oldCharactersUsedForColumn else 0)
|
||||
|
||||
if (oldCodePointDisplayWidth == 2 && newCodePointDisplayWidth == 1) {
|
||||
if (_spaceUsed + 1 > textArray.size) {
|
||||
val newText = CharArray(textArray.size + columns)
|
||||
System.arraycopy(textArray, 0, newText, 0, newNextColumnIndex)
|
||||
System.arraycopy(textArray, newNextColumnIndex, newText, newNextColumnIndex + 1, _spaceUsed - newNextColumnIndex)
|
||||
text = newText
|
||||
textArray = newText
|
||||
} else {
|
||||
System.arraycopy(textArray, newNextColumnIndex, textArray, newNextColumnIndex + 1, _spaceUsed - newNextColumnIndex)
|
||||
}
|
||||
textArray[newNextColumnIndex] = ' '
|
||||
++_spaceUsed
|
||||
} else if (oldCodePointDisplayWidth == 1 && newCodePointDisplayWidth == 2) {
|
||||
if (col == columns - 1) {
|
||||
throw IllegalArgumentException("Cannot put wide character in last column")
|
||||
} else if (col == columns - 2) {
|
||||
_spaceUsed = newNextColumnIndex.toShort()
|
||||
} else {
|
||||
val newNextNextColumnIndex = newNextColumnIndex + if (Character.isHighSurrogate(textArray[newNextColumnIndex])) 2 else 1
|
||||
val nextLen = newNextNextColumnIndex - newNextColumnIndex
|
||||
System.arraycopy(textArray, newNextNextColumnIndex, textArray, newNextColumnIndex, _spaceUsed - newNextNextColumnIndex)
|
||||
_spaceUsed = (_spaceUsed - nextLen).toShort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isBlank(): Boolean {
|
||||
for (charIndex in 0 until spaceUsed)
|
||||
if (text[charIndex] != ' ') return false
|
||||
return true
|
||||
}
|
||||
|
||||
fun getStyle(column: Int): Long = styles[column]
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
import android.graphics.Color
|
||||
import java.util.Properties
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
object TextStyle {
|
||||
|
||||
const val CHARACTER_ATTRIBUTE_BOLD = 1
|
||||
const val CHARACTER_ATTRIBUTE_ITALIC = 1 shl 1
|
||||
const val CHARACTER_ATTRIBUTE_UNDERLINE = 1 shl 2
|
||||
const val CHARACTER_ATTRIBUTE_BLINK = 1 shl 3
|
||||
const val CHARACTER_ATTRIBUTE_INVERSE = 1 shl 4
|
||||
const val CHARACTER_ATTRIBUTE_INVISIBLE = 1 shl 5
|
||||
const val CHARACTER_ATTRIBUTE_STRIKETHROUGH = 1 shl 6
|
||||
const val CHARACTER_ATTRIBUTE_PROTECTED = 1 shl 7
|
||||
const val CHARACTER_ATTRIBUTE_DIM = 1 shl 8
|
||||
private const val CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND = 1 shl 9
|
||||
private const val CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND = 1 shl 10
|
||||
|
||||
const val COLOR_INDEX_FOREGROUND = 256
|
||||
const val COLOR_INDEX_BACKGROUND = 257
|
||||
const val COLOR_INDEX_CURSOR = 258
|
||||
const val NUM_INDEXED_COLORS = 259
|
||||
|
||||
val NORMAL = encode(COLOR_INDEX_FOREGROUND, COLOR_INDEX_BACKGROUND, 0)
|
||||
|
||||
fun encode(foreColor: Int, backColor: Int, effect: Int): Long {
|
||||
var result = (effect and 0b111111111).toLong()
|
||||
if (foreColor and 0xff000000.toInt() == 0xff000000.toInt()) {
|
||||
result = result or CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND.toLong() or ((foreColor.toLong() and 0x00ffffffL) shl 40)
|
||||
} else {
|
||||
result = result or ((foreColor.toLong() and 0b111111111L) shl 40)
|
||||
}
|
||||
if (backColor and 0xff000000.toInt() == 0xff000000.toInt()) {
|
||||
result = result or CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND.toLong() or ((backColor.toLong() and 0x00ffffffL) shl 16)
|
||||
} else {
|
||||
result = result or ((backColor.toLong() and 0b111111111L) shl 16)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun decodeForeColor(style: Long): Int {
|
||||
return if (style and CHARACTER_ATTRIBUTE_TRUECOLOR_FOREGROUND.toLong() == 0L) {
|
||||
((style ushr 40) and 0b111111111L).toInt()
|
||||
} else {
|
||||
0xff000000.toInt() or ((style ushr 40) and 0x00ffffffL).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeBackColor(style: Long): Int {
|
||||
return if (style and CHARACTER_ATTRIBUTE_TRUECOLOR_BACKGROUND.toLong() == 0L) {
|
||||
((style ushr 16) and 0b111111111L).toInt()
|
||||
} else {
|
||||
0xff000000.toInt() or ((style ushr 16) and 0x00ffffffL).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeEffect(style: Long): Int {
|
||||
return (style and 0b11111111111L).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using
|
||||
* Operating System Control (OSC) sequences.
|
||||
*/
|
||||
class TerminalColorScheme {
|
||||
|
||||
val defaultColors: IntArray = IntArray(TextStyle.NUM_INDEXED_COLORS)
|
||||
|
||||
init {
|
||||
reset()
|
||||
}
|
||||
|
||||
fun updateWith(props: Properties) {
|
||||
reset()
|
||||
var cursorPropExists = false
|
||||
for ((keyObj, valueObj) in props) {
|
||||
val key = keyObj as String
|
||||
val value = valueObj as String
|
||||
val colorIndex: Int = when {
|
||||
key == "foreground" -> TextStyle.COLOR_INDEX_FOREGROUND
|
||||
key == "background" -> TextStyle.COLOR_INDEX_BACKGROUND
|
||||
key == "cursor" -> {
|
||||
cursorPropExists = true
|
||||
TextStyle.COLOR_INDEX_CURSOR
|
||||
}
|
||||
key.startsWith("color") -> {
|
||||
try {
|
||||
key.substring(5).toInt()
|
||||
} catch (_: NumberFormatException) {
|
||||
throw IllegalArgumentException("Invalid property: '$key'")
|
||||
}
|
||||
}
|
||||
else -> throw IllegalArgumentException("Invalid property: '$key'")
|
||||
}
|
||||
|
||||
val colorValue = TerminalColors.parse(value)
|
||||
if (colorValue == 0) {
|
||||
throw IllegalArgumentException("Property '$key' has invalid color: '$value'")
|
||||
}
|
||||
|
||||
defaultColors[colorIndex] = colorValue
|
||||
}
|
||||
|
||||
if (!cursorPropExists) {
|
||||
setCursorColorForBackground()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCursorColorForBackground() {
|
||||
val backgroundColor = defaultColors[TextStyle.COLOR_INDEX_BACKGROUND]
|
||||
val brightness = TerminalColors.perceivedBrightness(backgroundColor)
|
||||
if (brightness > 0) {
|
||||
defaultColors[TextStyle.COLOR_INDEX_CURSOR] = if (brightness < 130) {
|
||||
0xffffffff.toInt()
|
||||
} else {
|
||||
0xff000000.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reset() {
|
||||
System.arraycopy(DEFAULT_COLORSCHEME, 0, defaultColors, 0, TextStyle.NUM_INDEXED_COLORS)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_COLORSCHEME = longArrayOf(
|
||||
// 16 original colors. First 8 are dim.
|
||||
0xff000000, // black
|
||||
0xffcd0000, // dim red
|
||||
0xff00cd00, // dim green
|
||||
0xffcdcd00, // dim yellow
|
||||
0xff6495ed, // dim blue
|
||||
0xffcd00cd, // dim magenta
|
||||
0xff00cdcd, // dim cyan
|
||||
0xffe5e5e5, // dim white
|
||||
// Second 8 are bright:
|
||||
0xff7f7f7f, // medium grey
|
||||
0xffff0000, // bright red
|
||||
0xff00ff00, // bright green
|
||||
0xffffff00, // bright yellow
|
||||
0xff5c5cff, // light blue
|
||||
0xffff00ff, // bright magenta
|
||||
0xff00ffff, // bright cyan
|
||||
0xffffffffL, // bright white
|
||||
|
||||
// 216 color cube, six shades of each color:
|
||||
0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff,
|
||||
0xff008700, 0xff00875f, 0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff,
|
||||
0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, 0xff00ffff,
|
||||
0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff,
|
||||
0xff5f8700, 0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff,
|
||||
0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, 0xff5fffd7, 0xff5fffff,
|
||||
0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff,
|
||||
0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff,
|
||||
0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, 0xff87ffaf, 0xff87ffd7, 0xff87ffff,
|
||||
0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff,
|
||||
0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff,
|
||||
0xffafd700, 0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff,
|
||||
0xffd70000, 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, 0xffd75fff,
|
||||
0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff,
|
||||
0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff,
|
||||
0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, 0xffff5fd7, 0xffff5fff,
|
||||
0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff,
|
||||
0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffffL,
|
||||
|
||||
// 24 grey scale ramp:
|
||||
0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676,
|
||||
0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
|
||||
|
||||
// COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
|
||||
0xffffffffL, 0xff000000L, 0xffffffffL
|
||||
).map { it.toInt() }.toIntArray()
|
||||
}
|
||||
}
|
||||
|
||||
/** Current terminal colors (if different from default). */
|
||||
class TerminalColors {
|
||||
|
||||
val currentColors: IntArray = IntArray(TextStyle.NUM_INDEXED_COLORS)
|
||||
|
||||
init {
|
||||
reset()
|
||||
}
|
||||
|
||||
fun reset(index: Int) {
|
||||
currentColors[index] = COLOR_SCHEME.defaultColors[index]
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
System.arraycopy(COLOR_SCHEME.defaultColors, 0, currentColors, 0, TextStyle.NUM_INDEXED_COLORS)
|
||||
}
|
||||
|
||||
fun tryParseColor(intoIndex: Int, textParameter: String) {
|
||||
val c = parse(textParameter)
|
||||
if (c != 0) currentColors[intoIndex] = c
|
||||
}
|
||||
|
||||
companion object {
|
||||
val COLOR_SCHEME = TerminalColorScheme()
|
||||
|
||||
internal fun parse(c: String): Int {
|
||||
return try {
|
||||
val (skipInitial, skipBetween) = when {
|
||||
c[0] == '#' -> 1 to 0
|
||||
c.startsWith("rgb:") -> 4 to 1
|
||||
else -> return 0
|
||||
}
|
||||
val charsForColors = c.length - skipInitial - 2 * skipBetween
|
||||
if (charsForColors % 3 != 0) return 0
|
||||
val componentLength = charsForColors / 3
|
||||
val mult = 255.0 / (2.0.pow(componentLength * 4) - 1)
|
||||
|
||||
var currentPosition = skipInitial
|
||||
val rString = c.substring(currentPosition, currentPosition + componentLength)
|
||||
currentPosition += componentLength + skipBetween
|
||||
val gString = c.substring(currentPosition, currentPosition + componentLength)
|
||||
currentPosition += componentLength + skipBetween
|
||||
val bString = c.substring(currentPosition, currentPosition + componentLength)
|
||||
|
||||
val r = (rString.toInt(16) * mult).toInt()
|
||||
val g = (gString.toInt(16) * mult).toInt()
|
||||
val b = (bString.toInt(16) * mult).toInt()
|
||||
(0xFF shl 24) or (r shl 16) or (g shl 8) or b
|
||||
} catch (_: NumberFormatException) {
|
||||
0
|
||||
} catch (_: IndexOutOfBoundsException) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fun perceivedBrightness(color: Int): Int {
|
||||
return floor(
|
||||
sqrt(
|
||||
Color.red(color).toDouble().pow(2) * 0.241 +
|
||||
Color.green(color).toDouble().pow(2) * 0.691 +
|
||||
Color.blue(color).toDouble().pow(2) * 0.068
|
||||
)
|
||||
).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
package com.topjohnwu.magisk.terminal
|
||||
|
||||
/**
|
||||
* Implementation of wcwidth(3) for Unicode 15.
|
||||
*
|
||||
* Implementation from https://github.com/jquast/wcwidth but we return 0 for unprintable characters.
|
||||
*
|
||||
* IMPORTANT:
|
||||
* Must be kept in sync with the following:
|
||||
* https://github.com/termux/wcwidth
|
||||
* https://github.com/termux/libandroid-support
|
||||
* https://github.com/termux/termux-packages/tree/master/packages/libandroid-support
|
||||
*/
|
||||
object WcWidth {
|
||||
|
||||
// From https://github.com/jquast/wcwidth/blob/master/wcwidth/table_zero.py
|
||||
// from https://github.com/jquast/wcwidth/pull/64
|
||||
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
|
||||
private val ZERO_WIDTH = arrayOf(
|
||||
intArrayOf(0x00300, 0x0036f), // Combining Grave Accent ..Combining Latin Small Le
|
||||
intArrayOf(0x00483, 0x00489), // Combining Cyrillic Titlo..Combining Cyrillic Milli
|
||||
intArrayOf(0x00591, 0x005bd), // Hebrew Accent Etnahta ..Hebrew Point Meteg
|
||||
intArrayOf(0x005bf, 0x005bf), // Hebrew Point Rafe ..Hebrew Point Rafe
|
||||
intArrayOf(0x005c1, 0x005c2), // Hebrew Point Shin Dot ..Hebrew Point Sin Dot
|
||||
intArrayOf(0x005c4, 0x005c5), // Hebrew Mark Upper Dot ..Hebrew Mark Lower Dot
|
||||
intArrayOf(0x005c7, 0x005c7), // Hebrew Point Qamats Qata..Hebrew Point Qamats Qata
|
||||
intArrayOf(0x00610, 0x0061a), // Arabic Sign Sallallahou ..Arabic Small Kasra
|
||||
intArrayOf(0x0064b, 0x0065f), // Arabic Fathatan ..Arabic Wavy Hamza Below
|
||||
intArrayOf(0x00670, 0x00670), // Arabic Letter Superscrip..Arabic Letter Superscrip
|
||||
intArrayOf(0x006d6, 0x006dc), // Arabic Small High Ligatu..Arabic Small High Seen
|
||||
intArrayOf(0x006df, 0x006e4), // Arabic Small High Rounde..Arabic Small High Madda
|
||||
intArrayOf(0x006e7, 0x006e8), // Arabic Small High Yeh ..Arabic Small High Noon
|
||||
intArrayOf(0x006ea, 0x006ed), // Arabic Empty Centre Low ..Arabic Small Low Meem
|
||||
intArrayOf(0x00711, 0x00711), // Syriac Letter Superscrip..Syriac Letter Superscrip
|
||||
intArrayOf(0x00730, 0x0074a), // Syriac Pthaha Above ..Syriac Barrekh
|
||||
intArrayOf(0x007a6, 0x007b0), // Thaana Abafili ..Thaana Sukun
|
||||
intArrayOf(0x007eb, 0x007f3), // Nko Combining Short High..Nko Combining Double Dot
|
||||
intArrayOf(0x007fd, 0x007fd), // Nko Dantayalan ..Nko Dantayalan
|
||||
intArrayOf(0x00816, 0x00819), // Samaritan Mark In ..Samaritan Mark Dagesh
|
||||
intArrayOf(0x0081b, 0x00823), // Samaritan Mark Epentheti..Samaritan Vowel Sign A
|
||||
intArrayOf(0x00825, 0x00827), // Samaritan Vowel Sign Sho..Samaritan Vowel Sign U
|
||||
intArrayOf(0x00829, 0x0082d), // Samaritan Vowel Sign Lon..Samaritan Mark Nequdaa
|
||||
intArrayOf(0x00859, 0x0085b), // Mandaic Affrication Mark..Mandaic Gemination Mark
|
||||
intArrayOf(0x00898, 0x0089f), // Arabic Small High Word A..Arabic Half Madda Over M
|
||||
intArrayOf(0x008ca, 0x008e1), // Arabic Small High Farsi ..Arabic Small High Sign S
|
||||
intArrayOf(0x008e3, 0x00902), // Arabic Turned Damma Belo..Devanagari Sign Anusvara
|
||||
intArrayOf(0x0093a, 0x0093a), // Devanagari Vowel Sign Oe..Devanagari Vowel Sign Oe
|
||||
intArrayOf(0x0093c, 0x0093c), // Devanagari Sign Nukta ..Devanagari Sign Nukta
|
||||
intArrayOf(0x00941, 0x00948), // Devanagari Vowel Sign U ..Devanagari Vowel Sign Ai
|
||||
intArrayOf(0x0094d, 0x0094d), // Devanagari Sign Virama ..Devanagari Sign Virama
|
||||
intArrayOf(0x00951, 0x00957), // Devanagari Stress Sign U..Devanagari Vowel Sign Uu
|
||||
intArrayOf(0x00962, 0x00963), // Devanagari Vowel Sign Vo..Devanagari Vowel Sign Vo
|
||||
intArrayOf(0x00981, 0x00981), // Bengali Sign Candrabindu..Bengali Sign Candrabindu
|
||||
intArrayOf(0x009bc, 0x009bc), // Bengali Sign Nukta ..Bengali Sign Nukta
|
||||
intArrayOf(0x009c1, 0x009c4), // Bengali Vowel Sign U ..Bengali Vowel Sign Vocal
|
||||
intArrayOf(0x009cd, 0x009cd), // Bengali Sign Virama ..Bengali Sign Virama
|
||||
intArrayOf(0x009e2, 0x009e3), // Bengali Vowel Sign Vocal..Bengali Vowel Sign Vocal
|
||||
intArrayOf(0x009fe, 0x009fe), // Bengali Sandhi Mark ..Bengali Sandhi Mark
|
||||
intArrayOf(0x00a01, 0x00a02), // Gurmukhi Sign Adak Bindi..Gurmukhi Sign Bindi
|
||||
intArrayOf(0x00a3c, 0x00a3c), // Gurmukhi Sign Nukta ..Gurmukhi Sign Nukta
|
||||
intArrayOf(0x00a41, 0x00a42), // Gurmukhi Vowel Sign U ..Gurmukhi Vowel Sign Uu
|
||||
intArrayOf(0x00a47, 0x00a48), // Gurmukhi Vowel Sign Ee ..Gurmukhi Vowel Sign Ai
|
||||
intArrayOf(0x00a4b, 0x00a4d), // Gurmukhi Vowel Sign Oo ..Gurmukhi Sign Virama
|
||||
intArrayOf(0x00a51, 0x00a51), // Gurmukhi Sign Udaat ..Gurmukhi Sign Udaat
|
||||
intArrayOf(0x00a70, 0x00a71), // Gurmukhi Tippi ..Gurmukhi Addak
|
||||
intArrayOf(0x00a75, 0x00a75), // Gurmukhi Sign Yakash ..Gurmukhi Sign Yakash
|
||||
intArrayOf(0x00a81, 0x00a82), // Gujarati Sign Candrabind..Gujarati Sign Anusvara
|
||||
intArrayOf(0x00abc, 0x00abc), // Gujarati Sign Nukta ..Gujarati Sign Nukta
|
||||
intArrayOf(0x00ac1, 0x00ac5), // Gujarati Vowel Sign U ..Gujarati Vowel Sign Cand
|
||||
intArrayOf(0x00ac7, 0x00ac8), // Gujarati Vowel Sign E ..Gujarati Vowel Sign Ai
|
||||
intArrayOf(0x00acd, 0x00acd), // Gujarati Sign Virama ..Gujarati Sign Virama
|
||||
intArrayOf(0x00ae2, 0x00ae3), // Gujarati Vowel Sign Voca..Gujarati Vowel Sign Voca
|
||||
intArrayOf(0x00afa, 0x00aff), // Gujarati Sign Sukun ..Gujarati Sign Two-circle
|
||||
intArrayOf(0x00b01, 0x00b01), // Oriya Sign Candrabindu ..Oriya Sign Candrabindu
|
||||
intArrayOf(0x00b3c, 0x00b3c), // Oriya Sign Nukta ..Oriya Sign Nukta
|
||||
intArrayOf(0x00b3f, 0x00b3f), // Oriya Vowel Sign I ..Oriya Vowel Sign I
|
||||
intArrayOf(0x00b41, 0x00b44), // Oriya Vowel Sign U ..Oriya Vowel Sign Vocalic
|
||||
intArrayOf(0x00b4d, 0x00b4d), // Oriya Sign Virama ..Oriya Sign Virama
|
||||
intArrayOf(0x00b55, 0x00b56), // Oriya Sign Overline ..Oriya Ai Length Mark
|
||||
intArrayOf(0x00b62, 0x00b63), // Oriya Vowel Sign Vocalic..Oriya Vowel Sign Vocalic
|
||||
intArrayOf(0x00b82, 0x00b82), // Tamil Sign Anusvara ..Tamil Sign Anusvara
|
||||
intArrayOf(0x00bc0, 0x00bc0), // Tamil Vowel Sign Ii ..Tamil Vowel Sign Ii
|
||||
intArrayOf(0x00bcd, 0x00bcd), // Tamil Sign Virama ..Tamil Sign Virama
|
||||
intArrayOf(0x00c00, 0x00c00), // Telugu Sign Combining Ca..Telugu Sign Combining Ca
|
||||
intArrayOf(0x00c04, 0x00c04), // Telugu Sign Combining An..Telugu Sign Combining An
|
||||
intArrayOf(0x00c3c, 0x00c3c), // Telugu Sign Nukta ..Telugu Sign Nukta
|
||||
intArrayOf(0x00c3e, 0x00c40), // Telugu Vowel Sign Aa ..Telugu Vowel Sign Ii
|
||||
intArrayOf(0x00c46, 0x00c48), // Telugu Vowel Sign E ..Telugu Vowel Sign Ai
|
||||
intArrayOf(0x00c4a, 0x00c4d), // Telugu Vowel Sign O ..Telugu Sign Virama
|
||||
intArrayOf(0x00c55, 0x00c56), // Telugu Length Mark ..Telugu Ai Length Mark
|
||||
intArrayOf(0x00c62, 0x00c63), // Telugu Vowel Sign Vocali..Telugu Vowel Sign Vocali
|
||||
intArrayOf(0x00c81, 0x00c81), // Kannada Sign Candrabindu..Kannada Sign Candrabindu
|
||||
intArrayOf(0x00cbc, 0x00cbc), // Kannada Sign Nukta ..Kannada Sign Nukta
|
||||
intArrayOf(0x00cbf, 0x00cbf), // Kannada Vowel Sign I ..Kannada Vowel Sign I
|
||||
intArrayOf(0x00cc6, 0x00cc6), // Kannada Vowel Sign E ..Kannada Vowel Sign E
|
||||
intArrayOf(0x00ccc, 0x00ccd), // Kannada Vowel Sign Au ..Kannada Sign Virama
|
||||
intArrayOf(0x00ce2, 0x00ce3), // Kannada Vowel Sign Vocal..Kannada Vowel Sign Vocal
|
||||
intArrayOf(0x00d00, 0x00d01), // Malayalam Sign Combining..Malayalam Sign Candrabin
|
||||
intArrayOf(0x00d3b, 0x00d3c), // Malayalam Sign Vertical ..Malayalam Sign Circular
|
||||
intArrayOf(0x00d41, 0x00d44), // Malayalam Vowel Sign U ..Malayalam Vowel Sign Voc
|
||||
intArrayOf(0x00d4d, 0x00d4d), // Malayalam Sign Virama ..Malayalam Sign Virama
|
||||
intArrayOf(0x00d62, 0x00d63), // Malayalam Vowel Sign Voc..Malayalam Vowel Sign Voc
|
||||
intArrayOf(0x00d81, 0x00d81), // Sinhala Sign Candrabindu..Sinhala Sign Candrabindu
|
||||
intArrayOf(0x00dca, 0x00dca), // Sinhala Sign Al-lakuna ..Sinhala Sign Al-lakuna
|
||||
intArrayOf(0x00dd2, 0x00dd4), // Sinhala Vowel Sign Ketti..Sinhala Vowel Sign Ketti
|
||||
intArrayOf(0x00dd6, 0x00dd6), // Sinhala Vowel Sign Diga ..Sinhala Vowel Sign Diga
|
||||
intArrayOf(0x00e31, 0x00e31), // Thai Character Mai Han-a..Thai Character Mai Han-a
|
||||
intArrayOf(0x00e34, 0x00e3a), // Thai Character Sara I ..Thai Character Phinthu
|
||||
intArrayOf(0x00e47, 0x00e4e), // Thai Character Maitaikhu..Thai Character Yamakkan
|
||||
intArrayOf(0x00eb1, 0x00eb1), // Lao Vowel Sign Mai Kan ..Lao Vowel Sign Mai Kan
|
||||
intArrayOf(0x00eb4, 0x00ebc), // Lao Vowel Sign I ..Lao Semivowel Sign Lo
|
||||
intArrayOf(0x00ec8, 0x00ece), // Lao Tone Mai Ek ..(nil)
|
||||
intArrayOf(0x00f18, 0x00f19), // Tibetan Astrological Sig..Tibetan Astrological Sig
|
||||
intArrayOf(0x00f35, 0x00f35), // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
intArrayOf(0x00f37, 0x00f37), // Tibetan Mark Ngas Bzung ..Tibetan Mark Ngas Bzung
|
||||
intArrayOf(0x00f39, 0x00f39), // Tibetan Mark Tsa -phru ..Tibetan Mark Tsa -phru
|
||||
intArrayOf(0x00f71, 0x00f7e), // Tibetan Vowel Sign Aa ..Tibetan Sign Rjes Su Nga
|
||||
intArrayOf(0x00f80, 0x00f84), // Tibetan Vowel Sign Rever..Tibetan Mark Halanta
|
||||
intArrayOf(0x00f86, 0x00f87), // Tibetan Sign Lci Rtags ..Tibetan Sign Yang Rtags
|
||||
intArrayOf(0x00f8d, 0x00f97), // Tibetan Subjoined Sign L..Tibetan Subjoined Letter
|
||||
intArrayOf(0x00f99, 0x00fbc), // Tibetan Subjoined Letter..Tibetan Subjoined Letter
|
||||
intArrayOf(0x00fc6, 0x00fc6), // Tibetan Symbol Padma Gda..Tibetan Symbol Padma Gda
|
||||
intArrayOf(0x0102d, 0x01030), // Myanmar Vowel Sign I ..Myanmar Vowel Sign Uu
|
||||
intArrayOf(0x01032, 0x01037), // Myanmar Vowel Sign Ai ..Myanmar Sign Dot Below
|
||||
intArrayOf(0x01039, 0x0103a), // Myanmar Sign Virama ..Myanmar Sign Asat
|
||||
intArrayOf(0x0103d, 0x0103e), // Myanmar Consonant Sign M..Myanmar Consonant Sign M
|
||||
intArrayOf(0x01058, 0x01059), // Myanmar Vowel Sign Vocal..Myanmar Vowel Sign Vocal
|
||||
intArrayOf(0x0105e, 0x01060), // Myanmar Consonant Sign M..Myanmar Consonant Sign M
|
||||
intArrayOf(0x01071, 0x01074), // Myanmar Vowel Sign Geba ..Myanmar Vowel Sign Kayah
|
||||
intArrayOf(0x01082, 0x01082), // Myanmar Consonant Sign S..Myanmar Consonant Sign S
|
||||
intArrayOf(0x01085, 0x01086), // Myanmar Vowel Sign Shan ..Myanmar Vowel Sign Shan
|
||||
intArrayOf(0x0108d, 0x0108d), // Myanmar Sign Shan Counci..Myanmar Sign Shan Counci
|
||||
intArrayOf(0x0109d, 0x0109d), // Myanmar Vowel Sign Aiton..Myanmar Vowel Sign Aiton
|
||||
intArrayOf(0x0135d, 0x0135f), // Ethiopic Combining Gemin..Ethiopic Combining Gemin
|
||||
intArrayOf(0x01712, 0x01714), // Tagalog Vowel Sign I ..Tagalog Sign Virama
|
||||
intArrayOf(0x01732, 0x01733), // Hanunoo Vowel Sign I ..Hanunoo Vowel Sign U
|
||||
intArrayOf(0x01752, 0x01753), // Buhid Vowel Sign I ..Buhid Vowel Sign U
|
||||
intArrayOf(0x01772, 0x01773), // Tagbanwa Vowel Sign I ..Tagbanwa Vowel Sign U
|
||||
intArrayOf(0x017b4, 0x017b5), // Khmer Vowel Inherent Aq ..Khmer Vowel Inherent Aa
|
||||
intArrayOf(0x017b7, 0x017bd), // Khmer Vowel Sign I ..Khmer Vowel Sign Ua
|
||||
intArrayOf(0x017c6, 0x017c6), // Khmer Sign Nikahit ..Khmer Sign Nikahit
|
||||
intArrayOf(0x017c9, 0x017d3), // Khmer Sign Muusikatoan ..Khmer Sign Bathamasat
|
||||
intArrayOf(0x017dd, 0x017dd), // Khmer Sign Atthacan ..Khmer Sign Atthacan
|
||||
intArrayOf(0x0180b, 0x0180d), // Mongolian Free Variation..Mongolian Free Variation
|
||||
intArrayOf(0x0180f, 0x0180f), // Mongolian Free Variation..Mongolian Free Variation
|
||||
intArrayOf(0x01885, 0x01886), // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
intArrayOf(0x018a9, 0x018a9), // Mongolian Letter Ali Gal..Mongolian Letter Ali Gal
|
||||
intArrayOf(0x01920, 0x01922), // Limbu Vowel Sign A ..Limbu Vowel Sign U
|
||||
intArrayOf(0x01927, 0x01928), // Limbu Vowel Sign E ..Limbu Vowel Sign O
|
||||
intArrayOf(0x01932, 0x01932), // Limbu Small Letter Anusv..Limbu Small Letter Anusv
|
||||
intArrayOf(0x01939, 0x0193b), // Limbu Sign Mukphreng ..Limbu Sign Sa-i
|
||||
intArrayOf(0x01a17, 0x01a18), // Buginese Vowel Sign I ..Buginese Vowel Sign U
|
||||
intArrayOf(0x01a1b, 0x01a1b), // Buginese Vowel Sign Ae ..Buginese Vowel Sign Ae
|
||||
intArrayOf(0x01a56, 0x01a56), // Tai Tham Consonant Sign ..Tai Tham Consonant Sign
|
||||
intArrayOf(0x01a58, 0x01a5e), // Tai Tham Sign Mai Kang L..Tai Tham Consonant Sign
|
||||
intArrayOf(0x01a60, 0x01a60), // Tai Tham Sign Sakot ..Tai Tham Sign Sakot
|
||||
intArrayOf(0x01a62, 0x01a62), // Tai Tham Vowel Sign Mai ..Tai Tham Vowel Sign Mai
|
||||
intArrayOf(0x01a65, 0x01a6c), // Tai Tham Vowel Sign I ..Tai Tham Vowel Sign Oa B
|
||||
intArrayOf(0x01a73, 0x01a7c), // Tai Tham Vowel Sign Oa A..Tai Tham Sign Khuen-lue
|
||||
intArrayOf(0x01a7f, 0x01a7f), // Tai Tham Combining Crypt..Tai Tham Combining Crypt
|
||||
intArrayOf(0x01ab0, 0x01ace), // Combining Doubled Circum..Combining Latin Small Le
|
||||
intArrayOf(0x01b00, 0x01b03), // Balinese Sign Ulu Ricem ..Balinese Sign Surang
|
||||
intArrayOf(0x01b34, 0x01b34), // Balinese Sign Rerekan ..Balinese Sign Rerekan
|
||||
intArrayOf(0x01b36, 0x01b3a), // Balinese Vowel Sign Ulu ..Balinese Vowel Sign Ra R
|
||||
intArrayOf(0x01b3c, 0x01b3c), // Balinese Vowel Sign La L..Balinese Vowel Sign La L
|
||||
intArrayOf(0x01b42, 0x01b42), // Balinese Vowel Sign Pepe..Balinese Vowel Sign Pepe
|
||||
intArrayOf(0x01b6b, 0x01b73), // Balinese Musical Symbol ..Balinese Musical Symbol
|
||||
intArrayOf(0x01b80, 0x01b81), // Sundanese Sign Panyecek ..Sundanese Sign Panglayar
|
||||
intArrayOf(0x01ba2, 0x01ba5), // Sundanese Consonant Sign..Sundanese Vowel Sign Pan
|
||||
intArrayOf(0x01ba8, 0x01ba9), // Sundanese Vowel Sign Pam..Sundanese Vowel Sign Pan
|
||||
intArrayOf(0x01bab, 0x01bad), // Sundanese Sign Virama ..Sundanese Consonant Sign
|
||||
intArrayOf(0x01be6, 0x01be6), // Batak Sign Tompi ..Batak Sign Tompi
|
||||
intArrayOf(0x01be8, 0x01be9), // Batak Vowel Sign Pakpak ..Batak Vowel Sign Ee
|
||||
intArrayOf(0x01bed, 0x01bed), // Batak Vowel Sign Karo O ..Batak Vowel Sign Karo O
|
||||
intArrayOf(0x01bef, 0x01bf1), // Batak Vowel Sign U For S..Batak Consonant Sign H
|
||||
intArrayOf(0x01c2c, 0x01c33), // Lepcha Vowel Sign E ..Lepcha Consonant Sign T
|
||||
intArrayOf(0x01c36, 0x01c37), // Lepcha Sign Ran ..Lepcha Sign Nukta
|
||||
intArrayOf(0x01cd0, 0x01cd2), // Vedic Tone Karshana ..Vedic Tone Prenkha
|
||||
intArrayOf(0x01cd4, 0x01ce0), // Vedic Sign Yajurvedic Mi..Vedic Tone Rigvedic Kash
|
||||
intArrayOf(0x01ce2, 0x01ce8), // Vedic Sign Visarga Svari..Vedic Sign Visarga Anuda
|
||||
intArrayOf(0x01ced, 0x01ced), // Vedic Sign Tiryak ..Vedic Sign Tiryak
|
||||
intArrayOf(0x01cf4, 0x01cf4), // Vedic Tone Candra Above ..Vedic Tone Candra Above
|
||||
intArrayOf(0x01cf8, 0x01cf9), // Vedic Tone Ring Above ..Vedic Tone Double Ring A
|
||||
intArrayOf(0x01dc0, 0x01dff), // Combining Dotted Grave A..Combining Right Arrowhea
|
||||
intArrayOf(0x020d0, 0x020f0), // Combining Left Harpoon A..Combining Asterisk Above
|
||||
intArrayOf(0x02cef, 0x02cf1), // Coptic Combining Ni Abov..Coptic Combining Spiritu
|
||||
intArrayOf(0x02d7f, 0x02d7f), // Tifinagh Consonant Joine..Tifinagh Consonant Joine
|
||||
intArrayOf(0x02de0, 0x02dff), // Combining Cyrillic Lette..Combining Cyrillic Lette
|
||||
intArrayOf(0x0302a, 0x0302d), // Ideographic Level Tone M..Ideographic Entering Ton
|
||||
intArrayOf(0x03099, 0x0309a), // Combining Katakana-hirag..Combining Katakana-hirag
|
||||
intArrayOf(0x0a66f, 0x0a672), // Combining Cyrillic Vzmet..Combining Cyrillic Thous
|
||||
intArrayOf(0x0a674, 0x0a67d), // Combining Cyrillic Lette..Combining Cyrillic Payer
|
||||
intArrayOf(0x0a69e, 0x0a69f), // Combining Cyrillic Lette..Combining Cyrillic Lette
|
||||
intArrayOf(0x0a6f0, 0x0a6f1), // Bamum Combining Mark Koq..Bamum Combining Mark Tuk
|
||||
intArrayOf(0x0a802, 0x0a802), // Syloti Nagri Sign Dvisva..Syloti Nagri Sign Dvisva
|
||||
intArrayOf(0x0a806, 0x0a806), // Syloti Nagri Sign Hasant..Syloti Nagri Sign Hasant
|
||||
intArrayOf(0x0a80b, 0x0a80b), // Syloti Nagri Sign Anusva..Syloti Nagri Sign Anusva
|
||||
intArrayOf(0x0a825, 0x0a826), // Syloti Nagri Vowel Sign ..Syloti Nagri Vowel Sign
|
||||
intArrayOf(0x0a82c, 0x0a82c), // Syloti Nagri Sign Altern..Syloti Nagri Sign Altern
|
||||
intArrayOf(0x0a8c4, 0x0a8c5), // Saurashtra Sign Virama ..Saurashtra Sign Candrabi
|
||||
intArrayOf(0x0a8e0, 0x0a8f1), // Combining Devanagari Dig..Combining Devanagari Sig
|
||||
intArrayOf(0x0a8ff, 0x0a8ff), // Devanagari Vowel Sign Ay..Devanagari Vowel Sign Ay
|
||||
intArrayOf(0x0a926, 0x0a92d), // Kayah Li Vowel Ue ..Kayah Li Tone Calya Plop
|
||||
intArrayOf(0x0a947, 0x0a951), // Rejang Vowel Sign I ..Rejang Consonant Sign R
|
||||
intArrayOf(0x0a980, 0x0a982), // Javanese Sign Panyangga ..Javanese Sign Layar
|
||||
intArrayOf(0x0a9b3, 0x0a9b3), // Javanese Sign Cecak Telu..Javanese Sign Cecak Telu
|
||||
intArrayOf(0x0a9b6, 0x0a9b9), // Javanese Vowel Sign Wulu..Javanese Vowel Sign Suku
|
||||
intArrayOf(0x0a9bc, 0x0a9bd), // Javanese Vowel Sign Pepe..Javanese Consonant Sign
|
||||
intArrayOf(0x0a9e5, 0x0a9e5), // Myanmar Sign Shan Saw ..Myanmar Sign Shan Saw
|
||||
intArrayOf(0x0aa29, 0x0aa2e), // Cham Vowel Sign Aa ..Cham Vowel Sign Oe
|
||||
intArrayOf(0x0aa31, 0x0aa32), // Cham Vowel Sign Au ..Cham Vowel Sign Ue
|
||||
intArrayOf(0x0aa35, 0x0aa36), // Cham Consonant Sign La ..Cham Consonant Sign Wa
|
||||
intArrayOf(0x0aa43, 0x0aa43), // Cham Consonant Sign Fina..Cham Consonant Sign Fina
|
||||
intArrayOf(0x0aa4c, 0x0aa4c), // Cham Consonant Sign Fina..Cham Consonant Sign Fina
|
||||
intArrayOf(0x0aa7c, 0x0aa7c), // Myanmar Sign Tai Laing T..Myanmar Sign Tai Laing T
|
||||
intArrayOf(0x0aab0, 0x0aab0), // Tai Viet Mai Kang ..Tai Viet Mai Kang
|
||||
intArrayOf(0x0aab2, 0x0aab4), // Tai Viet Vowel I ..Tai Viet Vowel U
|
||||
intArrayOf(0x0aab7, 0x0aab8), // Tai Viet Mai Khit ..Tai Viet Vowel Ia
|
||||
intArrayOf(0x0aabe, 0x0aabf), // Tai Viet Vowel Am ..Tai Viet Tone Mai Ek
|
||||
intArrayOf(0x0aac1, 0x0aac1), // Tai Viet Tone Mai Tho ..Tai Viet Tone Mai Tho
|
||||
intArrayOf(0x0aaec, 0x0aaed), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
intArrayOf(0x0aaf6, 0x0aaf6), // Meetei Mayek Virama ..Meetei Mayek Virama
|
||||
intArrayOf(0x0abe5, 0x0abe5), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
intArrayOf(0x0abe8, 0x0abe8), // Meetei Mayek Vowel Sign ..Meetei Mayek Vowel Sign
|
||||
intArrayOf(0x0abed, 0x0abed), // Meetei Mayek Apun Iyek ..Meetei Mayek Apun Iyek
|
||||
intArrayOf(0x0fb1e, 0x0fb1e), // Hebrew Point Judeo-spani..Hebrew Point Judeo-spani
|
||||
intArrayOf(0x0fe00, 0x0fe0f), // Variation Selector-1 ..Variation Selector-16
|
||||
intArrayOf(0x0fe20, 0x0fe2f), // Combining Ligature Left ..Combining Cyrillic Titlo
|
||||
intArrayOf(0x101fd, 0x101fd), // Phaistos Disc Sign Combi..Phaistos Disc Sign Combi
|
||||
intArrayOf(0x102e0, 0x102e0), // Coptic Epact Thousands M..Coptic Epact Thousands M
|
||||
intArrayOf(0x10376, 0x1037a), // Combining Old Permic Let..Combining Old Permic Let
|
||||
intArrayOf(0x10a01, 0x10a03), // Kharoshthi Vowel Sign I ..Kharoshthi Vowel Sign Vo
|
||||
intArrayOf(0x10a05, 0x10a06), // Kharoshthi Vowel Sign E ..Kharoshthi Vowel Sign O
|
||||
intArrayOf(0x10a0c, 0x10a0f), // Kharoshthi Vowel Length ..Kharoshthi Sign Visarga
|
||||
intArrayOf(0x10a38, 0x10a3a), // Kharoshthi Sign Bar Abov..Kharoshthi Sign Dot Belo
|
||||
intArrayOf(0x10a3f, 0x10a3f), // Kharoshthi Virama ..Kharoshthi Virama
|
||||
intArrayOf(0x10ae5, 0x10ae6), // Manichaean Abbreviation ..Manichaean Abbreviation
|
||||
intArrayOf(0x10d24, 0x10d27), // Hanifi Rohingya Sign Har..Hanifi Rohingya Sign Tas
|
||||
intArrayOf(0x10eab, 0x10eac), // Yezidi Combining Hamza M..Yezidi Combining Madda M
|
||||
intArrayOf(0x10efd, 0x10eff), // (nil) ..(nil)
|
||||
intArrayOf(0x10f46, 0x10f50), // Sogdian Combining Dot Be..Sogdian Combining Stroke
|
||||
intArrayOf(0x10f82, 0x10f85), // Old Uyghur Combining Dot..Old Uyghur Combining Two
|
||||
intArrayOf(0x11001, 0x11001), // Brahmi Sign Anusvara ..Brahmi Sign Anusvara
|
||||
intArrayOf(0x11038, 0x11046), // Brahmi Vowel Sign Aa ..Brahmi Virama
|
||||
intArrayOf(0x11070, 0x11070), // Brahmi Sign Old Tamil Vi..Brahmi Sign Old Tamil Vi
|
||||
intArrayOf(0x11073, 0x11074), // Brahmi Vowel Sign Old Ta..Brahmi Vowel Sign Old Ta
|
||||
intArrayOf(0x1107f, 0x11081), // Brahmi Number Joiner ..Kaithi Sign Anusvara
|
||||
intArrayOf(0x110b3, 0x110b6), // Kaithi Vowel Sign U ..Kaithi Vowel Sign Ai
|
||||
intArrayOf(0x110b9, 0x110ba), // Kaithi Sign Virama ..Kaithi Sign Nukta
|
||||
intArrayOf(0x110c2, 0x110c2), // Kaithi Vowel Sign Vocali..Kaithi Vowel Sign Vocali
|
||||
intArrayOf(0x11100, 0x11102), // Chakma Sign Candrabindu ..Chakma Sign Visarga
|
||||
intArrayOf(0x11127, 0x1112b), // Chakma Vowel Sign A ..Chakma Vowel Sign Uu
|
||||
intArrayOf(0x1112d, 0x11134), // Chakma Vowel Sign Ai ..Chakma Maayyaa
|
||||
intArrayOf(0x11173, 0x11173), // Mahajani Sign Nukta ..Mahajani Sign Nukta
|
||||
intArrayOf(0x11180, 0x11181), // Sharada Sign Candrabindu..Sharada Sign Anusvara
|
||||
intArrayOf(0x111b6, 0x111be), // Sharada Vowel Sign U ..Sharada Vowel Sign O
|
||||
intArrayOf(0x111c9, 0x111cc), // Sharada Sandhi Mark ..Sharada Extra Short Vowe
|
||||
intArrayOf(0x111cf, 0x111cf), // Sharada Sign Inverted Ca..Sharada Sign Inverted Ca
|
||||
intArrayOf(0x1122f, 0x11231), // Khojki Vowel Sign U ..Khojki Vowel Sign Ai
|
||||
intArrayOf(0x11234, 0x11234), // Khojki Sign Anusvara ..Khojki Sign Anusvara
|
||||
intArrayOf(0x11236, 0x11237), // Khojki Sign Nukta ..Khojki Sign Shadda
|
||||
intArrayOf(0x1123e, 0x1123e), // Khojki Sign Sukun ..Khojki Sign Sukun
|
||||
intArrayOf(0x11241, 0x11241), // (nil) ..(nil)
|
||||
intArrayOf(0x112df, 0x112df), // Khudawadi Sign Anusvara ..Khudawadi Sign Anusvara
|
||||
intArrayOf(0x112e3, 0x112ea), // Khudawadi Vowel Sign U ..Khudawadi Sign Virama
|
||||
intArrayOf(0x11300, 0x11301), // Grantha Sign Combining A..Grantha Sign Candrabindu
|
||||
intArrayOf(0x1133b, 0x1133c), // Combining Bindu Below ..Grantha Sign Nukta
|
||||
intArrayOf(0x11340, 0x11340), // Grantha Vowel Sign Ii ..Grantha Vowel Sign Ii
|
||||
intArrayOf(0x11366, 0x1136c), // Combining Grantha Digit ..Combining Grantha Digit
|
||||
intArrayOf(0x11370, 0x11374), // Combining Grantha Letter..Combining Grantha Letter
|
||||
intArrayOf(0x11438, 0x1143f), // Newa Vowel Sign U ..Newa Vowel Sign Ai
|
||||
intArrayOf(0x11442, 0x11444), // Newa Sign Virama ..Newa Sign Anusvara
|
||||
intArrayOf(0x11446, 0x11446), // Newa Sign Nukta ..Newa Sign Nukta
|
||||
intArrayOf(0x1145e, 0x1145e), // Newa Sandhi Mark ..Newa Sandhi Mark
|
||||
intArrayOf(0x114b3, 0x114b8), // Tirhuta Vowel Sign U ..Tirhuta Vowel Sign Vocal
|
||||
intArrayOf(0x114ba, 0x114ba), // Tirhuta Vowel Sign Short..Tirhuta Vowel Sign Short
|
||||
intArrayOf(0x114bf, 0x114c0), // Tirhuta Sign Candrabindu..Tirhuta Sign Anusvara
|
||||
intArrayOf(0x114c2, 0x114c3), // Tirhuta Sign Virama ..Tirhuta Sign Nukta
|
||||
intArrayOf(0x115b2, 0x115b5), // Siddham Vowel Sign U ..Siddham Vowel Sign Vocal
|
||||
intArrayOf(0x115bc, 0x115bd), // Siddham Sign Candrabindu..Siddham Sign Anusvara
|
||||
intArrayOf(0x115bf, 0x115c0), // Siddham Sign Virama ..Siddham Sign Nukta
|
||||
intArrayOf(0x115dc, 0x115dd), // Siddham Vowel Sign Alter..Siddham Vowel Sign Alter
|
||||
intArrayOf(0x11633, 0x1163a), // Modi Vowel Sign U ..Modi Vowel Sign Ai
|
||||
intArrayOf(0x1163d, 0x1163d), // Modi Sign Anusvara ..Modi Sign Anusvara
|
||||
intArrayOf(0x1163f, 0x11640), // Modi Sign Virama ..Modi Sign Ardhacandra
|
||||
intArrayOf(0x116ab, 0x116ab), // Takri Sign Anusvara ..Takri Sign Anusvara
|
||||
intArrayOf(0x116ad, 0x116ad), // Takri Vowel Sign Aa ..Takri Vowel Sign Aa
|
||||
intArrayOf(0x116b0, 0x116b5), // Takri Vowel Sign U ..Takri Vowel Sign Au
|
||||
intArrayOf(0x116b7, 0x116b7), // Takri Sign Nukta ..Takri Sign Nukta
|
||||
intArrayOf(0x1171d, 0x1171f), // Ahom Consonant Sign Medi..Ahom Consonant Sign Medi
|
||||
intArrayOf(0x11722, 0x11725), // Ahom Vowel Sign I ..Ahom Vowel Sign Uu
|
||||
intArrayOf(0x11727, 0x1172b), // Ahom Vowel Sign Aw ..Ahom Sign Killer
|
||||
intArrayOf(0x1182f, 0x11837), // Dogra Vowel Sign U ..Dogra Sign Anusvara
|
||||
intArrayOf(0x11839, 0x1183a), // Dogra Sign Virama ..Dogra Sign Nukta
|
||||
intArrayOf(0x1193b, 0x1193c), // Dives Akuru Sign Anusvar..Dives Akuru Sign Candrab
|
||||
intArrayOf(0x1193e, 0x1193e), // Dives Akuru Virama ..Dives Akuru Virama
|
||||
intArrayOf(0x11943, 0x11943), // Dives Akuru Sign Nukta ..Dives Akuru Sign Nukta
|
||||
intArrayOf(0x119d4, 0x119d7), // Nandinagari Vowel Sign U..Nandinagari Vowel Sign V
|
||||
intArrayOf(0x119da, 0x119db), // Nandinagari Vowel Sign E..Nandinagari Vowel Sign A
|
||||
intArrayOf(0x119e0, 0x119e0), // Nandinagari Sign Virama ..Nandinagari Sign Virama
|
||||
intArrayOf(0x11a01, 0x11a0a), // Zanabazar Square Vowel S..Zanabazar Square Vowel L
|
||||
intArrayOf(0x11a33, 0x11a38), // Zanabazar Square Final C..Zanabazar Square Sign An
|
||||
intArrayOf(0x11a3b, 0x11a3e), // Zanabazar Square Cluster..Zanabazar Square Cluster
|
||||
intArrayOf(0x11a47, 0x11a47), // Zanabazar Square Subjoin..Zanabazar Square Subjoin
|
||||
intArrayOf(0x11a51, 0x11a56), // Soyombo Vowel Sign I ..Soyombo Vowel Sign Oe
|
||||
intArrayOf(0x11a59, 0x11a5b), // Soyombo Vowel Sign Vocal..Soyombo Vowel Length Mar
|
||||
intArrayOf(0x11a8a, 0x11a96), // Soyombo Final Consonant ..Soyombo Sign Anusvara
|
||||
intArrayOf(0x11a98, 0x11a99), // Soyombo Gemination Mark ..Soyombo Subjoiner
|
||||
intArrayOf(0x11c30, 0x11c36), // Bhaiksuki Vowel Sign I ..Bhaiksuki Vowel Sign Voc
|
||||
intArrayOf(0x11c38, 0x11c3d), // Bhaiksuki Vowel Sign E ..Bhaiksuki Sign Anusvara
|
||||
intArrayOf(0x11c3f, 0x11c3f), // Bhaiksuki Sign Virama ..Bhaiksuki Sign Virama
|
||||
intArrayOf(0x11c92, 0x11ca7), // Marchen Subjoined Letter..Marchen Subjoined Letter
|
||||
intArrayOf(0x11caa, 0x11cb0), // Marchen Subjoined Letter..Marchen Vowel Sign Aa
|
||||
intArrayOf(0x11cb2, 0x11cb3), // Marchen Vowel Sign U ..Marchen Vowel Sign E
|
||||
intArrayOf(0x11cb5, 0x11cb6), // Marchen Sign Anusvara ..Marchen Sign Candrabindu
|
||||
intArrayOf(0x11d31, 0x11d36), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
|
||||
intArrayOf(0x11d3a, 0x11d3a), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
|
||||
intArrayOf(0x11d3c, 0x11d3d), // Masaram Gondi Vowel Sign..Masaram Gondi Vowel Sign
|
||||
intArrayOf(0x11d3f, 0x11d45), // Masaram Gondi Vowel Sign..Masaram Gondi Virama
|
||||
intArrayOf(0x11d47, 0x11d47), // Masaram Gondi Ra-kara ..Masaram Gondi Ra-kara
|
||||
intArrayOf(0x11d90, 0x11d91), // Gunjala Gondi Vowel Sign..Gunjala Gondi Vowel Sign
|
||||
intArrayOf(0x11d95, 0x11d95), // Gunjala Gondi Sign Anusv..Gunjala Gondi Sign Anusv
|
||||
intArrayOf(0x11d97, 0x11d97), // Gunjala Gondi Virama ..Gunjala Gondi Virama
|
||||
intArrayOf(0x11ef3, 0x11ef4), // Makasar Vowel Sign I ..Makasar Vowel Sign U
|
||||
intArrayOf(0x11f00, 0x11f01), // (nil) ..(nil)
|
||||
intArrayOf(0x11f36, 0x11f3a), // (nil) ..(nil)
|
||||
intArrayOf(0x11f40, 0x11f40), // (nil) ..(nil)
|
||||
intArrayOf(0x11f42, 0x11f42), // (nil) ..(nil)
|
||||
intArrayOf(0x13440, 0x13440), // (nil) ..(nil)
|
||||
intArrayOf(0x13447, 0x13455), // (nil) ..(nil)
|
||||
intArrayOf(0x16af0, 0x16af4), // Bassa Vah Combining High..Bassa Vah Combining High
|
||||
intArrayOf(0x16b30, 0x16b36), // Pahawh Hmong Mark Cim Tu..Pahawh Hmong Mark Cim Ta
|
||||
intArrayOf(0x16f4f, 0x16f4f), // Miao Sign Consonant Modi..Miao Sign Consonant Modi
|
||||
intArrayOf(0x16f8f, 0x16f92), // Miao Tone Right ..Miao Tone Below
|
||||
intArrayOf(0x16fe4, 0x16fe4), // Khitan Small Script Fill..Khitan Small Script Fill
|
||||
intArrayOf(0x1bc9d, 0x1bc9e), // Duployan Thick Letter Se..Duployan Double Mark
|
||||
intArrayOf(0x1cf00, 0x1cf2d), // Znamenny Combining Mark ..Znamenny Combining Mark
|
||||
intArrayOf(0x1cf30, 0x1cf46), // Znamenny Combining Tonal..Znamenny Priznak Modifie
|
||||
intArrayOf(0x1d167, 0x1d169), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d17b, 0x1d182), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d185, 0x1d18b), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d1aa, 0x1d1ad), // Musical Symbol Combining..Musical Symbol Combining
|
||||
intArrayOf(0x1d242, 0x1d244), // Combining Greek Musical ..Combining Greek Musical
|
||||
intArrayOf(0x1da00, 0x1da36), // Signwriting Head Rim ..Signwriting Air Sucking
|
||||
intArrayOf(0x1da3b, 0x1da6c), // Signwriting Mouth Closed..Signwriting Excitement
|
||||
intArrayOf(0x1da75, 0x1da75), // Signwriting Upper Body T..Signwriting Upper Body T
|
||||
intArrayOf(0x1da84, 0x1da84), // Signwriting Location Hea..Signwriting Location Hea
|
||||
intArrayOf(0x1da9b, 0x1da9f), // Signwriting Fill Modifie..Signwriting Fill Modifie
|
||||
intArrayOf(0x1daa1, 0x1daaf), // Signwriting Rotation Mod..Signwriting Rotation Mod
|
||||
intArrayOf(0x1e000, 0x1e006), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e008, 0x1e018), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e01b, 0x1e021), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e023, 0x1e024), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e026, 0x1e02a), // Combining Glagolitic Let..Combining Glagolitic Let
|
||||
intArrayOf(0x1e08f, 0x1e08f), // (nil) ..(nil)
|
||||
intArrayOf(0x1e130, 0x1e136), // Nyiakeng Puachue Hmong T..Nyiakeng Puachue Hmong T
|
||||
intArrayOf(0x1e2ae, 0x1e2ae), // Toto Sign Rising Tone ..Toto Sign Rising Tone
|
||||
intArrayOf(0x1e2ec, 0x1e2ef), // Wancho Tone Tup ..Wancho Tone Koini
|
||||
intArrayOf(0x1e4ec, 0x1e4ef), // (nil) ..(nil)
|
||||
intArrayOf(0x1e8d0, 0x1e8d6), // Mende Kikakui Combining ..Mende Kikakui Combining
|
||||
intArrayOf(0x1e944, 0x1e94a), // Adlam Alif Lengthener ..Adlam Nukta
|
||||
intArrayOf(0xe0100, 0xe01ef), // Variation Selector-17 ..Variation Selector-256
|
||||
)
|
||||
|
||||
// https://github.com/jquast/wcwidth/blob/master/wcwidth/table_wide.py
|
||||
// from https://github.com/jquast/wcwidth/pull/64
|
||||
// at commit 1b9b6585b0080ea5cb88dc9815796505724793fe (2022-12-16):
|
||||
private val WIDE_EASTASIAN = arrayOf(
|
||||
intArrayOf(0x01100, 0x0115f), // Hangul Choseong Kiyeok ..Hangul Choseong Filler
|
||||
intArrayOf(0x0231a, 0x0231b), // Watch ..Hourglass
|
||||
intArrayOf(0x02329, 0x0232a), // Left-pointing Angle Brac..Right-pointing Angle Bra
|
||||
intArrayOf(0x023e9, 0x023ec), // Black Right-pointing Dou..Black Down-pointing Doub
|
||||
intArrayOf(0x023f0, 0x023f0), // Alarm Clock ..Alarm Clock
|
||||
intArrayOf(0x023f3, 0x023f3), // Hourglass With Flowing S..Hourglass With Flowing S
|
||||
intArrayOf(0x025fd, 0x025fe), // White Medium Small Squar..Black Medium Small Squar
|
||||
intArrayOf(0x02614, 0x02615), // Umbrella With Rain Drops..Hot Beverage
|
||||
intArrayOf(0x02648, 0x02653), // Aries ..Pisces
|
||||
intArrayOf(0x0267f, 0x0267f), // Wheelchair Symbol ..Wheelchair Symbol
|
||||
intArrayOf(0x02693, 0x02693), // Anchor ..Anchor
|
||||
intArrayOf(0x026a1, 0x026a1), // High Voltage Sign ..High Voltage Sign
|
||||
intArrayOf(0x026aa, 0x026ab), // Medium White Circle ..Medium Black Circle
|
||||
intArrayOf(0x026bd, 0x026be), // Soccer Ball ..Baseball
|
||||
intArrayOf(0x026c4, 0x026c5), // Snowman Without Snow ..Sun Behind Cloud
|
||||
intArrayOf(0x026ce, 0x026ce), // Ophiuchus ..Ophiuchus
|
||||
intArrayOf(0x026d4, 0x026d4), // No Entry ..No Entry
|
||||
intArrayOf(0x026ea, 0x026ea), // Church ..Church
|
||||
intArrayOf(0x026f2, 0x026f3), // Fountain ..Flag In Hole
|
||||
intArrayOf(0x026f5, 0x026f5), // Sailboat ..Sailboat
|
||||
intArrayOf(0x026fa, 0x026fa), // Tent ..Tent
|
||||
intArrayOf(0x026fd, 0x026fd), // Fuel Pump ..Fuel Pump
|
||||
intArrayOf(0x02705, 0x02705), // White Heavy Check Mark ..White Heavy Check Mark
|
||||
intArrayOf(0x0270a, 0x0270b), // Raised Fist ..Raised Hand
|
||||
intArrayOf(0x02728, 0x02728), // Sparkles ..Sparkles
|
||||
intArrayOf(0x0274c, 0x0274c), // Cross Mark ..Cross Mark
|
||||
intArrayOf(0x0274e, 0x0274e), // Negative Squared Cross M..Negative Squared Cross M
|
||||
intArrayOf(0x02753, 0x02755), // Black Question Mark Orna..White Exclamation Mark O
|
||||
intArrayOf(0x02757, 0x02757), // Heavy Exclamation Mark S..Heavy Exclamation Mark S
|
||||
intArrayOf(0x02795, 0x02797), // Heavy Plus Sign ..Heavy Division Sign
|
||||
intArrayOf(0x027b0, 0x027b0), // Curly Loop ..Curly Loop
|
||||
intArrayOf(0x027bf, 0x027bf), // Double Curly Loop ..Double Curly Loop
|
||||
intArrayOf(0x02b1b, 0x02b1c), // Black Large Square ..White Large Square
|
||||
intArrayOf(0x02b50, 0x02b50), // White Medium Star ..White Medium Star
|
||||
intArrayOf(0x02b55, 0x02b55), // Heavy Large Circle ..Heavy Large Circle
|
||||
intArrayOf(0x02e80, 0x02e99), // Cjk Radical Repeat ..Cjk Radical Rap
|
||||
intArrayOf(0x02e9b, 0x02ef3), // Cjk Radical Choke ..Cjk Radical C-simplified
|
||||
intArrayOf(0x02f00, 0x02fd5), // Kangxi Radical One ..Kangxi Radical Flute
|
||||
intArrayOf(0x02ff0, 0x02ffb), // Ideographic Description ..Ideographic Description
|
||||
intArrayOf(0x03000, 0x0303e), // Ideographic Space ..Ideographic Variation In
|
||||
intArrayOf(0x03041, 0x03096), // Hiragana Letter Small A ..Hiragana Letter Small Ke
|
||||
intArrayOf(0x03099, 0x030ff), // Combining Katakana-hirag..Katakana Digraph Koto
|
||||
intArrayOf(0x03105, 0x0312f), // Bopomofo Letter B ..Bopomofo Letter Nn
|
||||
intArrayOf(0x03131, 0x0318e), // Hangul Letter Kiyeok ..Hangul Letter Araeae
|
||||
intArrayOf(0x03190, 0x031e3), // Ideographic Annotation L..Cjk Stroke Q
|
||||
intArrayOf(0x031f0, 0x0321e), // Katakana Letter Small Ku..Parenthesized Korean Cha
|
||||
intArrayOf(0x03220, 0x03247), // Parenthesized Ideograph ..Circled Ideograph Koto
|
||||
intArrayOf(0x03250, 0x04dbf), // Partnership Sign ..Cjk Unified Ideograph-4d
|
||||
intArrayOf(0x04e00, 0x0a48c), // Cjk Unified Ideograph-4e..Yi Syllable Yyr
|
||||
intArrayOf(0x0a490, 0x0a4c6), // Yi Radical Qot ..Yi Radical Ke
|
||||
intArrayOf(0x0a960, 0x0a97c), // Hangul Choseong Tikeut-m..Hangul Choseong Ssangyeo
|
||||
intArrayOf(0x0ac00, 0x0d7a3), // Hangul Syllable Ga ..Hangul Syllable Hih
|
||||
intArrayOf(0x0f900, 0x0faff), // Cjk Compatibility Ideogr..(nil)
|
||||
intArrayOf(0x0fe10, 0x0fe19), // Presentation Form For Ve..Presentation Form For Ve
|
||||
intArrayOf(0x0fe30, 0x0fe52), // Presentation Form For Ve..Small Full Stop
|
||||
intArrayOf(0x0fe54, 0x0fe66), // Small Semicolon ..Small Equals Sign
|
||||
intArrayOf(0x0fe68, 0x0fe6b), // Small Reverse Solidus ..Small Commercial At
|
||||
intArrayOf(0x0ff01, 0x0ff60), // Fullwidth Exclamation Ma..Fullwidth Right White Pa
|
||||
intArrayOf(0x0ffe0, 0x0ffe6), // Fullwidth Cent Sign ..Fullwidth Won Sign
|
||||
intArrayOf(0x16fe0, 0x16fe4), // Tangut Iteration Mark ..Khitan Small Script Fill
|
||||
intArrayOf(0x16ff0, 0x16ff1), // Vietnamese Alternate Rea..Vietnamese Alternate Rea
|
||||
intArrayOf(0x17000, 0x187f7), // (nil) ..(nil)
|
||||
intArrayOf(0x18800, 0x18cd5), // Tangut Component-001 ..Khitan Small Script Char
|
||||
intArrayOf(0x18d00, 0x18d08), // (nil) ..(nil)
|
||||
intArrayOf(0x1aff0, 0x1aff3), // Katakana Letter Minnan T..Katakana Letter Minnan T
|
||||
intArrayOf(0x1aff5, 0x1affb), // Katakana Letter Minnan T..Katakana Letter Minnan N
|
||||
intArrayOf(0x1affd, 0x1affe), // Katakana Letter Minnan N..Katakana Letter Minnan N
|
||||
intArrayOf(0x1b000, 0x1b122), // Katakana Letter Archaic ..Katakana Letter Archaic
|
||||
intArrayOf(0x1b132, 0x1b132), // (nil) ..(nil)
|
||||
intArrayOf(0x1b150, 0x1b152), // Hiragana Letter Small Wi..Hiragana Letter Small Wo
|
||||
intArrayOf(0x1b155, 0x1b155), // (nil) ..(nil)
|
||||
intArrayOf(0x1b164, 0x1b167), // Katakana Letter Small Wi..Katakana Letter Small N
|
||||
intArrayOf(0x1b170, 0x1b2fb), // Nushu Character-1b170 ..Nushu Character-1b2fb
|
||||
intArrayOf(0x1f004, 0x1f004), // Mahjong Tile Red Dragon ..Mahjong Tile Red Dragon
|
||||
intArrayOf(0x1f0cf, 0x1f0cf), // Playing Card Black Joker..Playing Card Black Joker
|
||||
intArrayOf(0x1f18e, 0x1f18e), // Negative Squared Ab ..Negative Squared Ab
|
||||
intArrayOf(0x1f191, 0x1f19a), // Squared Cl ..Squared Vs
|
||||
intArrayOf(0x1f200, 0x1f202), // Square Hiragana Hoka ..Squared Katakana Sa
|
||||
intArrayOf(0x1f210, 0x1f23b), // Squared Cjk Unified Ideo..Squared Cjk Unified Ideo
|
||||
intArrayOf(0x1f240, 0x1f248), // Tortoise Shell Bracketed..Tortoise Shell Bracketed
|
||||
intArrayOf(0x1f250, 0x1f251), // Circled Ideograph Advant..Circled Ideograph Accept
|
||||
intArrayOf(0x1f260, 0x1f265), // Rounded Symbol For Fu ..Rounded Symbol For Cai
|
||||
intArrayOf(0x1f300, 0x1f320), // Cyclone ..Shooting Star
|
||||
intArrayOf(0x1f32d, 0x1f335), // Hot Dog ..Cactus
|
||||
intArrayOf(0x1f337, 0x1f37c), // Tulip ..Baby Bottle
|
||||
intArrayOf(0x1f37e, 0x1f393), // Bottle With Popping Cork..Graduation Cap
|
||||
intArrayOf(0x1f3a0, 0x1f3ca), // Carousel Horse ..Swimmer
|
||||
intArrayOf(0x1f3cf, 0x1f3d3), // Cricket Bat And Ball ..Table Tennis Paddle And
|
||||
intArrayOf(0x1f3e0, 0x1f3f0), // House Building ..European Castle
|
||||
intArrayOf(0x1f3f4, 0x1f3f4), // Waving Black Flag ..Waving Black Flag
|
||||
intArrayOf(0x1f3f8, 0x1f43e), // Badminton Racquet And Sh..Paw Prints
|
||||
intArrayOf(0x1f440, 0x1f440), // Eyes ..Eyes
|
||||
intArrayOf(0x1f442, 0x1f4fc), // Ear ..Videocassette
|
||||
intArrayOf(0x1f4ff, 0x1f53d), // Prayer Beads ..Down-pointing Small Red
|
||||
intArrayOf(0x1f54b, 0x1f54e), // Kaaba ..Menorah With Nine Branch
|
||||
intArrayOf(0x1f550, 0x1f567), // Clock Face One Oclock ..Clock Face Twelve-thirty
|
||||
intArrayOf(0x1f57a, 0x1f57a), // Man Dancing ..Man Dancing
|
||||
intArrayOf(0x1f595, 0x1f596), // Reversed Hand With Middl..Raised Hand With Part Be
|
||||
intArrayOf(0x1f5a4, 0x1f5a4), // Black Heart ..Black Heart
|
||||
intArrayOf(0x1f5fb, 0x1f64f), // Mount Fuji ..Person With Folded Hands
|
||||
intArrayOf(0x1f680, 0x1f6c5), // Rocket ..Left Luggage
|
||||
intArrayOf(0x1f6cc, 0x1f6cc), // Sleeping Accommodation ..Sleeping Accommodation
|
||||
intArrayOf(0x1f6d0, 0x1f6d2), // Place Of Worship ..Shopping Trolley
|
||||
intArrayOf(0x1f6d5, 0x1f6d7), // Hindu Temple ..Elevator
|
||||
intArrayOf(0x1f6dc, 0x1f6df), // (nil) ..Ring Buoy
|
||||
intArrayOf(0x1f6eb, 0x1f6ec), // Airplane Departure ..Airplane Arriving
|
||||
intArrayOf(0x1f6f4, 0x1f6fc), // Scooter ..Roller Skate
|
||||
intArrayOf(0x1f7e0, 0x1f7eb), // Large Orange Circle ..Large Brown Square
|
||||
intArrayOf(0x1f7f0, 0x1f7f0), // Heavy Equals Sign ..Heavy Equals Sign
|
||||
intArrayOf(0x1f90c, 0x1f93a), // Pinched Fingers ..Fencer
|
||||
intArrayOf(0x1f93c, 0x1f945), // Wrestlers ..Goal Net
|
||||
intArrayOf(0x1f947, 0x1f9ff), // First Place Medal ..Nazar Amulet
|
||||
intArrayOf(0x1fa70, 0x1fa7c), // Ballet Shoes ..Crutch
|
||||
intArrayOf(0x1fa80, 0x1fa88), // Yo-yo ..(nil)
|
||||
intArrayOf(0x1fa90, 0x1fabd), // Ringed Planet ..(nil)
|
||||
intArrayOf(0x1fabf, 0x1fac5), // (nil) ..Person With Crown
|
||||
intArrayOf(0x1face, 0x1fadb), // (nil) ..(nil)
|
||||
intArrayOf(0x1fae0, 0x1fae8), // Melting Face ..(nil)
|
||||
intArrayOf(0x1faf0, 0x1faf8), // Hand With Index Finger A..(nil)
|
||||
intArrayOf(0x20000, 0x2fffd), // Cjk Unified Ideograph-20..(nil)
|
||||
intArrayOf(0x30000, 0x3fffd), // Cjk Unified Ideograph-30..(nil)
|
||||
)
|
||||
|
||||
private fun intable(table: Array<IntArray>, c: Int): Boolean {
|
||||
if (c < table[0][0]) return false
|
||||
var bot = 0
|
||||
var top = table.size - 1
|
||||
while (top >= bot) {
|
||||
val mid = (bot + top) / 2
|
||||
if (table[mid][1] < c) {
|
||||
bot = mid + 1
|
||||
} else if (table[mid][0] > c) {
|
||||
top = mid - 1
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Return the terminal display width of a code point: 0, 1 or 2. */
|
||||
fun width(ucs: Int): Int {
|
||||
if (ucs == 0 ||
|
||||
ucs == 0x034F ||
|
||||
(ucs in 0x200B..0x200F) ||
|
||||
ucs == 0x2028 ||
|
||||
ucs == 0x2029 ||
|
||||
(ucs in 0x202A..0x202E) ||
|
||||
(ucs in 0x2060..0x2063)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// C0/C1 control characters
|
||||
// Termux change: Return 0 instead of -1.
|
||||
if (ucs < 32 || (ucs in 0x07F until 0x0A0)) return 0
|
||||
|
||||
if (intable(ZERO_WIDTH, ucs)) return 0
|
||||
|
||||
return if (intable(WIDE_EASTASIAN, ucs)) 2 else 1
|
||||
}
|
||||
|
||||
/** The width at an index position in a java char array. */
|
||||
fun width(chars: CharArray, index: Int): Int {
|
||||
val c = chars[index]
|
||||
return if (Character.isHighSurrogate(c)) width(Character.toCodePoint(c, chars[index + 1])) else width(c.code)
|
||||
}
|
||||
|
||||
/**
|
||||
* The zero width characters count like combining characters in the `chars` array from start
|
||||
* index to end index (exclusive).
|
||||
*/
|
||||
fun zeroWidthCharsCount(chars: CharArray, start: Int, end: Int): Int {
|
||||
if (start < 0 || start >= chars.size) return 0
|
||||
var count = 0
|
||||
var i = start
|
||||
while (i < end && i < chars.size) {
|
||||
if (Character.isHighSurrogate(chars[i])) {
|
||||
if (width(Character.toCodePoint(chars[i], chars[i + 1])) <= 0) {
|
||||
count++
|
||||
}
|
||||
i += 2
|
||||
} else {
|
||||
if (width(chars[i].code) <= 0) {
|
||||
count++
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
}
|
||||
362
app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
Normal file
362
app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainActivity.kt
Normal file
@@ -0,0 +1,362 @@
|
||||
package com.topjohnwu.magisk.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.Manifest.permission.REQUEST_INSTALL_PACKAGES
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.content.res.use
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.topjohnwu.magisk.arch.VMFactory
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.base.ActivityExtension
|
||||
import com.topjohnwu.magisk.core.base.SplashController
|
||||
import com.topjohnwu.magisk.core.base.SplashScreenHost
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.ktx.reflectField
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.tasks.AppMigration
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.ui.deny.DenyListScreen
|
||||
import com.topjohnwu.magisk.ui.deny.DenyListViewModel
|
||||
import com.topjohnwu.magisk.ui.flash.FlashScreen
|
||||
import com.topjohnwu.magisk.ui.flash.FlashUtils
|
||||
import com.topjohnwu.magisk.ui.flash.FlashViewModel
|
||||
import com.topjohnwu.magisk.ui.module.ActionScreen
|
||||
import com.topjohnwu.magisk.ui.module.ActionViewModel
|
||||
import com.topjohnwu.magisk.ui.navigation.LocalNavigator
|
||||
import com.topjohnwu.magisk.ui.navigation.Navigator
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import com.topjohnwu.magisk.ui.navigation.rememberNavigator
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserDetailScreen
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import com.topjohnwu.magisk.ui.theme.MagiskTheme
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import top.yukonga.miuix.kmp.utils.MiuixPopupUtils.Companion.MiuixPopupHost
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
class MainActivity : AppCompatActivity(), SplashScreenHost {
|
||||
|
||||
override val extension = ActivityExtension(this)
|
||||
override val splashController = SplashController(this)
|
||||
|
||||
private val intentState = MutableStateFlow(0)
|
||||
internal val showInvalidState = MutableStateFlow(false)
|
||||
internal val showUnsupported = MutableStateFlow<List<Pair<Int, Int>>>(emptyList())
|
||||
internal val showShortcutPrompt = MutableStateFlow(false)
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
extension.onCreate(savedInstanceState)
|
||||
if (isRunningAsStub) {
|
||||
val delegate = delegate
|
||||
val clz = delegate.javaClass
|
||||
clz.reflectField("mActivityHandlesConfigFlagsChecked").set(delegate, true)
|
||||
clz.reflectField("mActivityHandlesConfigFlags").set(delegate, 0)
|
||||
}
|
||||
|
||||
setTheme(Theme.selected.themeRes)
|
||||
splashController.preOnCreate()
|
||||
super.onCreate(savedInstanceState)
|
||||
splashController.onCreate(savedInstanceState)
|
||||
|
||||
setupWindow()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
splashController.onResume()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
extension.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
private fun setupWindow() {
|
||||
obtainStyledAttributes(intArrayOf(android.R.attr.windowBackground))
|
||||
.use { it.getDrawable(0) }
|
||||
.also { window.setBackgroundDrawable(it) }
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
window?.decorView?.post {
|
||||
if ((window.decorView.rootWindowInsets?.systemWindowInsetBottom
|
||||
?: 0) < Resources.getSystem().displayMetrics.density * 40) {
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
window.navigationBarDividerColor = Color.TRANSPARENT
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
window.isStatusBarContrastEnforced = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun onCreateUi(savedInstanceState: Bundle?) {
|
||||
showUnsupportedMessage()
|
||||
askForHomeShortcut()
|
||||
|
||||
if (Config.checkUpdate) {
|
||||
extension.withPermission(Manifest.permission.POST_NOTIFICATIONS) {
|
||||
Config.checkUpdate = it
|
||||
}
|
||||
}
|
||||
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
|
||||
val initialTab = getInitialTab(intent)
|
||||
|
||||
setContent {
|
||||
MagiskTheme {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
val navigator = rememberNavigator(Route.Main)
|
||||
CompositionLocalProvider(LocalNavigator provides navigator) {
|
||||
HandleFlashIntent(navigator)
|
||||
|
||||
NavDisplay(
|
||||
backStack = navigator.backStack,
|
||||
onBack = { navigator.pop() },
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
),
|
||||
entryProvider = entryProvider {
|
||||
entry<Route.Main> {
|
||||
MainScreen(initialTab = initialTab)
|
||||
}
|
||||
entry<Route.DenyList> { _ ->
|
||||
val vm: DenyListViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
|
||||
LaunchedEffect(Unit) { vm.startLoading() }
|
||||
DenyListScreen(vm, onBack = { navigator.pop() })
|
||||
}
|
||||
entry<Route.Flash> { key ->
|
||||
val vm: FlashViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
|
||||
LaunchedEffect(key) {
|
||||
if (vm.flashAction.isEmpty()) {
|
||||
vm.flashAction = key.action
|
||||
vm.flashUri = key.additionalData?.let { Uri.parse(it) }
|
||||
vm.startFlashing()
|
||||
}
|
||||
}
|
||||
FlashScreen(vm, action = key.action, onBack = { navigator.pop() })
|
||||
}
|
||||
entry<Route.SuperuserDetail> { key ->
|
||||
val vm: SuperuserViewModel = androidx.lifecycle.viewmodel.compose.viewModel(
|
||||
viewModelStoreOwner = this@MainActivity, factory = VMFactory
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
vm.authenticate = { onSuccess ->
|
||||
extension.withAuthentication { if (it) onSuccess() }
|
||||
}
|
||||
}
|
||||
SuperuserDetailScreen(uid = key.uid, viewModel = vm, onBack = { navigator.pop() })
|
||||
}
|
||||
entry<Route.Action> { key ->
|
||||
val vm: ActionViewModel = androidx.lifecycle.viewmodel.compose.viewModel(factory = VMFactory)
|
||||
LaunchedEffect(key) {
|
||||
if (vm.actionId.isEmpty()) {
|
||||
vm.actionId = key.id
|
||||
vm.actionName = key.name
|
||||
vm.startRunAction()
|
||||
}
|
||||
}
|
||||
ActionScreen(vm, actionName = key.name, onBack = { navigator.pop() })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
MainActivityDialogs(activity = this@MainActivity)
|
||||
MiuixPopupHost()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HandleFlashIntent(navigator: Navigator) {
|
||||
val intentVersion by intentState.collectAsState()
|
||||
LaunchedEffect(intentVersion) {
|
||||
val currentIntent = intent ?: return@LaunchedEffect
|
||||
if (currentIntent.action == FlashUtils.INTENT_FLASH) {
|
||||
val action = currentIntent.getStringExtra(FlashUtils.EXTRA_FLASH_ACTION)
|
||||
?: return@LaunchedEffect
|
||||
val uri = currentIntent.getStringExtra(FlashUtils.EXTRA_FLASH_URI)
|
||||
navigator.push(Route.Flash(action, uri))
|
||||
currentIntent.action = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
intentState.value += 1
|
||||
}
|
||||
|
||||
private fun getInitialTab(intent: Intent?): Int {
|
||||
val section = if (intent?.action == Intent.ACTION_APPLICATION_PREFERENCES) {
|
||||
Const.Nav.SETTINGS
|
||||
} else {
|
||||
intent?.getStringExtra(Const.Key.OPEN_SECTION)
|
||||
}
|
||||
return when (section) {
|
||||
Const.Nav.SUPERUSER -> Tab.SUPERUSER.ordinal
|
||||
Const.Nav.MODULES -> Tab.MODULES.ordinal
|
||||
Const.Nav.SETTINGS -> Tab.SETTINGS.ordinal
|
||||
else -> Tab.HOME.ordinal
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun showInvalidStateMessage() {
|
||||
showInvalidState.value = true
|
||||
}
|
||||
|
||||
internal fun handleInvalidStateInstall() {
|
||||
extension.withPermission(REQUEST_INSTALL_PACKAGES) {
|
||||
if (!it) {
|
||||
toast(CoreR.string.install_unknown_denied, Toast.LENGTH_SHORT)
|
||||
showInvalidState.value = true
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
if (!AppMigration.restoreApp(this@MainActivity)) {
|
||||
toast(CoreR.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsupportedMessage() {
|
||||
val messages = mutableListOf<Pair<Int, Int>>()
|
||||
|
||||
if (Info.env.isUnsupported) {
|
||||
messages.add(CoreR.string.unsupport_magisk_title to CoreR.string.unsupport_magisk_msg)
|
||||
}
|
||||
if (!Info.isEmulator && Info.env.isActive && System.getenv("PATH")
|
||||
?.split(':')
|
||||
?.filterNot { java.io.File("$it/magisk").exists() }
|
||||
?.any { java.io.File("$it/su").exists() } == true) {
|
||||
messages.add(CoreR.string.unsupport_general_title to CoreR.string.unsupport_other_su_msg)
|
||||
}
|
||||
if (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) {
|
||||
messages.add(CoreR.string.unsupport_general_title to CoreR.string.unsupport_system_app_msg)
|
||||
}
|
||||
if (applicationInfo.flags and ApplicationInfo.FLAG_EXTERNAL_STORAGE != 0) {
|
||||
messages.add(CoreR.string.unsupport_general_title to CoreR.string.unsupport_external_storage_msg)
|
||||
}
|
||||
|
||||
if (messages.isNotEmpty()) {
|
||||
showUnsupported.value = messages
|
||||
}
|
||||
}
|
||||
|
||||
private fun askForHomeShortcut() {
|
||||
if (isRunningAsStub && !Config.askedHome &&
|
||||
ShortcutManagerCompat.isRequestPinShortcutSupported(this)) {
|
||||
Config.askedHome = true
|
||||
showShortcutPrompt.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MainActivityDialogs(activity: MainActivity) {
|
||||
val showInvalid by activity.showInvalidState.collectAsState()
|
||||
val unsupportedMessages by activity.showUnsupported.collectAsState()
|
||||
val showShortcut by activity.showShortcutPrompt.collectAsState()
|
||||
|
||||
val invalidDialog = com.topjohnwu.magisk.ui.component.rememberConfirmDialog(
|
||||
onConfirm = {
|
||||
activity.showInvalidState.value = false
|
||||
activity.handleInvalidStateInstall()
|
||||
},
|
||||
onDismiss = {}
|
||||
)
|
||||
|
||||
LaunchedEffect(showInvalid) {
|
||||
if (showInvalid) {
|
||||
invalidDialog.showConfirm(
|
||||
title = activity.getString(CoreR.string.unsupport_nonroot_stub_title),
|
||||
content = activity.getString(CoreR.string.unsupport_nonroot_stub_msg),
|
||||
confirm = activity.getString(CoreR.string.install),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for ((index, pair) in unsupportedMessages.withIndex()) {
|
||||
val (titleRes, msgRes) = pair
|
||||
val show = rememberSaveable { androidx.compose.runtime.mutableStateOf(true) }
|
||||
com.topjohnwu.magisk.ui.component.rememberConfirmDialog(
|
||||
onConfirm = { show.value = false },
|
||||
).also { dialog ->
|
||||
LaunchedEffect(Unit) {
|
||||
dialog.showConfirm(
|
||||
title = activity.getString(titleRes),
|
||||
content = activity.getString(msgRes),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val shortcutDialog = com.topjohnwu.magisk.ui.component.rememberConfirmDialog(
|
||||
onConfirm = {
|
||||
activity.showShortcutPrompt.value = false
|
||||
Shortcuts.addHomeIcon(activity)
|
||||
},
|
||||
onDismiss = { activity.showShortcutPrompt.value = false }
|
||||
)
|
||||
|
||||
LaunchedEffect(showShortcut) {
|
||||
if (showShortcut) {
|
||||
shortcutDialog.showConfirm(
|
||||
title = activity.getString(CoreR.string.add_shortcut_title),
|
||||
content = activity.getString(CoreR.string.add_shortcut_msg),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
227
app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainScreen.kt
Normal file
227
app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/MainScreen.kt
Normal file
@@ -0,0 +1,227 @@
|
||||
package com.topjohnwu.magisk.ui
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.VMFactory
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.ui.home.HomeScreen
|
||||
import com.topjohnwu.magisk.ui.home.HomeViewModel
|
||||
import com.topjohnwu.magisk.ui.install.InstallViewModel
|
||||
import com.topjohnwu.magisk.ui.log.LogScreen
|
||||
import com.topjohnwu.magisk.ui.log.LogViewModel
|
||||
import com.topjohnwu.magisk.ui.module.ModuleScreen
|
||||
import com.topjohnwu.magisk.ui.module.ModuleViewModel
|
||||
import com.topjohnwu.magisk.ui.navigation.CollectNavEvents
|
||||
import com.topjohnwu.magisk.ui.navigation.LocalNavigator
|
||||
import com.topjohnwu.magisk.ui.settings.SettingsScreen
|
||||
import com.topjohnwu.magisk.ui.settings.SettingsViewModel
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserScreen
|
||||
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
enum class Tab(val titleRes: Int, val iconRes: Int) {
|
||||
MODULES(CoreR.string.modules, R.drawable.ic_module_outlined_md2),
|
||||
SUPERUSER(CoreR.string.superuser, R.drawable.ic_superuser_outlined_md2),
|
||||
HOME(CoreR.string.section_home, R.drawable.ic_home_outlined_md2),
|
||||
LOG(CoreR.string.logs, R.drawable.ic_bug_outlined_md2),
|
||||
SETTINGS(CoreR.string.settings, R.drawable.ic_settings_outlined_md2);
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainScreen(initialTab: Int = Tab.HOME.ordinal) {
|
||||
val navigator = LocalNavigator.current
|
||||
val visibleTabs = remember {
|
||||
Tab.entries.filter { tab ->
|
||||
when (tab) {
|
||||
Tab.SUPERUSER -> Info.showSuperUser
|
||||
Tab.MODULES -> Info.env.isActive && LocalModule.loaded()
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
val initialPage = visibleTabs.indexOf(Tab.entries[initialTab]).coerceAtLeast(0)
|
||||
val pagerState = rememberPagerState(initialPage = initialPage, pageCount = { visibleTabs.size })
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
beyondViewportPageCount = visibleTabs.size - 1,
|
||||
userScrollEnabled = true,
|
||||
) { page ->
|
||||
when (visibleTabs[page]) {
|
||||
Tab.HOME -> {
|
||||
val vm: HomeViewModel = viewModel(factory = VMFactory)
|
||||
val installVm: InstallViewModel = viewModel(factory = VMFactory)
|
||||
LaunchedEffect(Unit) { vm.startLoading() }
|
||||
CollectNavEvents(vm, navigator)
|
||||
CollectNavEvents(installVm, navigator)
|
||||
HomeScreen(vm, installVm)
|
||||
}
|
||||
Tab.SUPERUSER -> {
|
||||
val activity = LocalContext.current as MainActivity
|
||||
val vm: SuperuserViewModel = viewModel(viewModelStoreOwner = activity, factory = VMFactory)
|
||||
LaunchedEffect(Unit) {
|
||||
vm.authenticate = { onSuccess ->
|
||||
activity.extension.withAuthentication { if (it) onSuccess() }
|
||||
}
|
||||
vm.startLoading()
|
||||
}
|
||||
SuperuserScreen(vm)
|
||||
}
|
||||
Tab.LOG -> {
|
||||
val vm: LogViewModel = viewModel(factory = VMFactory)
|
||||
LaunchedEffect(Unit) { vm.startLoading() }
|
||||
LogScreen(vm)
|
||||
}
|
||||
Tab.MODULES -> {
|
||||
val vm: ModuleViewModel = viewModel(factory = VMFactory)
|
||||
LaunchedEffect(Unit) { vm.startLoading() }
|
||||
CollectNavEvents(vm, navigator)
|
||||
ModuleScreen(vm)
|
||||
}
|
||||
Tab.SETTINGS -> {
|
||||
val activity = LocalContext.current as MainActivity
|
||||
val vm: SettingsViewModel = viewModel(factory = VMFactory)
|
||||
LaunchedEffect(Unit) {
|
||||
vm.authenticate = { onSuccess ->
|
||||
activity.extension.withAuthentication { if (it) onSuccess() }
|
||||
}
|
||||
}
|
||||
CollectNavEvents(vm, navigator)
|
||||
SettingsScreen(vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingNavigationBar(
|
||||
pagerState = pagerState,
|
||||
visibleTabs = visibleTabs,
|
||||
modifier = Modifier.align(Alignment.BottomCenter)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingNavigationBar(
|
||||
pagerState: PagerState,
|
||||
visibleTabs: List<Tab>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val shape = RoundedCornerShape(28.dp)
|
||||
val navBarInset = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(bottom = navBarInset + 12.dp, start = 24.dp, end = 24.dp)
|
||||
.shadow(elevation = 6.dp, shape = shape)
|
||||
.clip(shape)
|
||||
.background(MiuixTheme.colorScheme.surfaceContainer)
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
visibleTabs.forEachIndexed { index, tab ->
|
||||
FloatingNavItem(
|
||||
icon = ImageVector.vectorResource(tab.iconRes),
|
||||
label = stringResource(tab.titleRes),
|
||||
selected = pagerState.currentPage == index,
|
||||
enabled = true,
|
||||
onClick = { scope.launch { pagerState.animateScrollToPage(index) } },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingNavItem(
|
||||
icon: ImageVector,
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
enabled: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val contentColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
!enabled -> MiuixTheme.colorScheme.disabledOnSecondaryVariant
|
||||
selected -> MiuixTheme.colorScheme.primary
|
||||
else -> MiuixTheme.colorScheme.onSurfaceVariantActions
|
||||
},
|
||||
animationSpec = tween(200),
|
||||
label = "navItemColor"
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
role = Role.Tab,
|
||||
onClick = onClick,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = contentColor,
|
||||
)
|
||||
Spacer(Modifier.height(2.dp))
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 11.sp,
|
||||
color = contentColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
package com.topjohnwu.magisk.ui.component
|
||||
|
||||
import android.widget.TextView
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import kotlinx.coroutines.CancellableContinuation
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import top.yukonga.miuix.kmp.basic.ButtonDefaults
|
||||
import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TextButton
|
||||
import top.yukonga.miuix.kmp.extra.SuperDialog
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
sealed interface ConfirmResult {
|
||||
data object Confirmed : ConfirmResult
|
||||
data object Canceled : ConfirmResult
|
||||
}
|
||||
|
||||
data class DialogVisuals(
|
||||
val title: String = "",
|
||||
val content: String? = null,
|
||||
val markdown: Boolean = false,
|
||||
val confirm: String? = null,
|
||||
val dismiss: String? = null,
|
||||
)
|
||||
|
||||
interface LoadingDialogHandle {
|
||||
suspend fun <R> withLoading(block: suspend () -> R): R
|
||||
}
|
||||
|
||||
interface ConfirmDialogHandle {
|
||||
fun showConfirm(
|
||||
title: String,
|
||||
content: String? = null,
|
||||
markdown: Boolean = false,
|
||||
confirm: String? = null,
|
||||
dismiss: String? = null
|
||||
)
|
||||
|
||||
suspend fun awaitConfirm(
|
||||
title: String,
|
||||
content: String? = null,
|
||||
markdown: Boolean = false,
|
||||
confirm: String? = null,
|
||||
dismiss: String? = null
|
||||
): ConfirmResult
|
||||
}
|
||||
|
||||
private class LoadingDialogHandleImpl(
|
||||
private val visible: MutableState<Boolean>,
|
||||
private val coroutineScope: CoroutineScope
|
||||
) : LoadingDialogHandle {
|
||||
override suspend fun <R> withLoading(block: suspend () -> R): R {
|
||||
return coroutineScope.async {
|
||||
try {
|
||||
visible.value = true
|
||||
block()
|
||||
} finally {
|
||||
visible.value = false
|
||||
}
|
||||
}.await()
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfirmDialogHandleImpl(
|
||||
private val visible: MutableState<Boolean>,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val callback: ConfirmCallback,
|
||||
private val resultChannel: Channel<ConfirmResult>
|
||||
) : ConfirmDialogHandle {
|
||||
|
||||
var visuals by mutableStateOf(DialogVisuals())
|
||||
private set
|
||||
|
||||
private var awaitContinuation: CancellableContinuation<ConfirmResult>? = null
|
||||
|
||||
init {
|
||||
coroutineScope.launch {
|
||||
resultChannel
|
||||
.consumeAsFlow()
|
||||
.onEach { result ->
|
||||
awaitContinuation?.let {
|
||||
awaitContinuation = null
|
||||
if (it.isActive) it.resume(result)
|
||||
}
|
||||
}
|
||||
.onEach { visible.value = false }
|
||||
.collect { result ->
|
||||
when (result) {
|
||||
ConfirmResult.Confirmed -> callback.onConfirm?.invoke()
|
||||
ConfirmResult.Canceled -> callback.onDismiss?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun showConfirm(
|
||||
title: String,
|
||||
content: String?,
|
||||
markdown: Boolean,
|
||||
confirm: String?,
|
||||
dismiss: String?
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
visuals = DialogVisuals(title, content, markdown, confirm, dismiss)
|
||||
visible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun awaitConfirm(
|
||||
title: String,
|
||||
content: String?,
|
||||
markdown: Boolean,
|
||||
confirm: String?,
|
||||
dismiss: String?
|
||||
): ConfirmResult {
|
||||
coroutineScope.launch {
|
||||
visuals = DialogVisuals(title, content, markdown, confirm, dismiss)
|
||||
visible.value = true
|
||||
}
|
||||
return suspendCancellableCoroutine { cont ->
|
||||
awaitContinuation = cont.apply {
|
||||
invokeOnCancellation { visible.value = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ConfirmCallback {
|
||||
val onConfirm: (() -> Unit)?
|
||||
val onDismiss: (() -> Unit)?
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberConfirmCallback(
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
onDismiss: (() -> Unit)? = null
|
||||
): ConfirmCallback {
|
||||
val currentOnConfirm by rememberUpdatedState(onConfirm)
|
||||
val currentOnDismiss by rememberUpdatedState(onDismiss)
|
||||
return remember {
|
||||
object : ConfirmCallback {
|
||||
override val onConfirm get() = currentOnConfirm
|
||||
override val onDismiss get() = currentOnDismiss
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberLoadingDialog(): LoadingDialogHandle {
|
||||
val visible = remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
LoadingDialog(visible)
|
||||
return remember { LoadingDialogHandleImpl(visible, scope) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberConfirmDialog(
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
onDismiss: (() -> Unit)? = null
|
||||
): ConfirmDialogHandle {
|
||||
return rememberConfirmDialog(rememberConfirmCallback(onConfirm, onDismiss))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberConfirmDialog(callback: ConfirmCallback): ConfirmDialogHandle {
|
||||
val visible = rememberSaveable { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val resultChannel = remember { Channel<ConfirmResult>() }
|
||||
|
||||
val handle = remember {
|
||||
ConfirmDialogHandleImpl(visible, scope, callback, resultChannel)
|
||||
}
|
||||
|
||||
if (visible.value) {
|
||||
ConfirmDialogContent(
|
||||
visuals = handle.visuals,
|
||||
confirm = { scope.launch { resultChannel.send(ConfirmResult.Confirmed) } },
|
||||
dismiss = { scope.launch { resultChannel.send(ConfirmResult.Canceled) } },
|
||||
showDialog = visible
|
||||
)
|
||||
}
|
||||
|
||||
return handle
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingDialog(showDialog: MutableState<Boolean>) {
|
||||
SuperDialog(
|
||||
show = showDialog,
|
||||
onDismissRequest = {},
|
||||
content = {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
) {
|
||||
InfiniteProgressIndicator(
|
||||
color = MiuixTheme.colorScheme.onBackground
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 12.dp),
|
||||
text = stringResource(com.topjohnwu.magisk.core.R.string.loading),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfirmDialogContent(
|
||||
visuals: DialogVisuals,
|
||||
confirm: () -> Unit,
|
||||
dismiss: () -> Unit,
|
||||
showDialog: MutableState<Boolean>
|
||||
) {
|
||||
SuperDialog(
|
||||
modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),
|
||||
show = showDialog,
|
||||
title = visuals.title,
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
showDialog.value = false
|
||||
},
|
||||
content = {
|
||||
Layout(
|
||||
content = {
|
||||
visuals.content?.let { content ->
|
||||
if (visuals.markdown) {
|
||||
MarkdownText(content)
|
||||
} else {
|
||||
Text(
|
||||
text = content,
|
||||
color = MiuixTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.padding(top = 12.dp)
|
||||
) {
|
||||
TextButton(
|
||||
text = visuals.dismiss
|
||||
?: stringResource(android.R.string.cancel),
|
||||
onClick = {
|
||||
dismiss()
|
||||
showDialog.value = false
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(Modifier.width(20.dp))
|
||||
TextButton(
|
||||
text = visuals.confirm
|
||||
?: stringResource(android.R.string.ok),
|
||||
onClick = {
|
||||
confirm()
|
||||
showDialog.value = false
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.textButtonColorsPrimary()
|
||||
)
|
||||
}
|
||||
}
|
||||
) { measurables, constraints ->
|
||||
if (measurables.size != 2) {
|
||||
val button = measurables[0].measure(constraints)
|
||||
layout(constraints.maxWidth, button.height) {
|
||||
button.place(0, 0)
|
||||
}
|
||||
} else {
|
||||
val button = measurables[1].measure(constraints)
|
||||
val content = measurables[0].measure(
|
||||
constraints.copy(maxHeight = constraints.maxHeight - button.height)
|
||||
)
|
||||
layout(constraints.maxWidth, content.height + button.height) {
|
||||
content.place(0, 0)
|
||||
button.place(0, content.height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarkdownText(text: String) {
|
||||
val contentColor = MiuixTheme.colorScheme.onBackground.toArgb()
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
TextView(context).apply {
|
||||
setTextColor(contentColor)
|
||||
ServiceLocator.markwon.setMarkdown(this, text)
|
||||
}
|
||||
},
|
||||
update = { textView ->
|
||||
textView.setTextColor(contentColor)
|
||||
ServiceLocator.markwon.setMarkdown(textView, text)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 300.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MarkdownTextAsync(getMarkdownText: suspend () -> String) {
|
||||
var mdText by remember { mutableStateOf<String?>(null) }
|
||||
var error by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
mdText = withContext(Dispatchers.IO) { getMarkdownText() }
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
error = true
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
error -> Text(stringResource(com.topjohnwu.magisk.core.R.string.download_file_error))
|
||||
mdText != null -> MarkdownText(mdText!!)
|
||||
else -> Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
InfiniteProgressIndicator(color = MiuixTheme.colorScheme.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.topjohnwu.magisk.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntRect
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import top.yukonga.miuix.kmp.basic.PopupPositionProvider
|
||||
|
||||
object ListPopupDefaults {
|
||||
val MenuPositionProvider = object : PopupPositionProvider {
|
||||
override fun calculatePosition(
|
||||
anchorBounds: IntRect,
|
||||
windowBounds: IntRect,
|
||||
layoutDirection: LayoutDirection,
|
||||
popupContentSize: IntSize,
|
||||
popupMargin: IntRect,
|
||||
alignment: PopupPositionProvider.Align,
|
||||
): IntOffset {
|
||||
val resolved = alignment.resolve(layoutDirection)
|
||||
val offsetX: Int
|
||||
val offsetY: Int
|
||||
when (resolved) {
|
||||
PopupPositionProvider.Align.TopStart -> {
|
||||
offsetX = anchorBounds.left + popupMargin.left
|
||||
offsetY = anchorBounds.bottom + popupMargin.top
|
||||
}
|
||||
PopupPositionProvider.Align.TopEnd -> {
|
||||
offsetX = anchorBounds.right - popupContentSize.width - popupMargin.right
|
||||
offsetY = anchorBounds.bottom + popupMargin.top
|
||||
}
|
||||
PopupPositionProvider.Align.BottomStart -> {
|
||||
offsetX = anchorBounds.left + popupMargin.left
|
||||
offsetY = anchorBounds.top - popupContentSize.height - popupMargin.bottom
|
||||
}
|
||||
PopupPositionProvider.Align.BottomEnd -> {
|
||||
offsetX = anchorBounds.right - popupContentSize.width - popupMargin.right
|
||||
offsetY = anchorBounds.top - popupContentSize.height - popupMargin.bottom
|
||||
}
|
||||
else -> {
|
||||
offsetX = if (resolved == PopupPositionProvider.Align.End) {
|
||||
anchorBounds.right - popupContentSize.width - popupMargin.right
|
||||
} else {
|
||||
anchorBounds.left + popupMargin.left
|
||||
}
|
||||
offsetY = if (windowBounds.bottom - anchorBounds.bottom > popupContentSize.height) {
|
||||
anchorBounds.bottom + popupMargin.bottom
|
||||
} else if (anchorBounds.top - windowBounds.top > popupContentSize.height) {
|
||||
anchorBounds.top - popupContentSize.height - popupMargin.top
|
||||
} else {
|
||||
anchorBounds.top + anchorBounds.height / 2 - popupContentSize.height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
return IntOffset(
|
||||
x = offsetX.coerceIn(
|
||||
windowBounds.left,
|
||||
(windowBounds.right - popupContentSize.width - popupMargin.right)
|
||||
.coerceAtLeast(windowBounds.left),
|
||||
),
|
||||
y = offsetY.coerceIn(
|
||||
(windowBounds.top + popupMargin.top)
|
||||
.coerceAtMost(windowBounds.bottom - popupContentSize.height - popupMargin.bottom),
|
||||
windowBounds.bottom - popupContentSize.height - popupMargin.bottom,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMargins(): PaddingValues = PaddingValues(start = 20.dp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun PopupPositionProvider.Align.resolve(layoutDirection: LayoutDirection): PopupPositionProvider.Align {
|
||||
if (layoutDirection == LayoutDirection.Ltr) return this
|
||||
return when (this) {
|
||||
PopupPositionProvider.Align.Start -> PopupPositionProvider.Align.End
|
||||
PopupPositionProvider.Align.End -> PopupPositionProvider.Align.Start
|
||||
PopupPositionProvider.Align.TopStart -> PopupPositionProvider.Align.TopEnd
|
||||
PopupPositionProvider.Align.TopEnd -> PopupPositionProvider.Align.TopStart
|
||||
PopupPositionProvider.Align.BottomStart -> PopupPositionProvider.Align.BottomEnd
|
||||
PopupPositionProvider.Align.BottomEnd -> PopupPositionProvider.Align.BottomStart
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.topjohnwu.magisk.ui.deny
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.GET_ACTIVITIES
|
||||
import android.content.pm.PackageManager.GET_PROVIDERS
|
||||
import android.content.pm.PackageManager.GET_RECEIVERS
|
||||
import android.content.pm.PackageManager.GET_SERVICES
|
||||
import android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS
|
||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.os.ProcessCompat
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
import java.util.Locale
|
||||
import java.util.TreeSet
|
||||
|
||||
class CmdlineListItem(line: String) {
|
||||
val packageName: String
|
||||
val process: String
|
||||
|
||||
init {
|
||||
val split = line.split(Regex("\\|"), 2)
|
||||
packageName = split[0]
|
||||
process = split.getOrElse(1) { packageName }
|
||||
}
|
||||
}
|
||||
|
||||
const val ISOLATED_MAGIC = "isolated"
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
class AppProcessInfo(
|
||||
private val info: ApplicationInfo,
|
||||
pm: PackageManager,
|
||||
denyList: List<CmdlineListItem>
|
||||
) : Comparable<AppProcessInfo> {
|
||||
|
||||
private val denyList = denyList.filter {
|
||||
it.packageName == info.packageName || it.packageName == ISOLATED_MAGIC
|
||||
}
|
||||
|
||||
val label = info.getLabel(pm)
|
||||
val iconImage: Drawable = runCatching { info.loadIcon(pm) }.getOrDefault(pm.defaultActivityIcon)
|
||||
val packageName: String get() = info.packageName
|
||||
var firstInstallTime: Long = 0L
|
||||
private set
|
||||
var lastUpdateTime: Long = 0L
|
||||
private set
|
||||
val processes = fetchProcesses(pm)
|
||||
|
||||
override fun compareTo(other: AppProcessInfo) = comparator.compare(this, other)
|
||||
|
||||
fun isSystemApp() = info.flags and ApplicationInfo.FLAG_SYSTEM != 0
|
||||
|
||||
fun isApp() = ProcessCompat.isApplicationUid(info.uid)
|
||||
|
||||
private fun createProcess(name: String, pkg: String = info.packageName) =
|
||||
ProcessInfo(name, pkg, denyList.any { it.process == name && it.packageName == pkg })
|
||||
|
||||
private fun ComponentInfo.getProcName(): String = processName
|
||||
?: applicationInfo.processName
|
||||
?: applicationInfo.packageName
|
||||
|
||||
private val ServiceInfo.isIsolated get() = (flags and ServiceInfo.FLAG_ISOLATED_PROCESS) != 0
|
||||
private val ServiceInfo.useAppZygote get() = (flags and ServiceInfo.FLAG_USE_APP_ZYGOTE) != 0
|
||||
|
||||
private fun Array<out ComponentInfo>?.toProcessList() =
|
||||
orEmpty().map { createProcess(it.getProcName()) }
|
||||
|
||||
private fun Array<ServiceInfo>?.toProcessList(): List<ProcessInfo> {
|
||||
if (this == null) return emptyList()
|
||||
val result = mutableListOf<ProcessInfo>()
|
||||
var hasIsolated = false
|
||||
for (si in this) {
|
||||
if (si.isIsolated) {
|
||||
if (si.useAppZygote) {
|
||||
val proc = info.processName ?: info.packageName
|
||||
result.add(createProcess("${proc}_zygote"))
|
||||
} else {
|
||||
hasIsolated = true
|
||||
}
|
||||
} else {
|
||||
result.add(createProcess(si.getProcName()))
|
||||
}
|
||||
}
|
||||
if (hasIsolated) {
|
||||
val prefix = "${info.processName ?: info.packageName}:"
|
||||
val isEnabled = denyList.any {
|
||||
it.packageName == ISOLATED_MAGIC && it.process.startsWith(prefix)
|
||||
}
|
||||
result.add(ProcessInfo(prefix, ISOLATED_MAGIC, isEnabled))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun fetchProcesses(pm: PackageManager): Collection<ProcessInfo> {
|
||||
val flag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES or
|
||||
GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
|
||||
val packageInfo = try {
|
||||
pm.getPackageInfo(info.packageName, flag)
|
||||
} catch (e: Exception) {
|
||||
// Exceed binder data transfer limit, parse the package locally
|
||||
pm.getPackageArchiveInfo(info.sourceDir, flag) ?: return emptyList()
|
||||
}
|
||||
|
||||
firstInstallTime = packageInfo.firstInstallTime
|
||||
lastUpdateTime = packageInfo.lastUpdateTime
|
||||
|
||||
val processSet = TreeSet<ProcessInfo>(compareBy({ it.name }, { it.isIsolated }))
|
||||
processSet += packageInfo.activities.toProcessList()
|
||||
processSet += packageInfo.services.toProcessList()
|
||||
processSet += packageInfo.receivers.toProcessList()
|
||||
processSet += packageInfo.providers.toProcessList()
|
||||
return processSet
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val comparator = compareBy<AppProcessInfo>(
|
||||
{ it.label.lowercase(Locale.ROOT) },
|
||||
{ it.info.packageName }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class ProcessInfo(
|
||||
val name: String,
|
||||
val packageName: String,
|
||||
var isEnabled: Boolean
|
||||
) {
|
||||
val isIsolated = packageName == ISOLATED_MAGIC
|
||||
val isAppZygote = name.endsWith("_zygote")
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package com.topjohnwu.magisk.ui.deny
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.topjohnwu.magisk.ui.component.ListPopupDefaults.MenuPositionProvider
|
||||
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.Checkbox
|
||||
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
|
||||
import top.yukonga.miuix.kmp.basic.DropdownImpl
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.LinearProgressIndicator
|
||||
import top.yukonga.miuix.kmp.basic.ListPopupColumn
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.PopupPositionProvider
|
||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||
import top.yukonga.miuix.kmp.extra.SuperListPopup
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.extended.Back
|
||||
import top.yukonga.miuix.kmp.icon.extended.Sort
|
||||
import top.yukonga.miuix.kmp.icon.extended.Tune
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@Composable
|
||||
fun DenyListScreen(viewModel: DenyListViewModel, onBack: () -> Unit) {
|
||||
val loading by viewModel.loading.collectAsState()
|
||||
val apps by viewModel.filteredApps.collectAsState()
|
||||
val query by viewModel.query.collectAsState()
|
||||
val showSystem by viewModel.showSystem.collectAsState()
|
||||
val showOS by viewModel.showOS.collectAsState()
|
||||
val sortBy by viewModel.sortBy.collectAsState()
|
||||
val sortReverse by viewModel.sortReverse.collectAsState()
|
||||
|
||||
val showSortMenu = remember { mutableStateOf(false) }
|
||||
val showFilterMenu = remember { mutableStateOf(false) }
|
||||
|
||||
val scrollBehavior = MiuixScrollBehavior()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(CoreR.string.denylist),
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MiuixIcons.Back,
|
||||
contentDescription = null,
|
||||
tint = MiuixTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
Box {
|
||||
IconButton(
|
||||
onClick = { showSortMenu.value = true },
|
||||
holdDownState = showSortMenu.value,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MiuixIcons.Sort,
|
||||
contentDescription = stringResource(CoreR.string.menu_sort),
|
||||
)
|
||||
}
|
||||
SuperListPopup(
|
||||
show = showSortMenu,
|
||||
popupPositionProvider = MenuPositionProvider,
|
||||
alignment = PopupPositionProvider.Align.End,
|
||||
onDismissRequest = { showSortMenu.value = false }
|
||||
) {
|
||||
ListPopupColumn {
|
||||
val sortOptions = listOf(
|
||||
CoreR.string.sort_by_name to SortBy.NAME,
|
||||
CoreR.string.sort_by_package_name to SortBy.PACKAGE_NAME,
|
||||
CoreR.string.sort_by_install_time to SortBy.INSTALL_TIME,
|
||||
CoreR.string.sort_by_update_time to SortBy.UPDATE_TIME,
|
||||
)
|
||||
val totalSize = sortOptions.size + 1
|
||||
sortOptions.forEachIndexed { index, (resId, sort) ->
|
||||
DropdownImpl(
|
||||
text = stringResource(resId),
|
||||
optionSize = totalSize,
|
||||
isSelected = sortBy == sort,
|
||||
index = index,
|
||||
onSelectedIndexChange = {
|
||||
viewModel.setSortBy(sort)
|
||||
showSortMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
DropdownImpl(
|
||||
text = stringResource(CoreR.string.sort_reverse),
|
||||
optionSize = totalSize,
|
||||
isSelected = sortReverse,
|
||||
index = sortOptions.size,
|
||||
onSelectedIndexChange = {
|
||||
viewModel.toggleSortReverse()
|
||||
showSortMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
onClick = { showFilterMenu.value = true },
|
||||
holdDownState = showFilterMenu.value,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MiuixIcons.Tune,
|
||||
contentDescription = stringResource(CoreR.string.hide_filter_hint),
|
||||
)
|
||||
}
|
||||
SuperListPopup(
|
||||
show = showFilterMenu,
|
||||
popupPositionProvider = MenuPositionProvider,
|
||||
alignment = PopupPositionProvider.Align.End,
|
||||
onDismissRequest = { showFilterMenu.value = false }
|
||||
) {
|
||||
ListPopupColumn {
|
||||
DropdownImpl(
|
||||
text = stringResource(CoreR.string.show_system_app),
|
||||
optionSize = 2,
|
||||
isSelected = showSystem,
|
||||
index = 0,
|
||||
onSelectedIndexChange = {
|
||||
viewModel.setShowSystem(!showSystem)
|
||||
showFilterMenu.value = false
|
||||
}
|
||||
)
|
||||
DropdownImpl(
|
||||
text = stringResource(CoreR.string.show_os_app),
|
||||
optionSize = 2,
|
||||
isSelected = showOS,
|
||||
index = 1,
|
||||
onSelectedIndexChange = {
|
||||
if (!showOS && !showSystem) {
|
||||
viewModel.setShowSystem(true)
|
||||
}
|
||||
viewModel.setShowOS(!showOS)
|
||||
showFilterMenu.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||
SearchInput(
|
||||
query = query,
|
||||
onQueryChange = viewModel::setQuery,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
)
|
||||
|
||||
if (loading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.loading),
|
||||
style = MiuixTheme.textStyles.headline2
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(top = 8.dp, bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(
|
||||
items = apps,
|
||||
key = { it.info.packageName }
|
||||
) { app ->
|
||||
DenyAppCard(app)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchInput(query: String, onQueryChange: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||
top.yukonga.miuix.kmp.basic.TextField(
|
||||
value = query,
|
||||
onValueChange = onQueryChange,
|
||||
modifier = modifier,
|
||||
label = stringResource(CoreR.string.hide_filter_hint)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DenyAppCard(app: DenyAppState) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column {
|
||||
if (app.checkedPercent > 0f) {
|
||||
LinearProgressIndicator(
|
||||
progress = app.checkedPercent,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { app.isExpanded = !app.isExpanded }
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(app.info.iconImage),
|
||||
contentDescription = app.info.label,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = app.info.label,
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
)
|
||||
Text(
|
||||
text = app.info.packageName,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Checkbox(
|
||||
state = when {
|
||||
app.itemsChecked == 0 -> ToggleableState.Off
|
||||
app.checkedPercent < 1f -> ToggleableState.Indeterminate
|
||||
else -> ToggleableState.On
|
||||
},
|
||||
onClick = { app.toggleAll() }
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = app.isExpanded) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 52.dp)
|
||||
) {
|
||||
app.processes.forEach { proc ->
|
||||
ProcessRow(proc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProcessRow(proc: DenyProcessState) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { proc.toggle() }
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = proc.displayName,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = if (proc.isEnabled) MiuixTheme.colorScheme.onSurface
|
||||
else MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Checkbox(
|
||||
state = ToggleableState(proc.isEnabled),
|
||||
onClick = { proc.toggle() }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.topjohnwu.magisk.ui.deny
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.ktx.concurrentMap
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.toCollection
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
enum class SortBy { NAME, PACKAGE_NAME, INSTALL_TIME, UPDATE_TIME }
|
||||
|
||||
class DenyListViewModel : AsyncLoadViewModel() {
|
||||
|
||||
private val _loading = MutableStateFlow(true)
|
||||
val loading: StateFlow<Boolean> = _loading.asStateFlow()
|
||||
|
||||
private val _allApps = MutableStateFlow<List<DenyAppState>>(emptyList())
|
||||
|
||||
private val _query = MutableStateFlow("")
|
||||
val query: StateFlow<String> = _query.asStateFlow()
|
||||
|
||||
private val _showSystem = MutableStateFlow(false)
|
||||
val showSystem: StateFlow<Boolean> = _showSystem.asStateFlow()
|
||||
|
||||
private val _showOS = MutableStateFlow(false)
|
||||
val showOS: StateFlow<Boolean> = _showOS.asStateFlow()
|
||||
|
||||
private val _sortBy = MutableStateFlow(SortBy.NAME)
|
||||
val sortBy: StateFlow<SortBy> = _sortBy.asStateFlow()
|
||||
|
||||
private val _sortReverse = MutableStateFlow(false)
|
||||
val sortReverse: StateFlow<Boolean> = _sortReverse.asStateFlow()
|
||||
|
||||
val filteredApps: StateFlow<List<DenyAppState>> = combine(
|
||||
_allApps, _query, _showSystem, _showOS, _sortBy, _sortReverse
|
||||
) { args ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val apps = args[0] as List<DenyAppState>
|
||||
val q = args[1] as String
|
||||
val showSys = args[2] as Boolean
|
||||
val showOS = args[3] as Boolean
|
||||
val sort = args[4] as SortBy
|
||||
val reverse = args[5] as Boolean
|
||||
|
||||
val filtered = apps.filter { app ->
|
||||
val passFilter = app.isChecked ||
|
||||
((showSys || !app.info.isSystemApp()) &&
|
||||
((showSys && showOS) || app.info.isApp()))
|
||||
val passQuery = q.isBlank() ||
|
||||
app.info.label.contains(q, true) ||
|
||||
app.info.packageName.contains(q, true) ||
|
||||
app.processes.any { it.process.name.contains(q, true) }
|
||||
passFilter && passQuery
|
||||
}
|
||||
|
||||
val secondary: Comparator<DenyAppState> = when (sort) {
|
||||
SortBy.NAME -> compareBy(String.CASE_INSENSITIVE_ORDER) { it.info.label }
|
||||
SortBy.PACKAGE_NAME -> compareBy(String.CASE_INSENSITIVE_ORDER) { it.info.packageName }
|
||||
SortBy.INSTALL_TIME -> compareByDescending { it.info.firstInstallTime }
|
||||
SortBy.UPDATE_TIME -> compareByDescending { it.info.lastUpdateTime }
|
||||
}
|
||||
val comparator = compareBy<DenyAppState> { it.itemsChecked == 0 }
|
||||
.then(if (reverse) secondary.reversed() else secondary)
|
||||
filtered.sortedWith(comparator)
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
fun setQuery(q: String) { _query.value = q }
|
||||
fun setShowSystem(v: Boolean) {
|
||||
_showSystem.value = v
|
||||
if (!v) _showOS.value = false
|
||||
}
|
||||
fun setShowOS(v: Boolean) { _showOS.value = v }
|
||||
fun setSortBy(s: SortBy) { _sortBy.value = s }
|
||||
fun toggleSortReverse() { _sortReverse.value = !_sortReverse.value }
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override suspend fun doLoadWork() {
|
||||
_loading.value = true
|
||||
val apps = withContext(Dispatchers.Default) {
|
||||
val pm = AppContext.packageManager
|
||||
val denyList = Shell.cmd("magisk --denylist ls").exec().out
|
||||
.map { CmdlineListItem(it) }
|
||||
val apps = pm.getInstalledApplications(MATCH_UNINSTALLED_PACKAGES).run {
|
||||
asFlow()
|
||||
.filter { AppContext.packageName != it.packageName }
|
||||
.concurrentMap { AppProcessInfo(it, pm, denyList) }
|
||||
.filter { it.processes.isNotEmpty() }
|
||||
.concurrentMap { DenyAppState(it) }
|
||||
.toCollection(ArrayList(size))
|
||||
}
|
||||
apps.sortWith(compareBy(
|
||||
{ it.processes.count { p -> p.isEnabled } == 0 },
|
||||
{ it.info }
|
||||
))
|
||||
apps
|
||||
}
|
||||
_allApps.value = apps
|
||||
_loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
class DenyAppState(val info: AppProcessInfo) : Comparable<DenyAppState> {
|
||||
val processes = info.processes.map { DenyProcessState(it) }
|
||||
var isExpanded by mutableStateOf(false)
|
||||
|
||||
val itemsChecked: Int get() = processes.count { it.isEnabled }
|
||||
val isChecked: Boolean get() = itemsChecked > 0
|
||||
val checkedPercent: Float get() = if (processes.isEmpty()) 0f else itemsChecked.toFloat() / processes.size
|
||||
|
||||
fun toggleAll() {
|
||||
if (isChecked) {
|
||||
Shell.cmd("magisk --denylist rm ${info.packageName}").submit()
|
||||
processes.filter { it.isEnabled }.forEach { proc ->
|
||||
if (proc.process.isIsolated) {
|
||||
proc.toggle()
|
||||
} else {
|
||||
proc.isEnabled = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
processes.filterNot { it.isEnabled }.forEach { it.toggle() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun compareTo(other: DenyAppState) = comparator.compare(this, other)
|
||||
|
||||
companion object {
|
||||
private val comparator = compareBy<DenyAppState>(
|
||||
{ it.itemsChecked == 0 },
|
||||
{ it.info }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class DenyProcessState(val process: ProcessInfo) {
|
||||
var isEnabled by mutableStateOf(process.isEnabled)
|
||||
|
||||
val displayName: String =
|
||||
if (process.isIsolated) "(isolated) ${process.name}*" else process.name
|
||||
|
||||
fun toggle() {
|
||||
isEnabled = !isEnabled
|
||||
val arg = if (isEnabled) "add" else "rm"
|
||||
val (name, pkg) = process
|
||||
Shell.cmd("magisk --denylist $arg $pkg \'$name\'").submit()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.topjohnwu.magisk.ui.flash
|
||||
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.ui.terminal.TerminalScreen
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.extended.Back
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@Composable
|
||||
fun FlashScreen(viewModel: FlashViewModel, action: String, onBack: () -> Unit) {
|
||||
val flashState by viewModel.flashState.collectAsState()
|
||||
val showReboot by viewModel.showReboot.collectAsState()
|
||||
val finished = flashState != FlashViewModel.State.FLASHING
|
||||
val useTerminal = action == Const.Value.FLASH_ZIP
|
||||
|
||||
val statusText = when (flashState) {
|
||||
FlashViewModel.State.FLASHING -> stringResource(CoreR.string.flashing)
|
||||
FlashViewModel.State.SUCCESS -> stringResource(CoreR.string.done)
|
||||
FlashViewModel.State.FAILED -> stringResource(CoreR.string.failure)
|
||||
}
|
||||
|
||||
val scrollBehavior = MiuixScrollBehavior()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SmallTopAppBar(
|
||||
title = "${stringResource(CoreR.string.flash_screen_title)} - $statusText",
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MiuixIcons.Back,
|
||||
contentDescription = null,
|
||||
tint = MiuixTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (finished) {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
onClick = { viewModel.saveLog() }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_save_md2),
|
||||
contentDescription = stringResource(CoreR.string.menuSaveLog),
|
||||
tint = MiuixTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
if (flashState == FlashViewModel.State.SUCCESS && showReboot) {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
onClick = { viewModel.restartPressed() }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_restart),
|
||||
contentDescription = stringResource(CoreR.string.reboot),
|
||||
tint = MiuixTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
if (useTerminal) {
|
||||
TerminalScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
onEmulatorCreated = { viewModel.onEmulatorCreated(it) },
|
||||
)
|
||||
} else {
|
||||
val items = viewModel.consoleItems
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LaunchedEffect(items.size) {
|
||||
if (items.isNotEmpty()) {
|
||||
listState.animateScrollToItem(items.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.horizontalScroll(rememberScrollState())
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
) {
|
||||
itemsIndexed(items) { _, line ->
|
||||
Text(
|
||||
text = line,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
color = MiuixTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.topjohnwu.magisk.ui.flash
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.cmp
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
|
||||
object FlashUtils {
|
||||
|
||||
const val INTENT_FLASH = "com.topjohnwu.magisk.intent.FLASH"
|
||||
const val EXTRA_FLASH_ACTION = "flash_action"
|
||||
const val EXTRA_FLASH_URI = "flash_uri"
|
||||
|
||||
fun installIntent(context: Context, file: Uri): PendingIntent {
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
component = MainActivity::class.java.cmp(context.packageName)
|
||||
action = INTENT_FLASH
|
||||
putExtra(EXTRA_FLASH_ACTION, Const.Value.FLASH_ZIP)
|
||||
putExtra(EXTRA_FLASH_URI, file.toString())
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
return PendingIntent.getActivity(
|
||||
context, file.hashCode(), intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.topjohnwu.magisk.ui.flash
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.core.net.toFile
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.ktx.reboot
|
||||
import com.topjohnwu.magisk.core.ktx.synchronized
|
||||
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.ktx.writeTo
|
||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.terminal.TerminalEmulator
|
||||
import com.topjohnwu.magisk.terminal.appendLineOnMain
|
||||
import com.topjohnwu.magisk.terminal.runSuCommand
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
|
||||
class FlashViewModel : BaseViewModel() {
|
||||
|
||||
enum class State {
|
||||
FLASHING, SUCCESS, FAILED
|
||||
}
|
||||
|
||||
private val _flashState = MutableStateFlow(State.FLASHING)
|
||||
val flashState: StateFlow<State> = _flashState.asStateFlow()
|
||||
|
||||
private val _showReboot = MutableStateFlow(Info.isRooted)
|
||||
val showReboot: StateFlow<Boolean> = _showReboot.asStateFlow()
|
||||
|
||||
var flashAction: String = ""
|
||||
var flashUri: Uri? = null
|
||||
|
||||
// --- TerminalScreen mode (FLASH_ZIP) ---
|
||||
|
||||
private var emulator: TerminalEmulator? = null
|
||||
private val emulatorReady = CompletableDeferred<TerminalEmulator>()
|
||||
|
||||
fun onEmulatorCreated(emu: TerminalEmulator) {
|
||||
emulator = emu
|
||||
emulatorReady.complete(emu)
|
||||
}
|
||||
|
||||
// --- LazyColumn mode (MagiskInstaller) ---
|
||||
|
||||
val consoleItems = mutableStateListOf<String>()
|
||||
private val logItems = mutableListOf<String>().synchronized()
|
||||
private val outItems = object : CallbackList<String>() {
|
||||
override fun onAddElement(e: String?) {
|
||||
e ?: return
|
||||
consoleItems.add(e)
|
||||
logItems.add(e)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Shared ---
|
||||
|
||||
fun startFlashing() {
|
||||
val action = flashAction
|
||||
val uri = flashUri
|
||||
|
||||
viewModelScope.launch {
|
||||
when (action) {
|
||||
Const.Value.FLASH_ZIP -> {
|
||||
uri ?: return@launch
|
||||
flashZip(uri)
|
||||
}
|
||||
Const.Value.UNINSTALL -> {
|
||||
_showReboot.value = false
|
||||
onResult(withContext(Dispatchers.IO) {
|
||||
MagiskInstaller.Uninstall(outItems, logItems).exec()
|
||||
})
|
||||
}
|
||||
Const.Value.FLASH_MAGISK -> {
|
||||
onResult(withContext(Dispatchers.IO) {
|
||||
if (Info.isEmulator)
|
||||
MagiskInstaller.Emulator(outItems, logItems).exec()
|
||||
else
|
||||
MagiskInstaller.Direct(outItems, logItems).exec()
|
||||
})
|
||||
}
|
||||
Const.Value.FLASH_INACTIVE_SLOT -> {
|
||||
_showReboot.value = false
|
||||
onResult(withContext(Dispatchers.IO) {
|
||||
MagiskInstaller.SecondSlot(outItems, logItems).exec()
|
||||
})
|
||||
}
|
||||
Const.Value.PATCH_FILE -> {
|
||||
uri ?: return@launch
|
||||
_showReboot.value = false
|
||||
onResult(withContext(Dispatchers.IO) {
|
||||
MagiskInstaller.Patch(uri, outItems, logItems).exec()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onResult(success: Boolean) {
|
||||
_flashState.value = if (success) State.SUCCESS else State.FAILED
|
||||
}
|
||||
|
||||
private suspend fun flashZip(uri: Uri) {
|
||||
val emu = emulatorReady.await()
|
||||
|
||||
val installDir = File(AppContext.cacheDir, "flash")
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
installDir.deleteRecursively()
|
||||
installDir.mkdirs()
|
||||
|
||||
val zipFile = if (uri.scheme == "file") {
|
||||
uri.toFile()
|
||||
} else {
|
||||
File(installDir, "install.zip").also {
|
||||
try {
|
||||
uri.inputStream().writeTo(it)
|
||||
} catch (e: IOException) {
|
||||
val msg = if (e is FileNotFoundException) "Invalid Uri" else "Cannot copy to cache"
|
||||
return@withContext msg to null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val binary = File(installDir, "update-binary")
|
||||
AppContext.assets.open("module_installer.sh").use { it.writeTo(binary) }
|
||||
|
||||
val name = uri.displayName
|
||||
null to Triple(installDir, zipFile, name)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
"Unable to extract files" to null
|
||||
}
|
||||
}
|
||||
|
||||
val (error, prepResult) = result
|
||||
if (prepResult == null) {
|
||||
emu.appendLineOnMain("! ${error ?: "Installation failed"}")
|
||||
_flashState.value = State.FAILED
|
||||
return
|
||||
}
|
||||
|
||||
val (dir, zipFile, displayName) = prepResult
|
||||
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
runSuCommand(
|
||||
emu,
|
||||
"echo '- Installing $displayName'; " +
|
||||
"sh $dir/update-binary dummy 1 '${zipFile.absolutePath}'; " +
|
||||
"EXIT=\$?; " +
|
||||
"if [ \$EXIT -ne 0 ]; then echo '! Installation failed'; fi; " +
|
||||
"exit \$EXIT"
|
||||
)
|
||||
}
|
||||
|
||||
Shell.cmd("cd /", "rm -rf $dir ${Const.TMPDIR}").submit()
|
||||
_flashState.value = if (success) State.SUCCESS else State.FAILED
|
||||
}
|
||||
|
||||
fun saveLog() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val name = "magisk_install_log_%s.log".format(
|
||||
System.currentTimeMillis().toTime(timeFormatStandard)
|
||||
)
|
||||
val file = MediaStoreUtils.getFile(name)
|
||||
file.uri.outputStream().bufferedWriter().use { writer ->
|
||||
val transcript = emulator?.screen?.transcriptText
|
||||
if (transcript != null) {
|
||||
writer.write(transcript)
|
||||
} else {
|
||||
synchronized(logItems) {
|
||||
logItems.forEach {
|
||||
writer.write(it)
|
||||
writer.newLine()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
showSnackbar(file.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun restartPressed() = reboot()
|
||||
}
|
||||
1160
app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt
Normal file
1160
app/apk-ng/src/main/java/com/topjohnwu/magisk/ui/home/HomeScreen.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
package com.topjohnwu.magisk.ui.home
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.core.download.Subject.App
|
||||
import com.topjohnwu.magisk.core.ktx.await
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlin.math.roundToInt
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
class HomeViewModel(
|
||||
private val svc: NetworkService
|
||||
) : AsyncLoadViewModel() {
|
||||
|
||||
enum class State {
|
||||
LOADING, INVALID, OUTDATED, UP_TO_DATE
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
val isNoticeVisible: Boolean = Config.safetyNotice,
|
||||
val appState: State = State.LOADING,
|
||||
val managerRemoteVersion: String = "",
|
||||
val managerProgress: Int = 0,
|
||||
val showUninstall: Boolean = false,
|
||||
val showManagerInstall: Boolean = false,
|
||||
val showHideRestore: Boolean = false,
|
||||
val envFixCode: Int = 0,
|
||||
)
|
||||
|
||||
private val _uiState = MutableStateFlow(UiState())
|
||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
||||
|
||||
val magiskState
|
||||
get() = when {
|
||||
Info.isRooted && Info.env.isUnsupported -> State.OUTDATED
|
||||
!Info.env.isActive -> State.INVALID
|
||||
Info.env.versionCode < BuildConfig.APP_VERSION_CODE -> State.OUTDATED
|
||||
else -> State.UP_TO_DATE
|
||||
}
|
||||
|
||||
val magiskInstalledVersion: String
|
||||
get() = Info.env.run {
|
||||
if (isActive)
|
||||
"$versionString ($versionCode)" + if (isDebug) " (D)" else ""
|
||||
else
|
||||
""
|
||||
}
|
||||
|
||||
val managerInstalledVersion: String
|
||||
get() = "${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})" +
|
||||
if (BuildConfig.DEBUG) " (D)" else ""
|
||||
|
||||
companion object {
|
||||
private var checkedEnv = false
|
||||
}
|
||||
|
||||
override suspend fun doLoadWork() {
|
||||
_uiState.update { it.copy(appState = State.LOADING) }
|
||||
Info.fetchUpdate(svc)?.apply {
|
||||
val isDebug = Config.updateChannel == Config.Value.DEBUG_CHANNEL
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
appState = if (BuildConfig.APP_VERSION_CODE < versionCode) State.OUTDATED else State.UP_TO_DATE,
|
||||
managerRemoteVersion = "$version ($versionCode)" + if (isDebug) " (D)" else ""
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
_uiState.update { it.copy(appState = State.INVALID, managerRemoteVersion = "") }
|
||||
}
|
||||
ensureEnv()
|
||||
}
|
||||
|
||||
private val networkObserver: (Boolean) -> Unit = { startLoading() }
|
||||
|
||||
init {
|
||||
Info.isConnected.observeForever(networkObserver)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
Info.isConnected.removeObserver(networkObserver)
|
||||
}
|
||||
|
||||
fun onProgressUpdate(progress: Float, subject: Subject) {
|
||||
if (subject is App)
|
||||
_uiState.update { it.copy(managerProgress = progress.times(100f).roundToInt()) }
|
||||
}
|
||||
|
||||
fun resetProgress() {
|
||||
_uiState.update { it.copy(managerProgress = 0) }
|
||||
}
|
||||
|
||||
fun onLinkPressed(link: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW, link.toUri())
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
AppContext.startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
AppContext.toast(CoreR.string.open_link_failed_toast, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeletePressed() {
|
||||
_uiState.update { it.copy(showUninstall = true) }
|
||||
}
|
||||
|
||||
fun onUninstallConsumed() {
|
||||
_uiState.update { it.copy(showUninstall = false) }
|
||||
}
|
||||
|
||||
fun onManagerPressed() {
|
||||
when (_uiState.value.appState) {
|
||||
State.LOADING -> showSnackbar(CoreR.string.loading)
|
||||
State.INVALID -> showSnackbar(CoreR.string.no_connection)
|
||||
else -> _uiState.update { it.copy(showManagerInstall = true) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onManagerInstallConsumed() {
|
||||
_uiState.update { it.copy(showManagerInstall = false) }
|
||||
}
|
||||
|
||||
fun onHideRestorePressed() {
|
||||
_uiState.update { it.copy(showHideRestore = true) }
|
||||
}
|
||||
|
||||
fun onHideRestoreConsumed() {
|
||||
_uiState.update { it.copy(showHideRestore = false) }
|
||||
}
|
||||
|
||||
fun onEnvFixConsumed() {
|
||||
_uiState.update { it.copy(envFixCode = 0) }
|
||||
}
|
||||
|
||||
fun hideNotice() {
|
||||
Config.safetyNotice = false
|
||||
_uiState.update { it.copy(isNoticeVisible = false) }
|
||||
}
|
||||
|
||||
private suspend fun ensureEnv() {
|
||||
if (magiskState == State.INVALID || checkedEnv) return
|
||||
val cmd = "env_check ${Info.env.versionString} ${Info.env.versionCode}"
|
||||
val code = Shell.cmd(cmd).await().code
|
||||
if (code != 0) {
|
||||
_uiState.update { it.copy(envFixCode = code) }
|
||||
}
|
||||
checkedEnv = true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.topjohnwu.magisk.ui.install
|
||||
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.BuildConfig.APP_VERSION_CODE
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
class InstallViewModel(svc: NetworkService) : BaseViewModel() {
|
||||
|
||||
enum class Method { NONE, PATCH, DIRECT, INACTIVE_SLOT }
|
||||
|
||||
data class UiState(
|
||||
val step: Int = 0,
|
||||
val method: Method = Method.NONE,
|
||||
val notes: String = "",
|
||||
val patchUri: Uri? = null,
|
||||
val requestFilePicker: Boolean = false,
|
||||
val showSecondSlotWarning: Boolean = false,
|
||||
)
|
||||
|
||||
val isRooted get() = Info.isRooted
|
||||
val skipOptions = Info.isEmulator || (Info.isSAR && !Info.isFDE && Info.ramdisk)
|
||||
val noSecondSlot = !isRooted || !Info.isAB || Info.isEmulator
|
||||
|
||||
private val _uiState = MutableStateFlow(UiState(step = if (skipOptions) 1 else 0))
|
||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val noteFile = File(AppContext.cacheDir, "${APP_VERSION_CODE}.md")
|
||||
val noteText = when {
|
||||
noteFile.exists() -> noteFile.readText()
|
||||
else -> {
|
||||
val note = svc.fetchUpdate(APP_VERSION_CODE)?.note.orEmpty()
|
||||
if (note.isEmpty()) return@launch
|
||||
noteFile.writeText(note)
|
||||
note
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_uiState.update { it.copy(notes = noteText) }
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun nextStep() {
|
||||
_uiState.update { it.copy(step = 1) }
|
||||
}
|
||||
|
||||
fun selectMethod(method: Method) {
|
||||
_uiState.update { it.copy(method = method) }
|
||||
when (method) {
|
||||
Method.PATCH -> {
|
||||
AppContext.toast(CoreR.string.patch_file_msg, Toast.LENGTH_LONG)
|
||||
_uiState.update { it.copy(requestFilePicker = true) }
|
||||
}
|
||||
Method.INACTIVE_SLOT -> {
|
||||
_uiState.update { it.copy(showSecondSlotWarning = true) }
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
fun onFilePickerConsumed() {
|
||||
_uiState.update { it.copy(requestFilePicker = false) }
|
||||
}
|
||||
|
||||
fun onSecondSlotWarningConsumed() {
|
||||
_uiState.update { it.copy(showSecondSlotWarning = false) }
|
||||
}
|
||||
|
||||
fun onPatchFileSelected(uri: Uri) {
|
||||
_uiState.update { it.copy(patchUri = uri) }
|
||||
if (_uiState.value.method == Method.PATCH) {
|
||||
install()
|
||||
}
|
||||
}
|
||||
|
||||
fun install() {
|
||||
when (_uiState.value.method) {
|
||||
Method.PATCH -> navigateTo(Route.Flash(
|
||||
action = Const.Value.PATCH_FILE,
|
||||
additionalData = _uiState.value.patchUri!!.toString()
|
||||
))
|
||||
Method.DIRECT -> navigateTo(Route.Flash(
|
||||
action = Const.Value.FLASH_MAGISK
|
||||
))
|
||||
Method.INACTIVE_SLOT -> navigateTo(Route.Flash(
|
||||
action = Const.Value.FLASH_INACTIVE_SLOT
|
||||
))
|
||||
else -> error("Unknown method")
|
||||
}
|
||||
}
|
||||
|
||||
val canInstall: Boolean
|
||||
get() {
|
||||
val state = _uiState.value
|
||||
return when (state.method) {
|
||||
Method.PATCH -> state.patchUri != null
|
||||
Method.DIRECT, Method.INACTIVE_SLOT -> true
|
||||
Method.NONE -> false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package com.topjohnwu.magisk.ui.log
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.topjohnwu.magisk.core.ktx.timeDateFormat
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.TabRow
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.extended.Delete
|
||||
import top.yukonga.miuix.kmp.icon.extended.Download
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@Composable
|
||||
fun LogScreen(viewModel: LogViewModel) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
var selectedTab by rememberSaveable { mutableIntStateOf(0) }
|
||||
val tabTitles = listOf(
|
||||
stringResource(CoreR.string.superuser),
|
||||
stringResource(CoreR.string.magisk)
|
||||
)
|
||||
val scrollBehavior = MiuixScrollBehavior()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(CoreR.string.logs),
|
||||
actions = {
|
||||
if (selectedTab == 1) {
|
||||
IconButton(onClick = { viewModel.saveMagiskLog() }) {
|
||||
Icon(
|
||||
imageVector = MiuixIcons.Download,
|
||||
contentDescription = stringResource(CoreR.string.save_log),
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
onClick = {
|
||||
if (selectedTab == 0) viewModel.clearLog()
|
||||
else viewModel.clearMagiskLog()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MiuixIcons.Delete,
|
||||
contentDescription = stringResource(CoreR.string.clear_log),
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
Column(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
TabRow(
|
||||
tabs = tabTitles,
|
||||
selectedTabIndex = selectedTab,
|
||||
onTabSelected = { selectedTab = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
if (uiState.loading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
when (selectedTab) {
|
||||
0 -> SuLogTab(
|
||||
logs = uiState.suLogs,
|
||||
nestedScrollConnection = scrollBehavior.nestedScrollConnection
|
||||
)
|
||||
1 -> MagiskLogTab(
|
||||
entries = uiState.magiskLogEntries,
|
||||
nestedScrollConnection = scrollBehavior.nestedScrollConnection
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SuLogTab(logs: List<SuLog>, nestedScrollConnection: NestedScrollConnection) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
if (logs.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.log_data_none),
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(top = 8.dp, bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(logs, key = { it.id }) { log ->
|
||||
SuLogCard(log)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SuLogCard(log: SuLog) {
|
||||
val res = LocalContext.current.resources
|
||||
val pm = LocalContext.current.packageManager
|
||||
val icon = remember(log.packageName) {
|
||||
runCatching {
|
||||
pm.getApplicationInfo(log.packageName, 0).loadIcon(pm)
|
||||
}.getOrDefault(pm.defaultActivityIcon)
|
||||
}
|
||||
val allowed = log.action >= 2
|
||||
|
||||
val uidPidText = buildString {
|
||||
append("UID: ${log.toUid} PID: ${log.fromPid}")
|
||||
if (log.target != -1) {
|
||||
val target = if (log.target == 0) "magiskd" else log.target.toString()
|
||||
append(" → $target")
|
||||
}
|
||||
}
|
||||
|
||||
val details = buildString {
|
||||
if (log.context.isNotEmpty()) {
|
||||
append(res.getString(CoreR.string.selinux_context, log.context))
|
||||
}
|
||||
if (log.gids.isNotEmpty()) {
|
||||
if (isNotEmpty()) append("\n")
|
||||
append(res.getString(CoreR.string.supp_group, log.gids))
|
||||
}
|
||||
if (log.command.isNotEmpty()) {
|
||||
if (isNotEmpty()) append("\n")
|
||||
append(log.command)
|
||||
}
|
||||
}
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(icon),
|
||||
contentDescription = log.appName,
|
||||
modifier = Modifier.size(36.dp)
|
||||
)
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = log.appName,
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = uidPidText,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Column(horizontalAlignment = Alignment.End) {
|
||||
Text(
|
||||
text = log.time.toTime(timeDateFormat),
|
||||
fontSize = 11.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
maxLines = 1,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
SuActionBadge(allowed)
|
||||
}
|
||||
}
|
||||
|
||||
if (details.isNotEmpty()) {
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
text = details,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SuActionBadge(allowed: Boolean) {
|
||||
val bg = if (allowed) MiuixTheme.colorScheme.primary else MiuixTheme.colorScheme.error
|
||||
val fg = if (allowed) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onError
|
||||
val text = if (allowed) "Approved" else "Rejected"
|
||||
Text(
|
||||
text = text,
|
||||
color = fg,
|
||||
fontSize = 10.sp,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.background(bg, RoundedCornerShape(6.dp))
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MagiskLogTab(
|
||||
entries: List<MagiskLogEntry>,
|
||||
nestedScrollConnection: NestedScrollConnection
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
if (entries.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.log_data_magisk_none),
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val listState = rememberLazyListState(initialFirstVisibleItemIndex = entries.size - 1)
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(top = 8.dp, bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(entries.size, key = { it }) { index ->
|
||||
MagiskLogCard(entries[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MagiskLogCard(entry: MagiskLogEntry) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { expanded = !expanded }
|
||||
) {
|
||||
Column(modifier = Modifier.padding(12.dp)) {
|
||||
if (entry.isParsed) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
LogLevelBadge(entry.level)
|
||||
Text(
|
||||
text = entry.tag,
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
fontWeight = FontWeight.Normal,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
text = entry.timestamp,
|
||||
fontSize = 11.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = entry.message,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
color = MiuixTheme.colorScheme.onSurface,
|
||||
maxLines = if (expanded) Int.MAX_VALUE else 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LogLevelBadge(level: Char) {
|
||||
val (bg, fg) = when (level) {
|
||||
'V' -> Color(0xFF9E9E9E) to Color.White
|
||||
'D' -> Color(0xFF2196F3) to Color.White
|
||||
'I' -> Color(0xFF4CAF50) to Color.White
|
||||
'W' -> Color(0xFFFFC107) to Color.Black
|
||||
'E' -> Color(0xFFF44336) to Color.White
|
||||
'F' -> Color(0xFF9C27B0) to Color.White
|
||||
else -> Color(0xFF757575) to Color.White
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(bg)
|
||||
.padding(horizontal = 5.dp, vertical = 1.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = level.toString(),
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = fg,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.topjohnwu.magisk.ui.log
|
||||
|
||||
import android.system.Os
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.model.su.SuLog
|
||||
import com.topjohnwu.magisk.core.repository.LogRepository
|
||||
import com.topjohnwu.magisk.core.su.SuEvents
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileInputStream
|
||||
|
||||
class LogViewModel(
|
||||
private val repo: LogRepository
|
||||
) : AsyncLoadViewModel() {
|
||||
|
||||
init {
|
||||
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||
viewModelScope.launch {
|
||||
SuEvents.logUpdated.debounce(500).collect { reload() }
|
||||
}
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
val loading: Boolean = true,
|
||||
val magiskLog: String = "",
|
||||
val magiskLogEntries: List<MagiskLogEntry> = emptyList(),
|
||||
val suLogs: List<SuLog> = emptyList(),
|
||||
)
|
||||
|
||||
private val _uiState = MutableStateFlow(UiState())
|
||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
||||
|
||||
private var magiskLogRaw = ""
|
||||
|
||||
override suspend fun doLoadWork() {
|
||||
_uiState.update { it.copy(loading = true) }
|
||||
withContext(Dispatchers.Default) {
|
||||
magiskLogRaw = repo.fetchMagiskLogs()
|
||||
val suLogs = repo.fetchSuLogs()
|
||||
val entries = MagiskLogParser.parse(magiskLogRaw)
|
||||
_uiState.update { it.copy(
|
||||
loading = false,
|
||||
magiskLog = magiskLogRaw,
|
||||
magiskLogEntries = entries,
|
||||
suLogs = suLogs,
|
||||
) }
|
||||
}
|
||||
}
|
||||
|
||||
fun saveMagiskLog() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val filename = "magisk_log_%s.log".format(
|
||||
System.currentTimeMillis().toTime(timeFormatStandard))
|
||||
val logFile = MediaStoreUtils.getFile(filename)
|
||||
logFile.uri.outputStream().bufferedWriter().use { file ->
|
||||
file.write("---Detected Device Info---\n\n")
|
||||
file.write("isAB=${Info.isAB}\n")
|
||||
file.write("isSAR=${Info.isSAR}\n")
|
||||
file.write("ramdisk=${Info.ramdisk}\n")
|
||||
val uname = Os.uname()
|
||||
file.write("kernel=${uname.sysname} ${uname.machine} ${uname.release} ${uname.version}\n")
|
||||
|
||||
file.write("\n\n---System Properties---\n\n")
|
||||
ProcessBuilder("getprop").start()
|
||||
.inputStream.reader().use { it.copyTo(file) }
|
||||
|
||||
file.write("\n\n---Environment Variables---\n\n")
|
||||
System.getenv().forEach { (key, value) -> file.write("${key}=${value}\n") }
|
||||
|
||||
file.write("\n\n---System MountInfo---\n\n")
|
||||
FileInputStream("/proc/self/mountinfo").reader().use { it.copyTo(file) }
|
||||
|
||||
file.write("\n---Magisk Logs---\n")
|
||||
file.write("${Info.env.versionString} (${Info.env.versionCode})\n\n")
|
||||
if (Info.env.isActive) file.write(magiskLogRaw)
|
||||
|
||||
file.write("\n---Manager Logs---\n")
|
||||
file.write("${BuildConfig.APP_VERSION_NAME} (${BuildConfig.APP_VERSION_CODE})\n\n")
|
||||
ProcessBuilder("logcat", "-d").start()
|
||||
.inputStream.reader().use { it.copyTo(file) }
|
||||
}
|
||||
showSnackbar(logFile.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMagiskLog() = repo.clearMagiskLogs {
|
||||
showSnackbar(R.string.logs_cleared)
|
||||
startLoading()
|
||||
}
|
||||
|
||||
fun clearLog() = viewModelScope.launch {
|
||||
repo.clearLogs()
|
||||
showSnackbar(R.string.logs_cleared)
|
||||
startLoading()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.topjohnwu.magisk.ui.log
|
||||
|
||||
data class MagiskLogEntry(
|
||||
val timestamp: String = "",
|
||||
val pid: Int = 0,
|
||||
val tid: Int = 0,
|
||||
val level: Char = 'I',
|
||||
val tag: String = "",
|
||||
val message: String = "",
|
||||
val isParsed: Boolean = false,
|
||||
)
|
||||
|
||||
object MagiskLogParser {
|
||||
|
||||
// Logcat format: "MM-DD HH:MM:SS.mmm PID TID LEVEL TAG : message"
|
||||
private val logcatRegex = Regex(
|
||||
"""(\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d{3})\s+(\d+)\s+(\d+)\s+([VDIWEF])\s+(.+?)\s*:\s+(.*)"""
|
||||
)
|
||||
|
||||
fun parse(raw: String): List<MagiskLogEntry> {
|
||||
if (raw.isBlank()) return emptyList()
|
||||
|
||||
val lines = raw.lines()
|
||||
val result = mutableListOf<MagiskLogEntry>()
|
||||
|
||||
for (line in lines) {
|
||||
if (line.isBlank()) continue
|
||||
|
||||
val match = logcatRegex.find(line)
|
||||
if (match != null) {
|
||||
result.add(
|
||||
MagiskLogEntry(
|
||||
timestamp = match.groupValues[1],
|
||||
pid = match.groupValues[2].toIntOrNull() ?: 0,
|
||||
tid = match.groupValues[3].toIntOrNull() ?: 0,
|
||||
level = match.groupValues[4].firstOrNull() ?: 'I',
|
||||
tag = match.groupValues[5].trim(),
|
||||
message = match.groupValues[6],
|
||||
isParsed = true,
|
||||
)
|
||||
)
|
||||
} else if (result.isNotEmpty() && result.last().isParsed) {
|
||||
// Continuation line — append to previous entry
|
||||
val prev = result.last()
|
||||
result[result.lastIndex] = prev.copy(
|
||||
message = prev.message + "\n" + line.trimEnd()
|
||||
)
|
||||
} else {
|
||||
result.add(
|
||||
MagiskLogEntry(message = line.trimEnd())
|
||||
)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.ui.terminal.TerminalScreen
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.extended.Back
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@Composable
|
||||
fun ActionScreen(viewModel: ActionViewModel, actionName: String, onBack: () -> Unit) {
|
||||
val actionState by viewModel.actionState.collectAsState()
|
||||
val finished = actionState != ActionViewModel.State.RUNNING
|
||||
|
||||
val scrollBehavior = MiuixScrollBehavior()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SmallTopAppBar(
|
||||
title = actionName,
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MiuixIcons.Back,
|
||||
contentDescription = null,
|
||||
tint = MiuixTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (finished) {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 16.dp),
|
||||
onClick = { viewModel.saveLog() }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_save_md2),
|
||||
contentDescription = stringResource(CoreR.string.menuSaveLog),
|
||||
tint = MiuixTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
TerminalScreen(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
onEmulatorCreated = { viewModel.onEmulatorCreated(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.core.ktx.toTime
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.terminal.TerminalEmulator
|
||||
import com.topjohnwu.magisk.terminal.runSuCommand
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ActionViewModel : BaseViewModel() {
|
||||
|
||||
enum class State {
|
||||
RUNNING, SUCCESS, FAILED
|
||||
}
|
||||
|
||||
private val _actionState = MutableStateFlow(State.RUNNING)
|
||||
val actionState: StateFlow<State> = _actionState.asStateFlow()
|
||||
|
||||
var actionId: String = ""
|
||||
var actionName: String = ""
|
||||
|
||||
private var emulator: TerminalEmulator? = null
|
||||
private val emulatorReady = CompletableDeferred<TerminalEmulator>()
|
||||
|
||||
fun onEmulatorCreated(emu: TerminalEmulator) {
|
||||
emulator = emu
|
||||
emulatorReady.complete(emu)
|
||||
}
|
||||
|
||||
fun startRunAction() {
|
||||
viewModelScope.launch {
|
||||
val emu = emulatorReady.await()
|
||||
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
runSuCommand(
|
||||
emu,
|
||||
"cd /data/adb/modules/$actionId && sh ./action.sh"
|
||||
)
|
||||
}
|
||||
|
||||
_actionState.value = if (success) State.SUCCESS else State.FAILED
|
||||
}
|
||||
}
|
||||
|
||||
fun saveLog() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val name = "%s_action_log_%s.log".format(
|
||||
actionName,
|
||||
System.currentTimeMillis().toTime(timeFormatStandard)
|
||||
)
|
||||
val file = MediaStoreUtils.getFile(name)
|
||||
file.uri.outputStream().bufferedWriter().use { writer ->
|
||||
val transcript = emulator?.screen?.transcriptText
|
||||
if (transcript != null) {
|
||||
writer.write(transcript)
|
||||
}
|
||||
}
|
||||
showSnackbar(file.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.di.ServiceLocator
|
||||
import com.topjohnwu.magisk.core.download.DownloadEngine
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.ui.component.ConfirmResult
|
||||
import com.topjohnwu.magisk.ui.component.MarkdownTextAsync
|
||||
import com.topjohnwu.magisk.ui.component.rememberConfirmDialog
|
||||
import kotlinx.coroutines.launch
|
||||
import top.yukonga.miuix.kmp.basic.ButtonDefaults
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
|
||||
import top.yukonga.miuix.kmp.basic.FloatingActionButton
|
||||
import top.yukonga.miuix.kmp.basic.HorizontalDivider
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.Switch
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TextButton
|
||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||
import top.yukonga.miuix.kmp.extra.SuperDialog
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.extended.Add
|
||||
import top.yukonga.miuix.kmp.icon.extended.Delete
|
||||
import top.yukonga.miuix.kmp.icon.extended.Play
|
||||
import top.yukonga.miuix.kmp.icon.extended.Undo
|
||||
import top.yukonga.miuix.kmp.icon.extended.UploadCloud
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@Composable
|
||||
fun ModuleScreen(viewModel: ModuleViewModel) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val scrollBehavior = MiuixScrollBehavior()
|
||||
val colorScheme = MiuixTheme.colorScheme
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val activity = context as MainActivity
|
||||
|
||||
var pendingZipUri by remember { mutableStateOf<Uri?>(null) }
|
||||
var pendingZipName by remember { mutableStateOf("") }
|
||||
val localInstallDialog = rememberConfirmDialog()
|
||||
val confirmInstallTitle = stringResource(CoreR.string.confirm_install_title)
|
||||
|
||||
var pendingOnlineModule by remember { mutableStateOf<OnlineModule?>(null) }
|
||||
val showOnlineDialog = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
|
||||
if (uri != null) {
|
||||
val displayName = context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (cursor.moveToFirst() && idx >= 0) cursor.getString(idx) else null
|
||||
} ?: uri.lastPathSegment ?: "module.zip"
|
||||
pendingZipUri = uri
|
||||
pendingZipName = displayName
|
||||
scope.launch {
|
||||
val result = localInstallDialog.awaitConfirm(
|
||||
title = confirmInstallTitle,
|
||||
content = context.getString(CoreR.string.confirm_install, displayName),
|
||||
)
|
||||
if (result == ConfirmResult.Confirmed) {
|
||||
viewModel.confirmLocalInstall(uri)
|
||||
}
|
||||
pendingZipUri = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showOnlineDialog.value && pendingOnlineModule != null) {
|
||||
OnlineModuleDialog(
|
||||
item = pendingOnlineModule!!,
|
||||
showDialog = showOnlineDialog,
|
||||
onDownload = { install ->
|
||||
showOnlineDialog.value = false
|
||||
DownloadEngine.startWithActivity(
|
||||
activity,
|
||||
OnlineModuleSubject(pendingOnlineModule!!, install)
|
||||
)
|
||||
pendingOnlineModule = null
|
||||
},
|
||||
onDismiss = {
|
||||
showOnlineDialog.value = false
|
||||
pendingOnlineModule = null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(CoreR.string.modules),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = { filePicker.launch("application/zip") },
|
||||
shadowElevation = 0.dp,
|
||||
modifier = Modifier
|
||||
.padding(bottom = 88.dp, end = 20.dp)
|
||||
.border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape),
|
||||
content = {
|
||||
Icon(
|
||||
imageVector = MiuixIcons.Add,
|
||||
contentDescription = stringResource(CoreR.string.module_action_install_external),
|
||||
modifier = Modifier.size(28.dp),
|
||||
tint = colorScheme.onPrimary
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
if (uiState.loading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
if (uiState.modules.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.module_empty),
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
color = colorScheme.onSurfaceVariantSummary
|
||||
)
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.padding(padding)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(bottom = 160.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item { Spacer(Modifier.height(4.dp)) }
|
||||
items(uiState.modules, key = { it.module.id }) { item ->
|
||||
ModuleCard(
|
||||
item = item,
|
||||
viewModel = viewModel,
|
||||
onUpdateClick = { onlineModule ->
|
||||
if (onlineModule != null && Info.isConnected.value == true) {
|
||||
pendingOnlineModule = onlineModule
|
||||
showOnlineDialog.value = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
item { Spacer(Modifier.height(4.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModuleCard(item: ModuleItem, viewModel: ModuleViewModel, onUpdateClick: (OnlineModule?) -> Unit) {
|
||||
val infoAlpha = if (!item.isRemoved && item.isEnabled && !item.showNotice) 1f else 0.5f
|
||||
val strikeThrough = if (item.isRemoved) TextDecoration.LineThrough else TextDecoration.None
|
||||
val colorScheme = MiuixTheme.colorScheme
|
||||
val actionIconTint = colorScheme.onSurface.copy(alpha = 0.8f)
|
||||
val actionBg = colorScheme.secondaryContainer.copy(alpha = 0.8f)
|
||||
val updateBg = colorScheme.tertiaryContainer.copy(alpha = 0.6f)
|
||||
val updateTint = colorScheme.onTertiaryContainer.copy(alpha = 0.8f)
|
||||
val removeBg = colorScheme.errorContainer.copy(alpha = 0.6f)
|
||||
val removeTint = colorScheme.onErrorContainer.copy(alpha = 0.8f)
|
||||
var expanded by rememberSaveable(item.module.id) { mutableStateOf(false) }
|
||||
val hasDescription = item.module.description.isNotBlank()
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
insideMargin = PaddingValues(16.dp),
|
||||
onClick = { if (hasDescription) expanded = !expanded }
|
||||
) {
|
||||
Column(modifier = Modifier.alpha(infoAlpha)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = item.module.name,
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
textDecoration = strikeThrough,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
CoreR.string.module_version_author,
|
||||
item.module.version,
|
||||
item.module.author
|
||||
),
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = colorScheme.onSurfaceVariantSummary,
|
||||
textDecoration = strikeThrough,
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = item.isEnabled,
|
||||
onCheckedChange = { viewModel.toggleEnabled(item) }
|
||||
)
|
||||
}
|
||||
|
||||
if (hasDescription) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp)
|
||||
.animateContentSize()
|
||||
) {
|
||||
Text(
|
||||
text = item.module.description,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = colorScheme.onSurfaceVariantSummary,
|
||||
textDecoration = strikeThrough,
|
||||
overflow = if (expanded) TextOverflow.Clip else TextOverflow.Ellipsis,
|
||||
maxLines = if (expanded) Int.MAX_VALUE else 4,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.showNotice) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = item.noticeText,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
thickness = 0.5.dp,
|
||||
color = colorScheme.outline.copy(alpha = 0.5f)
|
||||
)
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
AnimatedVisibility(
|
||||
visible = item.isEnabled && !item.isRemoved,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (item.showAction) {
|
||||
IconButton(
|
||||
backgroundColor = actionBg,
|
||||
minHeight = 35.dp,
|
||||
minWidth = 35.dp,
|
||||
onClick = { viewModel.runAction(item.module.id, item.module.name) },
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = MiuixIcons.Play,
|
||||
tint = actionIconTint,
|
||||
contentDescription = stringResource(CoreR.string.module_action)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(CoreR.string.module_action),
|
||||
color = actionIconTint,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = item.showUpdate && item.updateReady,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
backgroundColor = updateBg,
|
||||
minHeight = 35.dp,
|
||||
minWidth = 35.dp,
|
||||
onClick = { onUpdateClick(item.module.updateInfo) },
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = MiuixIcons.UploadCloud,
|
||||
tint = updateTint,
|
||||
contentDescription = stringResource(CoreR.string.update),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(CoreR.string.update),
|
||||
color = updateTint,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(
|
||||
backgroundColor = if (item.isRemoved) actionBg else removeBg,
|
||||
minHeight = 35.dp,
|
||||
minWidth = 35.dp,
|
||||
onClick = { viewModel.toggleRemove(item) },
|
||||
enabled = !item.isUpdated
|
||||
) {
|
||||
val tint = if (item.isRemoved) actionIconTint else removeTint
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = if (item.isRemoved) MiuixIcons.Undo else MiuixIcons.Delete,
|
||||
tint = tint,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (item.isRemoved) CoreR.string.module_state_restore
|
||||
else CoreR.string.module_state_remove
|
||||
),
|
||||
color = tint,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OnlineModuleDialog(
|
||||
item: OnlineModule,
|
||||
showDialog: MutableState<Boolean>,
|
||||
onDownload: (install: Boolean) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val svc = ServiceLocator.networkService
|
||||
val title = stringResource(
|
||||
CoreR.string.repo_install_title,
|
||||
item.name, item.version, item.versionCode
|
||||
)
|
||||
|
||||
SuperDialog(
|
||||
show = showDialog,
|
||||
title = title,
|
||||
onDismissRequest = onDismiss,
|
||||
) {
|
||||
MarkdownTextAsync {
|
||||
val str = svc.fetchString(item.changelog)
|
||||
if (str.length > 1000) str.substring(0, 1000) else str
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
TextButton(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
TextButton(
|
||||
text = stringResource(CoreR.string.download),
|
||||
onClick = { onDownload(false) },
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
TextButton(
|
||||
text = stringResource(CoreR.string.install),
|
||||
onClick = { onDownload(true) },
|
||||
colors = ButtonDefaults.textButtonColorsPrimary()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.topjohnwu.magisk.ui.module
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.ui.flash.FlashUtils
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
class ModuleItem(val module: LocalModule) {
|
||||
val showNotice: Boolean
|
||||
val showAction: Boolean
|
||||
val noticeText: String
|
||||
|
||||
init {
|
||||
val isZygisk = module.isZygisk
|
||||
val isRiru = module.isRiru
|
||||
val zygiskUnloaded = isZygisk && module.zygiskUnloaded
|
||||
|
||||
showNotice = zygiskUnloaded ||
|
||||
(Info.isZygiskEnabled && isRiru) ||
|
||||
(!Info.isZygiskEnabled && isZygisk)
|
||||
showAction = module.hasAction && !showNotice
|
||||
noticeText = when {
|
||||
zygiskUnloaded -> "Zygisk module not loaded due to incompatibility"
|
||||
isRiru -> "Module suspended because Zygisk is enabled"
|
||||
else -> "Module suspended because Zygisk isn't enabled"
|
||||
}
|
||||
}
|
||||
|
||||
var isEnabled by mutableStateOf(module.enable)
|
||||
var isRemoved by mutableStateOf(module.remove)
|
||||
var showUpdate by mutableStateOf(module.updateInfo != null)
|
||||
val isUpdated = module.updated
|
||||
val updateReady get() = module.outdated && !isRemoved && isEnabled
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class OnlineModuleSubject(
|
||||
override val module: OnlineModule,
|
||||
override val autoLaunch: Boolean,
|
||||
override val notifyId: Int = Notifications.nextId()
|
||||
) : Subject.Module() {
|
||||
override fun pendingIntent(context: Context) = FlashUtils.installIntent(context, file)
|
||||
}
|
||||
|
||||
class ModuleViewModel : AsyncLoadViewModel() {
|
||||
|
||||
data class UiState(
|
||||
val loading: Boolean = true,
|
||||
val modules: List<ModuleItem> = emptyList(),
|
||||
)
|
||||
|
||||
private val _uiState = MutableStateFlow(UiState())
|
||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
||||
|
||||
override suspend fun doLoadWork() {
|
||||
_uiState.update { it.copy(loading = true) }
|
||||
val moduleLoaded = Info.env.isActive &&
|
||||
withContext(Dispatchers.IO) { LocalModule.loaded() }
|
||||
if (moduleLoaded) {
|
||||
val modules = withContext(Dispatchers.Default) {
|
||||
LocalModule.installed().map { ModuleItem(it) }
|
||||
}
|
||||
_uiState.update { it.copy(loading = false, modules = modules) }
|
||||
loadUpdateInfo()
|
||||
} else {
|
||||
_uiState.update { it.copy(loading = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private val networkObserver: (Boolean) -> Unit = { startLoading() }
|
||||
|
||||
init {
|
||||
Info.isConnected.observeForever(networkObserver)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
Info.isConnected.removeObserver(networkObserver)
|
||||
}
|
||||
|
||||
private suspend fun loadUpdateInfo() {
|
||||
withContext(Dispatchers.IO) {
|
||||
_uiState.value.modules.forEach { item ->
|
||||
if (item.module.fetch()) {
|
||||
item.showUpdate = item.module.updateInfo != null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun confirmLocalInstall(uri: Uri) {
|
||||
navigateTo(Route.Flash(Const.Value.FLASH_ZIP, uri.toString()))
|
||||
}
|
||||
|
||||
fun runAction(id: String, name: String) {
|
||||
navigateTo(Route.Action(id, name))
|
||||
}
|
||||
|
||||
fun toggleEnabled(item: ModuleItem) {
|
||||
item.isEnabled = !item.isEnabled
|
||||
item.module.enable = item.isEnabled
|
||||
}
|
||||
|
||||
fun toggleRemove(item: ModuleItem) {
|
||||
item.isRemoved = !item.isRemoved
|
||||
item.module.remove = item.isRemoved
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.topjohnwu.magisk.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
|
||||
@Composable
|
||||
fun CollectNavEvents(viewModel: BaseViewModel, navigator: Navigator) {
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.navEvents.collect { route ->
|
||||
navigator.push(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.topjohnwu.magisk.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
|
||||
class Navigator(initialKey: NavKey) {
|
||||
val backStack: SnapshotStateList<NavKey> = mutableStateListOf(initialKey)
|
||||
|
||||
fun push(key: NavKey) {
|
||||
backStack.add(key)
|
||||
}
|
||||
|
||||
fun replace(key: NavKey) {
|
||||
if (backStack.isNotEmpty()) {
|
||||
backStack[backStack.lastIndex] = key
|
||||
} else {
|
||||
backStack.add(key)
|
||||
}
|
||||
}
|
||||
|
||||
fun replaceAll(keys: List<NavKey>) {
|
||||
if (keys.isEmpty()) return
|
||||
if (backStack.isNotEmpty()) {
|
||||
backStack.clear()
|
||||
backStack.addAll(keys)
|
||||
}
|
||||
}
|
||||
|
||||
fun pop() {
|
||||
backStack.removeLastOrNull()
|
||||
}
|
||||
|
||||
fun popUntil(predicate: (NavKey) -> Boolean) {
|
||||
while (backStack.isNotEmpty() && !predicate(backStack.last())) {
|
||||
backStack.removeAt(backStack.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun current(): NavKey? = backStack.lastOrNull()
|
||||
|
||||
fun backStackSize(): Int = backStack.size
|
||||
|
||||
companion object {
|
||||
val Saver: Saver<Navigator, Any> = listSaver(
|
||||
save = { navigator -> navigator.backStack.toList() },
|
||||
restore = { savedList ->
|
||||
val initialKey = savedList.firstOrNull() ?: Route.Main
|
||||
Navigator(initialKey).also {
|
||||
it.backStack.clear()
|
||||
it.backStack.addAll(savedList)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberNavigator(startRoute: NavKey): Navigator {
|
||||
return rememberSaveable(startRoute, saver = Navigator.Saver) {
|
||||
Navigator(startRoute)
|
||||
}
|
||||
}
|
||||
|
||||
val LocalNavigator = staticCompositionLocalOf<Navigator> {
|
||||
error("LocalNavigator not provided")
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.topjohnwu.magisk.ui.navigation
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
sealed interface Route : NavKey, Parcelable {
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data object Main : Route
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data object DenyList : Route
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class Flash(
|
||||
val action: String,
|
||||
val additionalData: String? = null,
|
||||
) : Route
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class SuperuserDetail(val uid: Int) : Route
|
||||
|
||||
@Parcelize
|
||||
@Serializable
|
||||
data class Action(
|
||||
val id: String,
|
||||
val name: String,
|
||||
) : Route
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
package com.topjohnwu.magisk.ui.settings
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import com.topjohnwu.magisk.core.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.AppMigration
|
||||
import com.topjohnwu.magisk.core.utils.LocaleSetting
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.ui.theme.ThemeState
|
||||
import top.yukonga.miuix.kmp.basic.ButtonDefaults
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.SmallTitle
|
||||
import top.yukonga.miuix.kmp.basic.TextButton
|
||||
import top.yukonga.miuix.kmp.basic.TextField
|
||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||
import top.yukonga.miuix.kmp.extra.SuperArrow
|
||||
import top.yukonga.miuix.kmp.extra.SuperDialog
|
||||
import top.yukonga.miuix.kmp.extra.SuperDropdown
|
||||
import top.yukonga.miuix.kmp.extra.SuperSwitch
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(viewModel: SettingsViewModel) {
|
||||
val scrollBehavior = MiuixScrollBehavior()
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(CoreR.string.settings),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(padding)
|
||||
.padding(horizontal = 12.dp)
|
||||
.padding(bottom = 88.dp)
|
||||
) {
|
||||
CustomizationSection(viewModel)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
AppSettingsSection(viewModel)
|
||||
if (Info.env.isActive) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
MagiskSection(viewModel)
|
||||
}
|
||||
if (Info.showSuperUser) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
SuperuserSection(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Customization ---
|
||||
|
||||
@Composable
|
||||
private fun CustomizationSection(viewModel: SettingsViewModel) {
|
||||
val context = LocalContext.current
|
||||
|
||||
SmallTitle(text = stringResource(CoreR.string.settings_customization))
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
if (LocaleSetting.useLocaleManager) {
|
||||
val locale = LocaleSetting.instance.appLocale
|
||||
val summary = locale?.getDisplayName(locale) ?: stringResource(CoreR.string.system_default)
|
||||
SuperArrow(
|
||||
title = stringResource(CoreR.string.language),
|
||||
summary = summary,
|
||||
onClick = {
|
||||
context.startActivity(LocaleSetting.localeSettingsIntent)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
val names = remember { LocaleSetting.available.names }
|
||||
val tags = remember { LocaleSetting.available.tags }
|
||||
var selectedIndex by remember {
|
||||
mutableIntStateOf(tags.indexOf(Config.locale).coerceAtLeast(0))
|
||||
}
|
||||
SuperDropdown(
|
||||
title = stringResource(CoreR.string.language),
|
||||
items = names.toList(),
|
||||
selectedIndex = selectedIndex,
|
||||
onSelectedIndexChange = { index ->
|
||||
selectedIndex = index
|
||||
Config.locale = tags[index]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Color Mode
|
||||
val resources = context.resources
|
||||
val colorModeEntries = remember {
|
||||
resources.getStringArray(CoreR.array.color_mode).toList()
|
||||
}
|
||||
var colorMode by remember { mutableIntStateOf(Config.colorMode) }
|
||||
SuperDropdown(
|
||||
title = stringResource(CoreR.string.settings_color_mode),
|
||||
items = colorModeEntries,
|
||||
selectedIndex = colorMode,
|
||||
onSelectedIndexChange = { index ->
|
||||
colorMode = index
|
||||
Config.colorMode = index
|
||||
ThemeState.colorMode = index
|
||||
}
|
||||
)
|
||||
|
||||
if (isRunningAsStub && ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {
|
||||
SuperArrow(
|
||||
title = stringResource(CoreR.string.add_shortcut_title),
|
||||
summary = stringResource(CoreR.string.setting_add_shortcut_summary),
|
||||
onClick = { viewModel.requestAddShortcut() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- App Settings ---
|
||||
|
||||
@Composable
|
||||
private fun AppSettingsSection(viewModel: SettingsViewModel) {
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
val hidden = context.packageName != BuildConfig.APP_PACKAGE_NAME
|
||||
|
||||
SmallTitle(text = stringResource(CoreR.string.home_app_title))
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
// Update Channel
|
||||
val updateChannelEntries = remember {
|
||||
resources.getStringArray(CoreR.array.update_channel).toList()
|
||||
}
|
||||
var updateChannel by remember {
|
||||
mutableIntStateOf(Config.updateChannel.coerceIn(0, updateChannelEntries.size - 1))
|
||||
}
|
||||
var showUrlDialog by remember { mutableStateOf(false) }
|
||||
|
||||
SuperDropdown(
|
||||
title = stringResource(CoreR.string.settings_update_channel_title),
|
||||
items = updateChannelEntries,
|
||||
selectedIndex = updateChannel,
|
||||
onSelectedIndexChange = { index ->
|
||||
updateChannel = index
|
||||
Config.updateChannel = index
|
||||
Info.resetUpdate()
|
||||
if (index == Config.Value.CUSTOM_CHANNEL && Config.customChannelUrl.isBlank()) {
|
||||
showUrlDialog = true
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Update Channel URL (for custom channel)
|
||||
if (updateChannel == Config.Value.CUSTOM_CHANNEL) {
|
||||
UpdateChannelUrlDialog(
|
||||
show = showUrlDialog,
|
||||
onDismiss = { showUrlDialog = false }
|
||||
)
|
||||
SuperArrow(
|
||||
title = stringResource(CoreR.string.settings_update_custom),
|
||||
summary = Config.customChannelUrl.ifBlank { null },
|
||||
onClick = { showUrlDialog = true }
|
||||
)
|
||||
}
|
||||
|
||||
// DoH Toggle
|
||||
var doh by remember { mutableStateOf(Config.doh) }
|
||||
SuperSwitch(
|
||||
title = stringResource(CoreR.string.settings_doh_title),
|
||||
summary = stringResource(CoreR.string.settings_doh_description),
|
||||
checked = doh,
|
||||
onCheckedChange = {
|
||||
doh = it
|
||||
Config.doh = it
|
||||
}
|
||||
)
|
||||
|
||||
// Update Checker
|
||||
var checkUpdate by remember { mutableStateOf(Config.checkUpdate) }
|
||||
SuperSwitch(
|
||||
title = stringResource(CoreR.string.settings_check_update_title),
|
||||
summary = stringResource(CoreR.string.settings_check_update_summary),
|
||||
checked = checkUpdate,
|
||||
onCheckedChange = { newValue ->
|
||||
checkUpdate = newValue
|
||||
Config.checkUpdate = newValue
|
||||
}
|
||||
)
|
||||
|
||||
// Download Path
|
||||
var showDownloadDialog by remember { mutableStateOf(false) }
|
||||
DownloadPathDialog(
|
||||
show = showDownloadDialog,
|
||||
onDismiss = { showDownloadDialog = false }
|
||||
)
|
||||
SuperArrow(
|
||||
title = stringResource(CoreR.string.settings_download_path_title),
|
||||
summary = MediaStoreUtils.fullPath(Config.downloadDir),
|
||||
onClick = {
|
||||
showDownloadDialog = true
|
||||
}
|
||||
)
|
||||
|
||||
// Random Package Name
|
||||
var randName by remember { mutableStateOf(Config.randName) }
|
||||
SuperSwitch(
|
||||
title = stringResource(CoreR.string.settings_random_name_title),
|
||||
summary = stringResource(CoreR.string.settings_random_name_description),
|
||||
checked = randName,
|
||||
onCheckedChange = {
|
||||
randName = it
|
||||
Config.randName = it
|
||||
}
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// --- Magisk ---
|
||||
|
||||
@Composable
|
||||
private fun MagiskSection(viewModel: SettingsViewModel) {
|
||||
SmallTitle(text = stringResource(CoreR.string.magisk))
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
// Systemless Hosts
|
||||
SuperArrow(
|
||||
title = stringResource(CoreR.string.settings_hosts_title),
|
||||
summary = stringResource(CoreR.string.settings_hosts_summary),
|
||||
onClick = { viewModel.createHosts() }
|
||||
)
|
||||
|
||||
if (Const.Version.atLeast_24_0()) {
|
||||
// Zygisk
|
||||
var zygisk by remember { mutableStateOf(Config.zygisk) }
|
||||
SuperSwitch(
|
||||
title = stringResource(CoreR.string.zygisk),
|
||||
summary = stringResource(
|
||||
if (zygisk != Info.isZygiskEnabled) CoreR.string.reboot_apply_change
|
||||
else CoreR.string.settings_zygisk_summary
|
||||
),
|
||||
checked = zygisk,
|
||||
onCheckedChange = {
|
||||
zygisk = it
|
||||
Config.zygisk = it
|
||||
viewModel.notifyZygiskChange()
|
||||
}
|
||||
)
|
||||
|
||||
// DenyList
|
||||
val denyListEnabled by viewModel.denyListEnabled.collectAsState()
|
||||
SuperSwitch(
|
||||
title = stringResource(CoreR.string.settings_denylist_title),
|
||||
summary = stringResource(CoreR.string.settings_denylist_summary),
|
||||
checked = denyListEnabled,
|
||||
onCheckedChange = { viewModel.toggleDenyList(it) }
|
||||
)
|
||||
|
||||
// DenyList Config
|
||||
SuperArrow(
|
||||
title = stringResource(CoreR.string.settings_denylist_config_title),
|
||||
summary = stringResource(CoreR.string.settings_denylist_config_summary),
|
||||
onClick = { viewModel.navigateToDenyList() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Superuser ---
|
||||
|
||||
@Composable
|
||||
private fun SuperuserSection(viewModel: SettingsViewModel) {
|
||||
val context = LocalContext.current
|
||||
val resources = context.resources
|
||||
|
||||
SmallTitle(text = stringResource(CoreR.string.superuser))
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
// Tapjack (SDK < S)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
var tapjack by remember { mutableStateOf(Config.suTapjack) }
|
||||
SuperSwitch(
|
||||
title = stringResource(CoreR.string.settings_su_tapjack_title),
|
||||
summary = stringResource(CoreR.string.settings_su_tapjack_summary),
|
||||
checked = tapjack,
|
||||
onCheckedChange = {
|
||||
tapjack = it
|
||||
Config.suTapjack = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Authentication
|
||||
var suAuth by remember { mutableStateOf(Config.suAuth) }
|
||||
SuperSwitch(
|
||||
title = stringResource(CoreR.string.settings_su_auth_title),
|
||||
summary = stringResource(
|
||||
if (Info.isDeviceSecure) CoreR.string.settings_su_auth_summary
|
||||
else CoreR.string.settings_su_auth_insecure
|
||||
),
|
||||
checked = suAuth,
|
||||
enabled = Info.isDeviceSecure,
|
||||
onCheckedChange = { newValue ->
|
||||
viewModel.withAuth {
|
||||
suAuth = newValue
|
||||
Config.suAuth = newValue
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Access Mode
|
||||
val accessEntries = remember {
|
||||
resources.getStringArray(CoreR.array.su_access).toList()
|
||||
}
|
||||
var accessMode by remember { mutableIntStateOf(Config.rootMode) }
|
||||
SuperDropdown(
|
||||
title = stringResource(CoreR.string.superuser_access),
|
||||
items = accessEntries,
|
||||
selectedIndex = accessMode,
|
||||
onSelectedIndexChange = {
|
||||
accessMode = it
|
||||
Config.rootMode = it
|
||||
}
|
||||
)
|
||||
|
||||
// Multiuser Mode
|
||||
val multiuserEntries = remember {
|
||||
resources.getStringArray(CoreR.array.multiuser_mode).toList()
|
||||
}
|
||||
val multiuserDescriptions = remember {
|
||||
resources.getStringArray(CoreR.array.multiuser_summary).toList()
|
||||
}
|
||||
var multiuserMode by remember { mutableIntStateOf(Config.suMultiuserMode) }
|
||||
SuperDropdown(
|
||||
title = stringResource(CoreR.string.multiuser_mode),
|
||||
summary = multiuserDescriptions.getOrElse(multiuserMode) { "" },
|
||||
items = multiuserEntries,
|
||||
selectedIndex = multiuserMode,
|
||||
enabled = Const.USER_ID == 0,
|
||||
onSelectedIndexChange = {
|
||||
multiuserMode = it
|
||||
Config.suMultiuserMode = it
|
||||
}
|
||||
)
|
||||
|
||||
// Mount Namespace Mode
|
||||
val namespaceEntries = remember {
|
||||
resources.getStringArray(CoreR.array.namespace).toList()
|
||||
}
|
||||
val namespaceDescriptions = remember {
|
||||
resources.getStringArray(CoreR.array.namespace_summary).toList()
|
||||
}
|
||||
var mntNamespaceMode by remember { mutableIntStateOf(Config.suMntNamespaceMode) }
|
||||
SuperDropdown(
|
||||
title = stringResource(CoreR.string.mount_namespace_mode),
|
||||
summary = namespaceDescriptions.getOrElse(mntNamespaceMode) { "" },
|
||||
items = namespaceEntries,
|
||||
selectedIndex = mntNamespaceMode,
|
||||
onSelectedIndexChange = {
|
||||
mntNamespaceMode = it
|
||||
Config.suMntNamespaceMode = it
|
||||
}
|
||||
)
|
||||
|
||||
// Automatic Response
|
||||
val autoResponseEntries = remember {
|
||||
resources.getStringArray(CoreR.array.auto_response).toList()
|
||||
}
|
||||
var autoResponse by remember { mutableIntStateOf(Config.suAutoResponse) }
|
||||
SuperDropdown(
|
||||
title = stringResource(CoreR.string.auto_response),
|
||||
items = autoResponseEntries,
|
||||
selectedIndex = autoResponse,
|
||||
onSelectedIndexChange = { newIndex ->
|
||||
val doIt = {
|
||||
autoResponse = newIndex
|
||||
Config.suAutoResponse = newIndex
|
||||
}
|
||||
if (Config.suAuth) viewModel.withAuth(doIt) else doIt()
|
||||
}
|
||||
)
|
||||
|
||||
// Request Timeout
|
||||
val timeoutEntries = remember {
|
||||
resources.getStringArray(CoreR.array.request_timeout).toList()
|
||||
}
|
||||
val timeoutValues = remember { listOf(10, 15, 20, 30, 45, 60) }
|
||||
var timeoutIndex by remember {
|
||||
mutableIntStateOf(timeoutValues.indexOf(Config.suDefaultTimeout).coerceAtLeast(0))
|
||||
}
|
||||
SuperDropdown(
|
||||
title = stringResource(CoreR.string.request_timeout),
|
||||
items = timeoutEntries,
|
||||
selectedIndex = timeoutIndex,
|
||||
onSelectedIndexChange = {
|
||||
timeoutIndex = it
|
||||
Config.suDefaultTimeout = timeoutValues[it]
|
||||
}
|
||||
)
|
||||
|
||||
// SU Notification
|
||||
val notifEntries = remember {
|
||||
resources.getStringArray(CoreR.array.su_notification).toList()
|
||||
}
|
||||
var suNotification by remember { mutableIntStateOf(Config.suNotification) }
|
||||
SuperDropdown(
|
||||
title = stringResource(CoreR.string.superuser_notification),
|
||||
items = notifEntries,
|
||||
selectedIndex = suNotification,
|
||||
onSelectedIndexChange = {
|
||||
suNotification = it
|
||||
Config.suNotification = it
|
||||
}
|
||||
)
|
||||
|
||||
// Reauthenticate (SDK < O)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
var reAuth by remember { mutableStateOf(Config.suReAuth) }
|
||||
SuperSwitch(
|
||||
title = stringResource(CoreR.string.settings_su_reauth_title),
|
||||
summary = stringResource(CoreR.string.settings_su_reauth_summary),
|
||||
checked = reAuth,
|
||||
onCheckedChange = {
|
||||
reAuth = it
|
||||
Config.suReAuth = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Restrict (version >= 30.1)
|
||||
if (Const.Version.atLeast_30_1()) {
|
||||
var restrict by remember { mutableStateOf(Config.suRestrict) }
|
||||
SuperSwitch(
|
||||
title = stringResource(CoreR.string.settings_su_restrict_title),
|
||||
summary = stringResource(CoreR.string.settings_su_restrict_summary),
|
||||
checked = restrict,
|
||||
onCheckedChange = {
|
||||
restrict = it
|
||||
Config.suRestrict = it
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Dialogs ---
|
||||
|
||||
@Composable
|
||||
private fun UpdateChannelUrlDialog(show: Boolean, onDismiss: () -> Unit) {
|
||||
val showState = rememberSaveable { mutableStateOf(show) }
|
||||
showState.value = show
|
||||
var url by rememberSaveable { mutableStateOf(Config.customChannelUrl) }
|
||||
|
||||
SuperDialog(
|
||||
show = showState,
|
||||
onDismissRequest = onDismiss,
|
||||
insideMargin = DpSize(24.dp, 24.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(top = 8.dp)) {
|
||||
TextField(
|
||||
value = url,
|
||||
onValueChange = { url = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = stringResource(CoreR.string.settings_update_custom_msg)
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
TextButton(
|
||||
text = stringResource(android.R.string.ok),
|
||||
onClick = {
|
||||
Config.customChannelUrl = url
|
||||
Info.resetUpdate()
|
||||
onDismiss()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadPathDialog(show: Boolean, onDismiss: () -> Unit) {
|
||||
val showState = rememberSaveable { mutableStateOf(show) }
|
||||
showState.value = show
|
||||
var path by rememberSaveable { mutableStateOf(Config.downloadDir) }
|
||||
|
||||
SuperDialog(
|
||||
show = showState,
|
||||
onDismissRequest = onDismiss,
|
||||
insideMargin = DpSize(24.dp, 24.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(top = 8.dp)) {
|
||||
top.yukonga.miuix.kmp.basic.Text(
|
||||
text = stringResource(CoreR.string.settings_download_path_message, MediaStoreUtils.fullPath(path)),
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
TextField(
|
||||
value = path,
|
||||
onValueChange = { path = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = stringResource(CoreR.string.settings_download_path_title)
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
TextButton(
|
||||
text = stringResource(android.R.string.ok),
|
||||
onClick = {
|
||||
Config.downloadDir = path
|
||||
onDismiss()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HideAppDialog(show: Boolean, onDismiss: () -> Unit, onConfirm: (String) -> Unit) {
|
||||
val showState = rememberSaveable { mutableStateOf(show) }
|
||||
showState.value = show
|
||||
var appName by rememberSaveable { mutableStateOf("Settings") }
|
||||
val isError = appName.length > AppMigration.MAX_LABEL_LENGTH || appName.isBlank()
|
||||
|
||||
SuperDialog(
|
||||
show = showState,
|
||||
title = stringResource(CoreR.string.settings_hide_app_title),
|
||||
onDismissRequest = onDismiss,
|
||||
insideMargin = DpSize(24.dp, 24.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(top = 8.dp)) {
|
||||
TextField(
|
||||
value = appName,
|
||||
onValueChange = { appName = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = stringResource(CoreR.string.settings_app_name_hint),
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TextButton(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(Modifier.width(20.dp))
|
||||
TextButton(
|
||||
text = stringResource(android.R.string.ok),
|
||||
onClick = { if (!isError) onConfirm(appName) },
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.textButtonColorsPrimary()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RestoreDialog(show: Boolean, onDismiss: () -> Unit, onConfirm: () -> Unit) {
|
||||
val showState = rememberSaveable { mutableStateOf(show) }
|
||||
showState.value = show
|
||||
|
||||
SuperDialog(
|
||||
show = showState,
|
||||
title = stringResource(CoreR.string.settings_restore_app_title),
|
||||
onDismissRequest = onDismiss,
|
||||
insideMargin = DpSize(24.dp, 24.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(top = 8.dp)) {
|
||||
top.yukonga.miuix.kmp.basic.Text(
|
||||
text = stringResource(CoreR.string.restore_app_confirmation),
|
||||
color = MiuixTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TextButton(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(Modifier.width(20.dp))
|
||||
TextButton(
|
||||
text = stringResource(android.R.string.ok),
|
||||
onClick = onConfirm,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.textButtonColorsPrimary()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.topjohnwu.magisk.ui.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.core.tasks.AppMigration
|
||||
import com.topjohnwu.magisk.core.utils.RootUtils
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SettingsViewModel : BaseViewModel() {
|
||||
|
||||
private val _denyListEnabled = MutableStateFlow(Config.denyList)
|
||||
val denyListEnabled: StateFlow<Boolean> = _denyListEnabled.asStateFlow()
|
||||
|
||||
val zygiskMismatch get() = Config.zygisk != Info.isZygiskEnabled
|
||||
|
||||
var authenticate: (onSuccess: () -> Unit) -> Unit = { it() }
|
||||
|
||||
fun navigateToDenyList() {
|
||||
navigateTo(Route.DenyList)
|
||||
}
|
||||
|
||||
fun requestAddShortcut() {
|
||||
Shortcuts.addHomeIcon(AppContext)
|
||||
}
|
||||
|
||||
suspend fun hideApp(context: Context, name: String): Boolean {
|
||||
val success = withContext(Dispatchers.IO) {
|
||||
AppMigration.patchAndHide(context, name)
|
||||
}
|
||||
if (!success) {
|
||||
context.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
suspend fun restoreApp(context: Context): Boolean {
|
||||
val success = AppMigration.restoreApp(context)
|
||||
if (!success) {
|
||||
context.toast(R.string.failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
fun createHosts() {
|
||||
viewModelScope.launch {
|
||||
RootUtils.addSystemlessHosts()
|
||||
AppContext.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleDenyList(enabled: Boolean) {
|
||||
_denyListEnabled.value = enabled
|
||||
val cmd = if (enabled) "enable" else "disable"
|
||||
Shell.cmd("magisk --denylist $cmd").submit { result ->
|
||||
if (result.isSuccess) {
|
||||
Config.denyList = enabled
|
||||
} else {
|
||||
_denyListEnabled.value = !enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun withAuth(action: () -> Unit) = authenticate(action)
|
||||
|
||||
fun notifyZygiskChange() {
|
||||
if (zygiskMismatch) showSnackbar(R.string.reboot_apply_change)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
package com.topjohnwu.magisk.ui.superuser
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.topjohnwu.magisk.ui.component.ConfirmResult
|
||||
import com.topjohnwu.magisk.ui.component.rememberConfirmDialog
|
||||
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
|
||||
import kotlinx.coroutines.launch
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.Switch
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.extended.Back
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@Composable
|
||||
fun SuperuserDetailScreen(
|
||||
uid: Int,
|
||||
viewModel: SuperuserViewModel,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val items = uiState.policies.filter { it.policy.uid == uid }
|
||||
val item = items.firstOrNull()
|
||||
val scrollBehavior = MiuixScrollBehavior()
|
||||
val scope = rememberCoroutineScope()
|
||||
val revokeDialog = rememberConfirmDialog()
|
||||
val revokeTitle = stringResource(CoreR.string.su_revoke_title)
|
||||
val revokeMsg = item?.let { stringResource(CoreR.string.su_revoke_msg, it.appName) } ?: ""
|
||||
|
||||
LaunchedEffect(Unit) { viewModel.refreshSuRestrict() }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(CoreR.string.superuser_setting),
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = MiuixIcons.Back,
|
||||
contentDescription = stringResource(CoreR.string.back),
|
||||
)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
if (item == null) return@Scaffold
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.padding(padding)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(item.icon),
|
||||
contentDescription = item.appName,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = item.title,
|
||||
style = MiuixTheme.textStyles.headline2,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (item.isSharedUid) {
|
||||
Spacer(Modifier.width(6.dp))
|
||||
SharedUidBadge()
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = item.packageName,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||
)
|
||||
Text(
|
||||
text = "UID: ${item.policy.uid}",
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column {
|
||||
if (uiState.suRestrict || item.isRestricted) {
|
||||
SwitchRow(
|
||||
title = stringResource(CoreR.string.settings_su_restrict_title),
|
||||
checked = item.isRestricted,
|
||||
onCheckedChange = { viewModel.toggleRestrict(item) }
|
||||
)
|
||||
}
|
||||
SwitchRow(
|
||||
title = stringResource(CoreR.string.superuser_toggle_notification),
|
||||
checked = item.notification,
|
||||
onCheckedChange = { viewModel.updateNotify(item) }
|
||||
)
|
||||
SwitchRow(
|
||||
title = stringResource(CoreR.string.logs),
|
||||
checked = item.logging,
|
||||
onCheckedChange = { viewModel.updateLogging(item) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
if (viewModel.requiresAuth) {
|
||||
viewModel.authenticate { viewModel.performDelete(item, onBack) }
|
||||
} else {
|
||||
scope.launch {
|
||||
val result = revokeDialog.awaitConfirm(
|
||||
title = revokeTitle,
|
||||
content = revokeMsg,
|
||||
)
|
||||
if (result == ConfirmResult.Confirmed) {
|
||||
viewModel.performDelete(item, onBack)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
RevokeRow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwitchRow(
|
||||
title: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = { onCheckedChange() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RevokeRow() {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.superuser_toggle_revoke),
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
color = MiuixTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 16.dp)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package com.topjohnwu.magisk.ui.superuser
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.topjohnwu.magisk.ui.navigation.LocalNavigator
|
||||
import com.topjohnwu.magisk.ui.navigation.Route
|
||||
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
|
||||
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
|
||||
import top.yukonga.miuix.kmp.basic.Scaffold
|
||||
import top.yukonga.miuix.kmp.basic.Switch
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TopAppBar
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@Composable
|
||||
fun SuperuserScreen(viewModel: SuperuserViewModel) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val scrollBehavior = MiuixScrollBehavior()
|
||||
val navigator = LocalNavigator.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = stringResource(CoreR.string.superuser),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
popupHost = { }
|
||||
) { padding ->
|
||||
if (uiState.loading) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
if (uiState.policies.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CoreR.string.superuser_policy_none),
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||
)
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.padding(padding)
|
||||
.padding(horizontal = 12.dp),
|
||||
contentPadding = PaddingValues(bottom = 88.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
item { Spacer(Modifier.height(4.dp)) }
|
||||
items(uiState.policies, key = { "${it.policy.uid}_${it.packageName}" }) { item ->
|
||||
PolicyCard(
|
||||
item = item,
|
||||
onToggle = { viewModel.togglePolicy(item) },
|
||||
onDetail = { navigator.push(Route.SuperuserDetail(item.policy.uid)) },
|
||||
)
|
||||
}
|
||||
item { Spacer(Modifier.height(4.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PolicyCard(
|
||||
item: PolicyItem,
|
||||
onToggle: () -> Unit,
|
||||
onDetail: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.alpha(if (item.isEnabled) 1f else 0.5f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.clickable(onClick = onDetail)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(item.icon),
|
||||
contentDescription = item.appName,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = item.title,
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (item.isSharedUid) {
|
||||
Spacer(Modifier.width(6.dp))
|
||||
SharedUidBadge()
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = item.packageName,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(vertical = 12.dp)
|
||||
.width(0.5.dp)
|
||||
.background(MiuixTheme.colorScheme.dividerLine)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onToggle)
|
||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Switch(
|
||||
checked = item.isEnabled,
|
||||
onCheckedChange = { onToggle() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SharedUidBadge(modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = "SharedUID",
|
||||
color = MiuixTheme.colorScheme.onPrimary,
|
||||
fontSize = 10.sp,
|
||||
maxLines = 1,
|
||||
modifier = modifier
|
||||
.background(MiuixTheme.colorScheme.primary, RoundedCornerShape(6.dp))
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package com.topjohnwu.magisk.ui.superuser
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Process
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.AsyncLoadViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.R
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.su.SuEvents
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
|
||||
class PolicyItem(
|
||||
val policy: SuPolicy,
|
||||
val packageName: String,
|
||||
val isSharedUid: Boolean,
|
||||
val icon: Drawable,
|
||||
val appName: String,
|
||||
) {
|
||||
val title get() = appName
|
||||
val showSlider = Config.suRestrict || policy.policy == SuPolicy.RESTRICT
|
||||
|
||||
var isExpanded by mutableStateOf(false)
|
||||
var policyValue by mutableIntStateOf(policy.policy)
|
||||
var notification by mutableStateOf(policy.notification)
|
||||
var logging by mutableStateOf(policy.logging)
|
||||
|
||||
val isEnabled get() = policyValue >= SuPolicy.ALLOW
|
||||
val isRestricted get() = policyValue == SuPolicy.RESTRICT
|
||||
}
|
||||
|
||||
class SuperuserViewModel(
|
||||
private val db: PolicyDao
|
||||
) : AsyncLoadViewModel() {
|
||||
|
||||
var authenticate: (onSuccess: () -> Unit) -> Unit = { it() }
|
||||
|
||||
init {
|
||||
@OptIn(kotlinx.coroutines.FlowPreview::class)
|
||||
viewModelScope.launch {
|
||||
SuEvents.policyChanged.debounce(500).collect { reload() }
|
||||
}
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
val loading: Boolean = true,
|
||||
val policies: List<PolicyItem> = emptyList(),
|
||||
val suRestrict: Boolean = Config.suRestrict,
|
||||
)
|
||||
|
||||
private val _uiState = MutableStateFlow(UiState())
|
||||
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override suspend fun doLoadWork() {
|
||||
if (!Info.showSuperUser) {
|
||||
_uiState.update { it.copy(loading = false) }
|
||||
return
|
||||
}
|
||||
_uiState.update { it.copy(loading = true) }
|
||||
withContext(Dispatchers.IO) {
|
||||
db.deleteOutdated()
|
||||
db.delete(AppContext.applicationInfo.uid)
|
||||
val policies = ArrayList<PolicyItem>()
|
||||
val pm = AppContext.packageManager
|
||||
for (policy in db.fetchAll()) {
|
||||
val pkgs =
|
||||
if (policy.uid == Process.SYSTEM_UID) arrayOf("android")
|
||||
else pm.getPackagesForUid(policy.uid)
|
||||
if (pkgs == null) {
|
||||
db.delete(policy.uid)
|
||||
continue
|
||||
}
|
||||
val map = pkgs.mapNotNull { pkg ->
|
||||
try {
|
||||
val info = pm.getPackageInfo(pkg, MATCH_UNINSTALLED_PACKAGES)
|
||||
PolicyItem(
|
||||
policy = policy,
|
||||
packageName = info.packageName,
|
||||
isSharedUid = info.sharedUserId != null,
|
||||
icon = info.applicationInfo?.loadIcon(pm) ?: pm.defaultActivityIcon,
|
||||
appName = info.applicationInfo?.getLabel(pm) ?: info.packageName
|
||||
)
|
||||
} catch (_: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
if (map.isEmpty()) {
|
||||
db.delete(policy.uid)
|
||||
continue
|
||||
}
|
||||
policies.addAll(map)
|
||||
}
|
||||
policies.sortWith(compareBy(
|
||||
{ it.appName.lowercase(Locale.ROOT) },
|
||||
{ it.packageName }
|
||||
))
|
||||
_uiState.update { it.copy(loading = false, policies = policies, suRestrict = Config.suRestrict) }
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshSuRestrict() {
|
||||
_uiState.update { it.copy(suRestrict = Config.suRestrict) }
|
||||
}
|
||||
|
||||
val requiresAuth get() = Config.suAuth
|
||||
|
||||
fun performDelete(item: PolicyItem, onDeleted: () -> Unit = {}) {
|
||||
viewModelScope.launch {
|
||||
db.delete(item.policy.uid)
|
||||
_uiState.update { state ->
|
||||
state.copy(policies = state.policies.filter { it.policy.uid != item.policy.uid })
|
||||
}
|
||||
onDeleted()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNotify(item: PolicyItem) {
|
||||
item.notification = !item.notification
|
||||
item.policy.notification = item.notification
|
||||
viewModelScope.launch {
|
||||
db.update(item.policy)
|
||||
_uiState.value.policies
|
||||
.filter { it.policy.uid == item.policy.uid }
|
||||
.forEach { it.notification = item.notification }
|
||||
val res = if (item.notification) R.string.su_snack_notif_on else R.string.su_snack_notif_off
|
||||
showSnackbar(AppContext.getString(res, item.appName))
|
||||
}
|
||||
}
|
||||
|
||||
fun updateLogging(item: PolicyItem) {
|
||||
item.logging = !item.logging
|
||||
item.policy.logging = item.logging
|
||||
viewModelScope.launch {
|
||||
db.update(item.policy)
|
||||
_uiState.value.policies
|
||||
.filter { it.policy.uid == item.policy.uid }
|
||||
.forEach { it.logging = item.logging }
|
||||
val res = if (item.logging) R.string.su_snack_log_on else R.string.su_snack_log_off
|
||||
showSnackbar(AppContext.getString(res, item.appName))
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePolicy(item: PolicyItem, newPolicy: Int) {
|
||||
fun updateState() {
|
||||
viewModelScope.launch {
|
||||
item.policy.policy = newPolicy
|
||||
item.policyValue = newPolicy
|
||||
db.update(item.policy)
|
||||
_uiState.value.policies
|
||||
.filter { it.policy.uid == item.policy.uid }
|
||||
.forEach { it.policyValue = newPolicy }
|
||||
val res = if (newPolicy >= SuPolicy.ALLOW) R.string.su_snack_grant else R.string.su_snack_deny
|
||||
showSnackbar(AppContext.getString(res, item.appName))
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.suAuth) {
|
||||
authenticate { updateState() }
|
||||
} else {
|
||||
updateState()
|
||||
}
|
||||
}
|
||||
|
||||
fun togglePolicy(item: PolicyItem) {
|
||||
val newPolicy = if (item.isEnabled) SuPolicy.DENY else SuPolicy.ALLOW
|
||||
updatePolicy(item, newPolicy)
|
||||
}
|
||||
|
||||
fun toggleRestrict(item: PolicyItem) {
|
||||
val newPolicy = if (item.isRestricted) SuPolicy.ALLOW else SuPolicy.RESTRICT
|
||||
updatePolicy(item, newPolicy)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.topjohnwu.magisk.ui.surequest
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.view.accessibility.AccessibilityNodeProvider
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.VMFactory
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.base.ActivityExtension
|
||||
import com.topjohnwu.magisk.core.base.UntrackedActivity
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler.REQUEST
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.ui.theme.MagiskTheme
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.yukonga.miuix.kmp.utils.MiuixPopupUtils.Companion.MiuixPopupHost
|
||||
|
||||
open class SuRequestActivity : AppCompatActivity(), UntrackedActivity {
|
||||
|
||||
private val extension = ActivityExtension(this)
|
||||
private val viewModel: SuRequestViewModel by lazy {
|
||||
ViewModelProvider(this, VMFactory)[SuRequestViewModel::class.java]
|
||||
}
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setDefaultNightMode(Config.darkTheme)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base.wrap())
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
extension.onCreate(savedInstanceState)
|
||||
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
window.addFlags(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
window.setHideOverlayWindows(true)
|
||||
}
|
||||
setTheme(Theme.selected.themeRes)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel.finishActivity = { finish() }
|
||||
viewModel.authenticate = { onSuccess ->
|
||||
extension.withAuthentication { if (it) onSuccess() }
|
||||
}
|
||||
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
val action = intent.getStringExtra("action")
|
||||
if (action == REQUEST) {
|
||||
viewModel.handleRequest(intent)
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
SuCallbackHandler.run(this@SuRequestActivity, action, intent.extras)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
|
||||
if (viewModel.useTapjackProtection) {
|
||||
window.decorView.rootView.accessibilityDelegate = EmptyAccessibilityDelegate
|
||||
}
|
||||
|
||||
setContent {
|
||||
MagiskTheme {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
SuRequestScreen(viewModel = viewModel)
|
||||
MiuixPopupHost()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
extension.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun getTheme(): Resources.Theme {
|
||||
val theme = super.getTheme()
|
||||
theme.applyStyle(R.style.Foundation_Floating, true)
|
||||
return theme
|
||||
}
|
||||
|
||||
@Deprecated("Use OnBackPressedDispatcher")
|
||||
override fun onBackPressed() {
|
||||
viewModel.denyPressed()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finishAndRemoveTask()
|
||||
}
|
||||
|
||||
private object EmptyAccessibilityDelegate : View.AccessibilityDelegate() {
|
||||
override fun sendAccessibilityEvent(host: View, eventType: Int) {}
|
||||
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?) = true
|
||||
override fun sendAccessibilityEventUnchecked(host: View, event: AccessibilityEvent) {}
|
||||
override fun dispatchPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) = true
|
||||
override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {}
|
||||
override fun onInitializeAccessibilityEvent(host: View, event: AccessibilityEvent) {}
|
||||
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {}
|
||||
override fun addExtraDataToAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo, extraDataKey: String, arguments: Bundle?) {}
|
||||
override fun onRequestSendAccessibilityEvent(host: ViewGroup, child: View, event: AccessibilityEvent): Boolean = false
|
||||
override fun getAccessibilityNodeProvider(host: View): AccessibilityNodeProvider? = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.topjohnwu.magisk.ui.surequest
|
||||
|
||||
import android.view.MotionEvent
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.topjohnwu.magisk.core.ktx.toast
|
||||
import com.topjohnwu.magisk.ui.superuser.SharedUidBadge
|
||||
import com.topjohnwu.magisk.ui.util.rememberDrawablePainter
|
||||
import top.yukonga.miuix.kmp.basic.ButtonDefaults
|
||||
import top.yukonga.miuix.kmp.basic.Card
|
||||
import top.yukonga.miuix.kmp.basic.Slider
|
||||
import top.yukonga.miuix.kmp.basic.SliderDefaults
|
||||
import top.yukonga.miuix.kmp.basic.Text
|
||||
import top.yukonga.miuix.kmp.basic.TextButton
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import com.topjohnwu.magisk.core.R as CoreR
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun SuRequestScreen(viewModel: SuRequestViewModel) {
|
||||
if (!viewModel.showUi) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val icon = viewModel.icon
|
||||
val title = viewModel.title
|
||||
val packageName = viewModel.packageName
|
||||
val grantEnabled = viewModel.grantEnabled
|
||||
val denyCountdown = viewModel.denyCountdown
|
||||
val selectedPosition = viewModel.selectedItemPosition
|
||||
val timeoutEntries = stringArrayResource(CoreR.array.allow_timeout).toList()
|
||||
// Slider order: Once(1), 10min(2), 20min(3), 30min(4), 60min(5), Forever(0)
|
||||
val sliderToIndex = intArrayOf(1, 2, 3, 4, 5, 0)
|
||||
val indexToSlider = remember {
|
||||
IntArray(sliderToIndex.size).also { arr ->
|
||||
sliderToIndex.forEachIndexed { slider, orig -> arr[orig] = slider }
|
||||
}
|
||||
}
|
||||
val sliderValue = indexToSlider[selectedPosition].toFloat()
|
||||
val sliderLabel by remember(sliderValue) {
|
||||
derivedStateOf { timeoutEntries[sliderToIndex[sliderValue.toInt()]] }
|
||||
}
|
||||
|
||||
val denyText = if (denyCountdown > 0) {
|
||||
"${stringResource(CoreR.string.deny)} ($denyCountdown)"
|
||||
} else {
|
||||
stringResource(CoreR.string.deny)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.widthIn(min = 320.dp, max = 420.dp)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
if (icon != null) {
|
||||
Image(
|
||||
painter = rememberDrawablePainter(icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
}
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MiuixTheme.textStyles.body1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (viewModel.isSharedUid) {
|
||||
Spacer(Modifier.width(6.dp))
|
||||
SharedUidBadge()
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = packageName,
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(CoreR.string.su_request_title),
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = MiuixTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Permission timeout: $sliderLabel",
|
||||
style = MiuixTheme.textStyles.body2,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 8.dp),
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Slider(
|
||||
value = sliderValue,
|
||||
onValueChange = { value ->
|
||||
viewModel.spinnerTouched()
|
||||
val pos = value.toInt().coerceIn(0, sliderToIndex.lastIndex)
|
||||
viewModel.selectedItemPosition = sliderToIndex[pos]
|
||||
},
|
||||
valueRange = 0f..5f,
|
||||
steps = 4,
|
||||
showKeyPoints = true,
|
||||
height = 20.dp,
|
||||
hapticEffect = SliderDefaults.SliderHapticEffect.Step,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 4.dp)
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
TextButton(
|
||||
text = denyText,
|
||||
onClick = { viewModel.denyPressed() },
|
||||
modifier = Modifier.weight(1f),
|
||||
cornerRadius = 12.dp,
|
||||
minHeight = 40.dp,
|
||||
insideMargin = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
TextButton(
|
||||
text = stringResource(CoreR.string.grant),
|
||||
enabled = grantEnabled,
|
||||
colors = ButtonDefaults.textButtonColorsPrimary(),
|
||||
onClick = { viewModel.grantPressed() },
|
||||
cornerRadius = 12.dp,
|
||||
minHeight = 40.dp,
|
||||
insideMargin = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.then(
|
||||
if (viewModel.useTapjackProtection) {
|
||||
Modifier.pointerInteropFilter { event ->
|
||||
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0 ||
|
||||
event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0
|
||||
) {
|
||||
if (event.action == MotionEvent.ACTION_UP) {
|
||||
context.toast(
|
||||
CoreR.string.touch_filtered_warning,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} else Modifier
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.topjohnwu.magisk.ui.surequest
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.CountDownTimer
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.core.AppContext
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.data.magiskdb.PolicyDao
|
||||
import com.topjohnwu.magisk.core.ktx.getLabel
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.ALLOW
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.DENY
|
||||
import com.topjohnwu.magisk.core.su.SuRequestHandler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
|
||||
class SuRequestViewModel(
|
||||
policyDB: PolicyDao,
|
||||
private val timeoutPrefs: SharedPreferences
|
||||
) : BaseViewModel() {
|
||||
|
||||
var authenticate: (onSuccess: () -> Unit) -> Unit = { it() }
|
||||
var finishActivity: () -> Unit = {}
|
||||
|
||||
var icon by mutableStateOf<Drawable?>(null)
|
||||
var title by mutableStateOf("")
|
||||
var packageName by mutableStateOf("")
|
||||
var isSharedUid by mutableStateOf(false)
|
||||
|
||||
var selectedItemPosition by mutableIntStateOf(0)
|
||||
var grantEnabled by mutableStateOf(false)
|
||||
var denyCountdown by mutableIntStateOf(0)
|
||||
|
||||
var showUi by mutableStateOf(false)
|
||||
var useTapjackProtection by mutableStateOf(false)
|
||||
|
||||
private val handler = SuRequestHandler(AppContext.packageManager, policyDB)
|
||||
private val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
|
||||
private var timer = SuTimer(millis, 1000)
|
||||
private var initialized = false
|
||||
|
||||
fun grantPressed() {
|
||||
cancelTimer()
|
||||
if (Config.suAuth) {
|
||||
authenticate { respond(ALLOW) }
|
||||
} else {
|
||||
respond(ALLOW)
|
||||
}
|
||||
}
|
||||
|
||||
fun denyPressed() {
|
||||
respond(DENY)
|
||||
}
|
||||
|
||||
fun spinnerTouched() {
|
||||
cancelTimer()
|
||||
}
|
||||
|
||||
fun handleRequest(intent: Intent) {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
if (handler.start(intent))
|
||||
showDialog()
|
||||
else
|
||||
finishActivity()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDialog() {
|
||||
val pm = handler.pm
|
||||
val info = handler.pkgInfo
|
||||
val app = info.applicationInfo
|
||||
|
||||
isSharedUid = info.sharedUserId != null
|
||||
if (app == null) {
|
||||
icon = pm.defaultActivityIcon
|
||||
title = info.sharedUserId.toString()
|
||||
packageName = info.sharedUserId.toString()
|
||||
} else {
|
||||
icon = app.loadIcon(pm)
|
||||
title = app.getLabel(pm)
|
||||
packageName = info.packageName
|
||||
}
|
||||
|
||||
selectedItemPosition = timeoutPrefs.getInt(packageName, 0)
|
||||
timer.start()
|
||||
useTapjackProtection = Config.suTapjack
|
||||
showUi = true
|
||||
initialized = true
|
||||
}
|
||||
|
||||
private fun respond(action: Int) {
|
||||
if (!initialized) return
|
||||
timer.cancel()
|
||||
|
||||
val pos = selectedItemPosition
|
||||
timeoutPrefs.edit().putInt(packageName, pos).apply()
|
||||
|
||||
viewModelScope.launch {
|
||||
handler.respond(action, Config.Value.TIMEOUT_LIST[pos])
|
||||
finishActivity()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelTimer() {
|
||||
timer.cancel()
|
||||
denyCountdown = 0
|
||||
}
|
||||
|
||||
private inner class SuTimer(
|
||||
private val millis: Long,
|
||||
interval: Long
|
||||
) : CountDownTimer(millis, interval) {
|
||||
|
||||
override fun onTick(remains: Long) {
|
||||
if (!grantEnabled && remains <= millis - 1000) {
|
||||
grantEnabled = true
|
||||
}
|
||||
denyCountdown = (remains / 1000).toInt() + 1
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
denyCountdown = 0
|
||||
respond(DENY)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package com.topjohnwu.magisk.ui.terminal
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.Typeface
|
||||
import com.topjohnwu.magisk.terminal.TerminalEmulator
|
||||
import com.topjohnwu.magisk.terminal.TextStyle
|
||||
import com.topjohnwu.magisk.terminal.WcWidth
|
||||
|
||||
/**
|
||||
* Renderer of a [TerminalEmulator] into a [Canvas].
|
||||
*
|
||||
* Saves font metrics, so needs to be recreated each time the typeface or font size changes.
|
||||
*/
|
||||
class TerminalRenderer(
|
||||
textSize: Int,
|
||||
typeface: Typeface,
|
||||
) {
|
||||
val textSize: Int = textSize
|
||||
val typeface: Typeface = typeface
|
||||
private val textPaint = Paint()
|
||||
|
||||
/** The width of a single mono spaced character obtained by [Paint.measureText] on a single 'X'. */
|
||||
val fontWidth: Float
|
||||
|
||||
/** The [Paint.getFontSpacing]. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
|
||||
val fontLineSpacing: Int
|
||||
|
||||
/** The [Paint.ascent]. See http://www.fampennings.nl/maarten/android/08numgrid/font.png */
|
||||
private val fontAscent: Int
|
||||
|
||||
/** The [fontLineSpacing] + [fontAscent]. */
|
||||
val fontLineSpacingAndAscent: Int
|
||||
|
||||
private val asciiMeasures = FloatArray(127)
|
||||
|
||||
init {
|
||||
textPaint.typeface = typeface
|
||||
textPaint.isAntiAlias = true
|
||||
textPaint.textSize = textSize.toFloat()
|
||||
|
||||
fontLineSpacing = kotlin.math.ceil(textPaint.fontSpacing).toInt()
|
||||
fontAscent = kotlin.math.ceil(textPaint.ascent()).toInt()
|
||||
fontLineSpacingAndAscent = fontLineSpacing + fontAscent
|
||||
fontWidth = textPaint.measureText("X")
|
||||
|
||||
val sb = StringBuilder(" ")
|
||||
for (i in asciiMeasures.indices) {
|
||||
sb[0] = i.toChar()
|
||||
asciiMeasures[i] = textPaint.measureText(sb, 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the terminal to a canvas with at a specified row scroll, and an optional rectangular selection.
|
||||
*/
|
||||
fun render(
|
||||
mEmulator: TerminalEmulator,
|
||||
canvas: Canvas,
|
||||
topRow: Int,
|
||||
selectionY1: Int,
|
||||
selectionY2: Int,
|
||||
selectionX1: Int,
|
||||
selectionX2: Int,
|
||||
) {
|
||||
val reverseVideo = mEmulator.isReverseVideo
|
||||
val endRow = topRow + mEmulator.mRows
|
||||
val columns = mEmulator.mColumns
|
||||
val cursorCol = mEmulator.cursorCol
|
||||
val cursorRow = mEmulator.cursorRow
|
||||
val cursorVisible = mEmulator.shouldCursorBeVisible()
|
||||
val screen = mEmulator.screen
|
||||
val palette = mEmulator.mColors.currentColors
|
||||
val cursorShape = mEmulator.cursorStyle
|
||||
|
||||
if (reverseVideo) {
|
||||
canvas.drawColor(palette[TextStyle.COLOR_INDEX_FOREGROUND], PorterDuff.Mode.SRC)
|
||||
}
|
||||
|
||||
var heightOffset = fontLineSpacingAndAscent.toFloat()
|
||||
for (row in topRow until endRow) {
|
||||
heightOffset += fontLineSpacing
|
||||
|
||||
val cursorX = if (row == cursorRow && cursorVisible) cursorCol else -1
|
||||
var selx1 = -1
|
||||
var selx2 = -1
|
||||
if (row in selectionY1..selectionY2) {
|
||||
if (row == selectionY1) selx1 = selectionX1
|
||||
selx2 = if (row == selectionY2) selectionX2 else mEmulator.mColumns
|
||||
}
|
||||
|
||||
val lineObject = screen.allocateFullLineIfNecessary(screen.externalToInternalRow(row))
|
||||
val line = lineObject.text
|
||||
val charsUsedInLine = lineObject.spaceUsed
|
||||
|
||||
var lastRunStyle = 0L
|
||||
var lastRunInsideCursor = false
|
||||
var lastRunInsideSelection = false
|
||||
var lastRunStartColumn = -1
|
||||
var lastRunStartIndex = 0
|
||||
var lastRunFontWidthMismatch = false
|
||||
var currentCharIndex = 0
|
||||
var measuredWidthForRun = 0f
|
||||
|
||||
var column = 0
|
||||
while (column < columns) {
|
||||
val charAtIndex = line[currentCharIndex]
|
||||
val charIsHighsurrogate = Character.isHighSurrogate(charAtIndex)
|
||||
val charsForCodePoint = if (charIsHighsurrogate) 2 else 1
|
||||
val codePoint = if (charIsHighsurrogate) {
|
||||
Character.toCodePoint(charAtIndex, line[currentCharIndex + 1])
|
||||
} else {
|
||||
charAtIndex.code
|
||||
}
|
||||
val codePointWcWidth = WcWidth.width(codePoint)
|
||||
val insideCursor = cursorX == column || (codePointWcWidth == 2 && cursorX == column + 1)
|
||||
val insideSelection = column >= selx1 && column <= selx2
|
||||
val style = lineObject.getStyle(column)
|
||||
|
||||
// Check if the measured text width for this code point is not the same as that expected by wcwidth().
|
||||
// This could happen for some fonts which are not truly monospace, or for more exotic characters such as
|
||||
// smileys which android font renders as wide.
|
||||
// If this is detected, we draw this code point scaled to match what wcwidth() expects.
|
||||
val measuredCodePointWidth = if (codePoint < asciiMeasures.size) {
|
||||
asciiMeasures[codePoint]
|
||||
} else {
|
||||
textPaint.measureText(line, currentCharIndex, charsForCodePoint)
|
||||
}
|
||||
val fontWidthMismatch = kotlin.math.abs(measuredCodePointWidth / fontWidth - codePointWcWidth.toFloat()) > 0.01f
|
||||
|
||||
if (style != lastRunStyle || insideCursor != lastRunInsideCursor || insideSelection != lastRunInsideSelection ||
|
||||
fontWidthMismatch || lastRunFontWidthMismatch
|
||||
) {
|
||||
if (column != 0) {
|
||||
val columnWidthSinceLastRun = column - lastRunStartColumn
|
||||
val charsSinceLastRun = currentCharIndex - lastRunStartIndex
|
||||
val cursorColor = if (lastRunInsideCursor) mEmulator.mColors.currentColors[TextStyle.COLOR_INDEX_CURSOR] else 0
|
||||
var invertCursorTextColor = false
|
||||
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
|
||||
invertCursorTextColor = true
|
||||
}
|
||||
drawTextRun(
|
||||
canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
||||
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
||||
cursorColor, cursorShape, lastRunStyle,
|
||||
reverseVideo || invertCursorTextColor || lastRunInsideSelection,
|
||||
)
|
||||
}
|
||||
measuredWidthForRun = 0f
|
||||
lastRunStyle = style
|
||||
lastRunInsideCursor = insideCursor
|
||||
lastRunInsideSelection = insideSelection
|
||||
lastRunStartColumn = column
|
||||
lastRunStartIndex = currentCharIndex
|
||||
lastRunFontWidthMismatch = fontWidthMismatch
|
||||
}
|
||||
measuredWidthForRun += measuredCodePointWidth
|
||||
column += codePointWcWidth
|
||||
currentCharIndex += charsForCodePoint
|
||||
while (currentCharIndex < charsUsedInLine && WcWidth.width(line, currentCharIndex) <= 0) {
|
||||
// Eat combining chars so that they are treated as part of the last non-combining code point,
|
||||
// instead of e.g. being considered inside the cursor in the next run.
|
||||
currentCharIndex += if (Character.isHighSurrogate(line[currentCharIndex])) 2 else 1
|
||||
}
|
||||
}
|
||||
|
||||
val columnWidthSinceLastRun = columns - lastRunStartColumn
|
||||
val charsSinceLastRun = currentCharIndex - lastRunStartIndex
|
||||
val cursorColor = if (lastRunInsideCursor) mEmulator.mColors.currentColors[TextStyle.COLOR_INDEX_CURSOR] else 0
|
||||
var invertCursorTextColor = false
|
||||
if (lastRunInsideCursor && cursorShape == TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK) {
|
||||
invertCursorTextColor = true
|
||||
}
|
||||
drawTextRun(
|
||||
canvas, line, palette, heightOffset, lastRunStartColumn, columnWidthSinceLastRun,
|
||||
lastRunStartIndex, charsSinceLastRun, measuredWidthForRun,
|
||||
cursorColor, cursorShape, lastRunStyle,
|
||||
reverseVideo || invertCursorTextColor || lastRunInsideSelection,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawTextRun(
|
||||
canvas: Canvas,
|
||||
text: CharArray,
|
||||
palette: IntArray,
|
||||
y: Float,
|
||||
startColumn: Int,
|
||||
runWidthColumns: Int,
|
||||
startCharIndex: Int,
|
||||
runWidthChars: Int,
|
||||
mes: Float,
|
||||
cursor: Int,
|
||||
cursorStyle: Int,
|
||||
textStyle: Long,
|
||||
reverseVideo: Boolean,
|
||||
) {
|
||||
var foreColor = TextStyle.decodeForeColor(textStyle)
|
||||
val effect = TextStyle.decodeEffect(textStyle)
|
||||
var backColor = TextStyle.decodeBackColor(textStyle)
|
||||
val bold = (effect and (TextStyle.CHARACTER_ATTRIBUTE_BOLD or TextStyle.CHARACTER_ATTRIBUTE_BLINK)) != 0
|
||||
val underline = (effect and TextStyle.CHARACTER_ATTRIBUTE_UNDERLINE) != 0
|
||||
val italic = (effect and TextStyle.CHARACTER_ATTRIBUTE_ITALIC) != 0
|
||||
val strikeThrough = (effect and TextStyle.CHARACTER_ATTRIBUTE_STRIKETHROUGH) != 0
|
||||
val dim = (effect and TextStyle.CHARACTER_ATTRIBUTE_DIM) != 0
|
||||
|
||||
if ((foreColor and 0xff000000.toInt()) != 0xff000000.toInt()) {
|
||||
// Let bold have bright colors if applicable (one of the first 8):
|
||||
if (bold && foreColor in 0..7) foreColor += 8
|
||||
foreColor = palette[foreColor]
|
||||
}
|
||||
|
||||
if ((backColor and 0xff000000.toInt()) != 0xff000000.toInt()) {
|
||||
backColor = palette[backColor]
|
||||
}
|
||||
|
||||
// Reverse video here if _one and only one_ of the reverse flags are set:
|
||||
val reverseVideoHere = reverseVideo xor ((effect and TextStyle.CHARACTER_ATTRIBUTE_INVERSE) != 0)
|
||||
if (reverseVideoHere) {
|
||||
val tmp = foreColor
|
||||
foreColor = backColor
|
||||
backColor = tmp
|
||||
}
|
||||
|
||||
var left = startColumn * fontWidth
|
||||
var right = left + runWidthColumns * fontWidth
|
||||
|
||||
var adjustedMes = mes / fontWidth
|
||||
var savedMatrix = false
|
||||
if (kotlin.math.abs(adjustedMes - runWidthColumns) > 0.01) {
|
||||
canvas.save()
|
||||
canvas.scale(runWidthColumns / adjustedMes, 1f)
|
||||
left *= adjustedMes / runWidthColumns
|
||||
right *= adjustedMes / runWidthColumns
|
||||
savedMatrix = true
|
||||
}
|
||||
|
||||
if (backColor != palette[TextStyle.COLOR_INDEX_BACKGROUND]) {
|
||||
// Only draw non-default background.
|
||||
textPaint.color = backColor
|
||||
canvas.drawRect(left, y - fontLineSpacingAndAscent + fontAscent, right, y, textPaint)
|
||||
}
|
||||
|
||||
if (cursor != 0) {
|
||||
textPaint.color = cursor
|
||||
var cursorHeight = (fontLineSpacingAndAscent - fontAscent).toFloat()
|
||||
if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE) cursorHeight /= 4f
|
||||
else if (cursorStyle == TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR) right -= ((right - left) * 3) / 4f
|
||||
canvas.drawRect(left, y - cursorHeight, right, y, textPaint)
|
||||
}
|
||||
|
||||
if ((effect and TextStyle.CHARACTER_ATTRIBUTE_INVISIBLE) == 0) {
|
||||
if (dim) {
|
||||
var red = 0xFF and (foreColor shr 16)
|
||||
var green = 0xFF and (foreColor shr 8)
|
||||
var blue = 0xFF and foreColor
|
||||
// Dim color handling used by libvte which in turn took it from xterm
|
||||
// (https://bug735245.bugzilla-attachments.gnome.org/attachment.cgi?id=284267):
|
||||
red = red * 2 / 3
|
||||
green = green * 2 / 3
|
||||
blue = blue * 2 / 3
|
||||
foreColor = -0x1000000 or (red shl 16) or (green shl 8) or blue
|
||||
}
|
||||
|
||||
textPaint.isFakeBoldText = bold
|
||||
textPaint.isUnderlineText = underline
|
||||
textPaint.textSkewX = if (italic) -0.35f else 0f
|
||||
textPaint.isStrikeThruText = strikeThrough
|
||||
textPaint.color = foreColor
|
||||
|
||||
// The text alignment is the default Paint.Align.LEFT.
|
||||
canvas.drawTextRun(
|
||||
text, startCharIndex, runWidthChars, startCharIndex, runWidthChars,
|
||||
left, y - fontLineSpacingAndAscent, false, textPaint,
|
||||
)
|
||||
}
|
||||
|
||||
if (savedMatrix) canvas.restore()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.topjohnwu.magisk.ui.terminal
|
||||
|
||||
import android.graphics.Typeface
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.rememberScrollableState
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.topjohnwu.magisk.terminal.TerminalEmulator
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
fun TerminalScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
onEmulatorCreated: (TerminalEmulator) -> Unit = {},
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val renderer = remember {
|
||||
val textSizePx = with(density) { 12.sp.toPx().toInt() }
|
||||
TerminalRenderer(textSizePx, Typeface.MONOSPACE)
|
||||
}
|
||||
|
||||
var emulator by remember { mutableStateOf<TerminalEmulator?>(null) }
|
||||
var updateTick by remember { mutableIntStateOf(0) }
|
||||
var topRow by remember { mutableIntStateOf(0) }
|
||||
var scrolledToBottom by remember { mutableStateOf(true) }
|
||||
|
||||
BoxWithConstraints(modifier = modifier) {
|
||||
val widthPx = constraints.maxWidth
|
||||
val heightPx = constraints.maxHeight
|
||||
val cols = max(4, (widthPx / renderer.fontWidth).toInt())
|
||||
val rows = max(4, (heightPx - renderer.fontLineSpacingAndAscent) / renderer.fontLineSpacing)
|
||||
val lineHeight = renderer.fontLineSpacing.toFloat()
|
||||
|
||||
LaunchedEffect(cols, rows) {
|
||||
val emu = emulator
|
||||
if (emu == null) {
|
||||
val newEmu = TerminalEmulator(cols, rows, renderer.fontWidth.toInt(), renderer.fontLineSpacing, null)
|
||||
newEmu.onScreenUpdate = {
|
||||
if (scrolledToBottom) topRow = 0
|
||||
updateTick++
|
||||
}
|
||||
emulator = newEmu
|
||||
onEmulatorCreated(newEmu)
|
||||
} else {
|
||||
emu.resize(cols, rows, renderer.fontWidth.toInt(), renderer.fontLineSpacing)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.scrollable(
|
||||
orientation = Orientation.Vertical,
|
||||
state = rememberScrollableState { delta ->
|
||||
val emu = emulator ?: return@rememberScrollableState 0f
|
||||
val minTop = -emu.screen.activeTranscriptRows
|
||||
val rowDelta = -(delta / lineHeight).toInt()
|
||||
if (rowDelta != 0) {
|
||||
val newTopRow = (topRow + rowDelta).coerceIn(minTop, 0)
|
||||
topRow = newTopRow
|
||||
scrolledToBottom = newTopRow >= 0
|
||||
}
|
||||
delta
|
||||
}
|
||||
)
|
||||
.drawBehind {
|
||||
@Suppress("UNUSED_EXPRESSION")
|
||||
updateTick
|
||||
val emu = emulator ?: return@drawBehind
|
||||
drawIntoCanvas { canvas ->
|
||||
renderer.render(emu, canvas.nativeCanvas, topRow, -1, -1, -1, -1)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.topjohnwu.magisk.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import top.yukonga.miuix.kmp.theme.ColorSchemeMode
|
||||
import top.yukonga.miuix.kmp.theme.LocalContentColor
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
import top.yukonga.miuix.kmp.theme.ThemeController
|
||||
|
||||
object ThemeState {
|
||||
var colorMode by mutableIntStateOf(Config.colorMode)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MagiskTheme(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val mode = ThemeState.colorMode
|
||||
val controller = when (mode) {
|
||||
1 -> ThemeController(ColorSchemeMode.Light)
|
||||
2 -> ThemeController(ColorSchemeMode.Dark)
|
||||
3 -> ThemeController(ColorSchemeMode.MonetSystem, isDark = isDark)
|
||||
4 -> ThemeController(ColorSchemeMode.MonetLight)
|
||||
5 -> ThemeController(ColorSchemeMode.MonetDark)
|
||||
else -> ThemeController(ColorSchemeMode.System)
|
||||
}
|
||||
MiuixTheme(controller = controller) {
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides MiuixTheme.colorScheme.onBackground,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.topjohnwu.magisk.ui.theme
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
|
||||
enum class Theme(
|
||||
val themeName: String,
|
||||
val themeRes: Int
|
||||
) {
|
||||
|
||||
Piplup(
|
||||
themeName = "Piplup",
|
||||
themeRes = R.style.ThemeFoundationMD2_Piplup
|
||||
),
|
||||
PiplupAmoled(
|
||||
themeName = "AMOLED",
|
||||
themeRes = R.style.ThemeFoundationMD2_Amoled
|
||||
),
|
||||
Rayquaza(
|
||||
themeName = "Rayquaza",
|
||||
themeRes = R.style.ThemeFoundationMD2_Rayquaza
|
||||
),
|
||||
Zapdos(
|
||||
themeName = "Zapdos",
|
||||
themeRes = R.style.ThemeFoundationMD2_Zapdos
|
||||
),
|
||||
Charmeleon(
|
||||
themeName = "Charmeleon",
|
||||
themeRes = R.style.ThemeFoundationMD2_Charmeleon
|
||||
),
|
||||
Mew(
|
||||
themeName = "Mew",
|
||||
themeRes = R.style.ThemeFoundationMD2_Mew
|
||||
),
|
||||
Salamence(
|
||||
themeName = "Salamence",
|
||||
themeRes = R.style.ThemeFoundationMD2_Salamence
|
||||
),
|
||||
Fraxure(
|
||||
themeName = "Fraxure (Legacy)",
|
||||
themeRes = R.style.ThemeFoundationMD2_Fraxure
|
||||
);
|
||||
|
||||
val isSelected get() = Config.themeOrdinal == ordinal
|
||||
|
||||
companion object {
|
||||
val selected get() = values().getOrNull(Config.themeOrdinal) ?: Piplup
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.topjohnwu.magisk.ui.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
|
||||
@Composable
|
||||
fun rememberDrawablePainter(drawable: Drawable): Painter {
|
||||
return remember(drawable) {
|
||||
val w = drawable.intrinsicWidth.coerceAtLeast(1)
|
||||
val h = drawable.intrinsicHeight.coerceAtLeast(1)
|
||||
val bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
val canvas = android.graphics.Canvas(bitmap)
|
||||
drawable.setBounds(0, 0, w, h)
|
||||
drawable.draw(canvas)
|
||||
BitmapPainter(bitmap.asImageBitmap())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.provider.Settings
|
||||
|
||||
class AccessibilityUtils {
|
||||
companion object {
|
||||
fun isAnimationEnabled(cr: ContentResolver): Boolean {
|
||||
return !(Settings.Global.getFloat(cr, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0.0f
|
||||
&& Settings.Global.getFloat(cr, Settings.Global.TRANSITION_ANIMATION_SCALE, 1.0f) == 0.0f
|
||||
&& Settings.Global.getFloat(cr, Settings.Global.WINDOW_ANIMATION_SCALE, 1.0f) == 0.0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?colorSurfaceVariant" android:state_enabled="true" />
|
||||
<item android:alpha="0.68" android:color="?colorSurfaceVariant" />
|
||||
</selector>
|
||||
5
app/apk-ng/src/main/res/color/color_error_transient.xml
Normal file
5
app/apk-ng/src/main/res/color/color_error_transient.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?colorDisabled" android:state_enabled="false" />
|
||||
<item android:color="?colorError" />
|
||||
</selector>
|
||||
6
app/apk-ng/src/main/res/color/color_menu_tint.xml
Normal file
6
app/apk-ng/src/main/res/color/color_menu_tint.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?colorDisabledVariant" android:state_enabled="false" />
|
||||
<item android:color="?colorSecondary" android:state_checked="true" />
|
||||
<item android:color="?colorOnSurfaceVariant" />
|
||||
</selector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?colorDisabled" android:state_enabled="false" />
|
||||
<item android:color="?colorOnPrimary" />
|
||||
</selector>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?colorError" android:state_selected="true" />
|
||||
<item android:color="?colorDisabled" android:state_enabled="false" />
|
||||
<item android:color="?colorPrimary" />
|
||||
</selector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?colorDisabled" android:state_enabled="false" />
|
||||
<item android:color="?colorPrimary" />
|
||||
</selector>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?colorPrimary" android:state_selected="true" />
|
||||
<item android:color="?colorPrimary" android:state_checked="true" />
|
||||
<item android:color="?colorDisabled" android:state_enabled="false" />
|
||||
<item android:color="?colorOnSurfaceVariant" />
|
||||
</selector>
|
||||
5
app/apk-ng/src/main/res/color/color_text_transient.xml
Normal file
5
app/apk-ng/src/main/res/color/color_text_transient.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?colorDisabled" android:state_enabled="false" />
|
||||
<item android:color="?colorOnSurface" />
|
||||
</selector>
|
||||
28
app/apk-ng/src/main/res/drawable/avd_bug_from_filled.xml
Normal file
28
app/apk-ng/src/main/res/drawable/avd_bug_from_filled.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M 20 8 L 17.19 8 C 16.74 7.22 16.12 6.55 15.37 6.04 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.49 5 12 5 C 11.51 5 11.04 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6.04 C 7.88 6.55 7.26 7.22 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.04 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.04 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 7.85 19.79 9.78 21 12 21 C 14.22 21 16.15 19.79 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.96 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.96 10.33 17.91 10 L 20 10 L 20 8 Z M 14 16 L 10 16 L 10 14 L 14 14 L 14 16 Z M 14 12 L 10 12 L 10 10 L 14 10 L 14 12 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path_1">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 20 8 L 20 8 L 17.19 8 C 16.74 7.22 16.12 6.55 15.37 6.04 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.49 5 12 5 C 11.51 5 11.04 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6.04 C 7.88 6.55 7.26 7.22 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.04 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.04 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 7.85 19.79 9.78 21 12 21 C 14.22 21 16.15 19.79 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.96 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.96 10.33 17.91 10 L 20 10 L 20 8 M 14 16 C 14 15.43 14 14.859 14 14.289 L 14 14 C 13.869 14 13.739 14 13.608 14 C 12.405 14 11.203 14 10 14 C 10 14.509 10 15.017 10 15.526 C 10 15.684 10 15.842 10 16 L 10.33 16 C 10.392 16 10.454 16 10.515 16 C 11.677 16 12.838 16 14 16 C 14 16 14 16 14 16 M 14 10 L 14 12 L 14 12 L 10 12 L 10 10 L 14 10 M 12 15 L 12 15 L 12 15 L 12 15 L 12 15 L 12 15"
|
||||
android:valueTo="M 20 8 L 18.595 8 L 17.19 8 C 16.74 7.2 16.12 6.5 15.37 6 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.5 5 12 5 C 11.5 5 11.05 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6 C 7.87 6.5 7.26 7.21 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.03 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.03 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 8.47 20.87 12.14 21.84 15 20.18 C 15.91 19.66 16.67 18.9 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.97 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.97 10.33 17.91 10 L 20 10 L 20 8 M 14.828 17.828 C 15.578 17.079 16 16.06 16 15 L 16 11 C 16 9.94 15.578 8.921 14.828 8.172 C 14.079 7.422 13.06 7 12 7 C 10.94 7 9.921 7.422 9.172 8.172 C 8.422 8.921 8 9.94 8 11 L 8 15 C 8 16.06 8.422 17.079 9.172 17.828 C 9.921 18.578 10.94 19 12 19 C 13.06 19 14.079 18.578 14.828 17.828 M 14 10 L 14 11 L 14 12 L 10 12 L 10 10 L 14 10 M 10 14 L 14 14 L 14 16 L 10 16 L 10 14 L 10 14"
|
||||
android:valueType="pathType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
28
app/apk-ng/src/main/res/drawable/avd_bug_to_filled.xml
Normal file
28
app/apk-ng/src/main/res/drawable/avd_bug_to_filled.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M 20 8 L 18.595 8 L 17.19 8 C 16.74 7.2 16.12 6.5 15.37 6 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.5 5 12 5 C 11.5 5 11.05 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6 C 7.87 6.5 7.26 7.21 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.03 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.03 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 8.47 20.87 12.14 21.84 15 20.18 C 15.91 19.66 16.67 18.9 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.97 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.97 10.33 17.91 10 L 20 10 L 20 8 M 14.828 17.828 C 15.578 17.079 16 16.06 16 15 L 16 11 C 16 9.94 15.578 8.921 14.828 8.172 C 14.079 7.422 13.06 7 12 7 C 10.94 7 9.921 7.422 9.172 8.172 C 8.422 8.921 8 9.94 8 11 L 8 15 C 8 16.06 8.422 17.079 9.172 17.828 C 9.921 18.578 10.94 19 12 19 C 13.06 19 14.079 18.578 14.828 17.828 M 14 10 L 14 11 L 14 12 L 10 12 L 10 10 L 14 10 M 10 14 L 14 14 L 14 16 L 10 16 L 10 14 L 10 14" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path_1">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 20 8 L 18.595 8 L 17.19 8 C 16.74 7.2 16.12 6.5 15.37 6 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.5 5 12 5 C 11.5 5 11.05 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6 C 7.87 6.5 7.26 7.21 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.03 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.03 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 8.47 20.87 12.14 21.84 15 20.18 C 15.91 19.66 16.67 18.9 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.97 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.97 10.33 17.91 10 L 20 10 L 20 8 M 14.828 17.828 C 15.578 17.079 16 16.06 16 15 L 16 11 C 16 9.94 15.578 8.921 14.828 8.172 C 14.079 7.422 13.06 7 12 7 C 10.94 7 9.921 7.422 9.172 8.172 C 8.422 8.921 8 9.94 8 11 L 8 15 C 8 16.06 8.422 17.079 9.172 17.828 C 9.921 18.578 10.94 19 12 19 C 13.06 19 14.079 18.578 14.828 17.828 M 14 10 L 14 11 L 14 12 L 10 12 L 10 10 L 14 10 M 10 14 L 14 14 L 14 16 L 10 16 L 10 14 L 10 14"
|
||||
android:valueTo="M 20 8 L 20 8 L 17.19 8 C 16.74 7.22 16.12 6.55 15.37 6.04 L 17 4.41 L 15.59 3 L 13.42 5.17 C 12.96 5.06 12.49 5 12 5 C 11.51 5 11.04 5.06 10.59 5.17 L 8.41 3 L 7 4.41 L 8.62 6.04 C 7.88 6.55 7.26 7.22 6.81 8 L 4 8 L 4 10 L 6.09 10 C 6.04 10.33 6 10.66 6 11 L 6 12 L 4 12 L 4 14 L 6 14 L 6 15 C 6 15.34 6.04 15.67 6.09 16 L 4 16 L 4 18 L 6.81 18 C 7.85 19.79 9.78 21 12 21 C 14.22 21 16.15 19.79 17.19 18 L 20 18 L 20 16 L 17.91 16 C 17.96 15.67 18 15.34 18 15 L 18 14 L 20 14 L 20 12 L 18 12 L 18 11 C 18 10.66 17.96 10.33 17.91 10 L 20 10 L 20 8 M 14 16 C 14 15.43 14 14.859 14 14.289 L 14 14 C 13.869 14 13.739 14 13.608 14 C 12.405 14 11.203 14 10 14 C 10 14.509 10 15.017 10 15.526 C 10 15.684 10 15.842 10 16 L 10.33 16 C 10.392 16 10.454 16 10.515 16 C 11.677 16 12.838 16 14 16 C 14 16 14 16 14 16 M 14 10 L 14 12 L 14 12 L 10 12 L 10 10 L 14 10 M 12 15 L 12 15 L 12 15 L 12 15 L 12 15 L 12 15"
|
||||
android:valueType="pathType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
@@ -0,0 +1,27 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path"
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M 12 2 C 6.5 2 2 6.5 2 12 C 2 17.5 6.5 22 12 22 C 17.5 22 22 17.5 22 12 C 22 6.5 17.5 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 16.59 7.58 L 10 14.17 L 7.41 11.59 L 6 13 L 10 17 L 18 9 L 16.59 7.58 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 12 2 C 9.217 2 6.689 3.152 4.872 5.004 C 3.098 6.811 2 9.283 2 12 C 2 14.744 3.12 17.24 4.927 19.052 C 6.74 20.87 9.244 22 12 22 C 13.911 22 15.701 21.457 17.224 20.517 C 18.628 19.651 19.804 18.448 20.638 17.024 C 21.503 15.545 22 13.828 22 12 C 22 10.2 21.518 8.507 20.677 7.044 C 19.755 5.441 18.402 4.114 16.779 3.224 C 15.357 2.444 13.728 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 6 13 L 10 17 L 18 9 L 16.59 7.58 L 16.59 7.58 L 10 14.17 L 7.41 11.59 L 6 13"
|
||||
android:valueTo="M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 13.755 22 15.48 21.538 17 20.66 C 18.52 19.783 19.783 18.52 20.66 17 C 21.538 15.48 22 13.755 22 12 C 22 10.245 21.538 8.52 20.66 7 C 19.783 5.48 18.52 4.217 17 3.34 C 15.48 2.462 13.755 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 7 13 L 7 13 L 17 13 L 17 11 L 17 11 L 7 11 L 7 11 L 7 11"
|
||||
android:valueType="pathType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
@@ -0,0 +1,27 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 13.755 22 15.48 21.538 17 20.66 C 18.52 19.783 19.783 18.52 20.66 17 C 21.538 15.48 22 13.755 22 12 C 22 10.245 21.538 8.52 20.66 7 C 19.783 5.48 18.52 4.217 17 3.34 C 15.48 2.462 13.755 2 12 2 M 7 13 L 17 13 L 17 11 L 7 11" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path_1">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 12 2 C 9.349 2 6.804 3.054 4.929 4.929 C 3.054 6.804 2 9.349 2 12 C 2 14.651 3.054 17.196 4.929 19.071 C 6.804 20.946 9.349 22 12 22 C 13.755 22 15.48 21.538 17 20.66 C 18.52 19.783 19.783 18.52 20.66 17 C 21.538 15.48 22 13.755 22 12 C 22 10.245 21.538 8.52 20.66 7 C 19.783 5.48 18.52 4.217 17 3.34 C 15.48 2.462 13.755 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 7 13 L 7 13 L 17 13 L 17 11 L 17 11 L 7 11 L 7 11 L 7 11"
|
||||
android:valueTo="M 12 2 C 9.217 2 6.689 3.152 4.872 5.004 C 3.098 6.811 2 9.283 2 12 C 2 14.856 3.213 17.442 5.149 19.268 C 6.942 20.96 9.356 22 12 22 C 14.061 22 15.982 21.368 17.578 20.288 C 19.114 19.249 20.349 17.796 21.119 16.092 C 21.685 14.841 22 13.456 22 12 C 22 10.122 21.475 8.361 20.566 6.856 C 19.691 5.408 18.46 4.197 16.997 3.347 C 15.524 2.491 13.817 2 12 2 M 12 20 C 7.59 20 4 16.41 4 12 C 4 7.59 7.59 4 12 4 C 16.41 4 20 7.59 20 12 C 20 16.41 16.41 20 12 20 M 6 13 L 10 17 L 18 9 L 16.59 7.58 L 16.59 7.58 L 10 14.17 L 7.41 11.59 L 6 13"
|
||||
android:valueType="pathType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
29
app/apk-ng/src/main/res/drawable/avd_home_from_filled.xml
Normal file
29
app/apk-ng/src/main/res/drawable/avd_home_from_filled.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path_3"
|
||||
android:fillColor="#000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M 12 3 L 20 9 L 20 21 L 15 21 L 15 14 L 9 14 L 9 21 L 4 21 L 4 9 L 12 3 Z"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path_3">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 9 14 L 9 21 L 4 21 L 4 9 L 12 3 L 12 3 L 20 9 L 20 21 L 15 21 L 15 14 L 9 14 M 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4"
|
||||
android:valueTo="M 9 13 L 9 19 L 6 19 L 6 10 L 12 5.5 L 15 7.75 L 18 10 L 18 19 L 15 19 L 15 13 L 9 13 M 4 21 L 4 9 L 12 3 L 20 9 L 20 21 L 4 21 L 4 21"
|
||||
android:valueType="pathType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
29
app/apk-ng/src/main/res/drawable/avd_home_to_filled.xml
Normal file
29
app/apk-ng/src/main/res/drawable/avd_home_to_filled.xml
Normal file
@@ -0,0 +1,29 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path_3"
|
||||
android:fillColor="#000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M 9 13 L 9 19 L 6 19 L 6 10 L 12 5.5 L 15 7.75 L 18 10 L 18 19 L 15 19 L 15 13 L 9 13 M 4 21 L 4 9 L 12 3 L 20 9 L 20 21 L 4 21 L 4 21"
|
||||
android:strokeWidth="1" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path_3">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 9 13 L 9 19 L 6 19 L 6 10 L 12 5.5 L 15 7.75 L 18 10 L 18 19 L 15 19 L 15 13 L 9 13 M 4 21 L 4 9 L 12 3 L 20 9 L 20 21 L 4 21 L 4 21"
|
||||
android:valueTo="M 9 14 L 9 21 L 4 21 L 4 9 L 12 3 L 12 3 L 20 9 L 20 21 L 15 21 L 15 14 L 9 14 M 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4"
|
||||
android:valueType="pathType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
28
app/apk-ng/src/main/res/drawable/avd_module_from_filled.xml
Normal file
28
app/apk-ng/src/main/res/drawable/avd_module_from_filled.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="outlined"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M 23 13.5 C 23 14.163 22.736 14.799 22.268 15.268 C 21.799 15.736 21.163 16 20.5 16 C 20 16 19.5 16 19 16 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 15.1 22 L 13.2 22 C 13.2 21.5 13.2 21 13.2 20.5 C 13.2 19 12 17.8 10.5 17.8 C 9 17.8 7.8 19 7.8 20.5 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 3.5 16.2 C 5 16.2 6.2 15 6.2 13.5 C 6.2 12 5 10.8 3.5 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 8 5 C 8 4.5 8 4 8 3.5 C 8 2.837 8.264 2.201 8.732 1.732 C 9.201 1.264 9.837 1 10.5 1 C 11.163 1 11.799 1.264 12.268 1.732 C 12.736 2.201 13 2.837 13 3.5 C 13 4 13 4.5 13 5 L 17 5 C 17.55 5 18.05 5.223 18.413 5.584 C 18.775 5.945 19 6.445 19 7 L 19 11 C 19.5 11 20 11 20.5 11 C 20.5 11 20.5 11 20.5 11 C 21.163 11 21.799 11.264 22.268 11.732 C 22.736 12.201 23 12.837 23 13.5 M 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="outlined">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 23 13.5 C 23 14.163 22.736 14.799 22.268 15.268 C 21.799 15.736 21.163 16 20.5 16 C 20 16 19.5 16 19 16 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 15.1 22 L 13.2 22 C 13.2 21.5 13.2 21 13.2 20.5 C 13.2 19 12 17.8 10.5 17.8 C 9 17.8 7.8 19 7.8 20.5 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 3.5 16.2 C 5 16.2 6.2 15 6.2 13.5 C 6.2 12 5 10.8 3.5 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 8 5 C 8 4.5 8 4 8 3.5 C 8 2.837 8.264 2.201 8.732 1.732 C 9.201 1.264 9.837 1 10.5 1 C 11.163 1 11.799 1.264 12.268 1.732 C 12.736 2.201 13 2.837 13 3.5 C 13 4 13 4.5 13 5 L 17 5 C 17.55 5 18.05 5.223 18.413 5.584 C 18.775 5.945 19 6.445 19 7 L 19 11 C 19.5 11 20 11 20.5 11 C 20.5 11 20.5 11 20.5 11 C 21.163 11 21.799 11.264 22.268 11.732 C 22.736 12.201 23 12.837 23 13.5 M 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12"
|
||||
android:valueTo="M 22 13.5 C 22 14.087 21.856 14.64 21.6 15.126 C 21.344 15.612 20.978 16.03 20.533 16.347 C 20.089 16.664 19.567 16.88 19 16.96 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 13.2 22 L 13.2 21.7 C 13.2 20.984 12.915 20.297 12.409 19.791 C 11.903 19.285 11.216 19 10.5 19 C 9 19 7.8 20.21 7.8 21.7 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 2.3 16.2 C 3.79 16.2 5 15 5 13.5 C 5 12 3.79 10.8 2.3 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 7.04 5 C 7.12 4.433 7.336 3.911 7.653 3.467 C 7.97 3.022 8.388 2.656 8.874 2.4 C 9.36 2.144 9.913 2 10.5 2 C 11.087 2 11.64 2.144 12.126 2.4 C 12.612 2.656 13.03 3.022 13.347 3.467 C 13.664 3.911 13.88 4.433 13.96 5 L 17 5 C 17.53 5 18.039 5.211 18.414 5.586 C 18.789 5.961 19 6.47 19 7 L 19 10.04 C 19.425 10.1 19.825 10.236 20.186 10.434 C 20.547 10.633 20.869 10.893 21.137 11.2 C 21.406 11.508 21.622 11.863 21.77 12.251 C 21.919 12.639 22 13.06 22 13.5 M 17 12 L 18.5 12 C 18.898 12 19.279 12.158 19.561 12.439 C 19.842 12.721 20 13.102 20 13.5 C 20 13.898 19.842 14.279 19.561 14.561 C 19.279 14.842 18.898 15 18.5 15 L 17 15 L 17 15 L 17 20 L 14.88 20 C 14.2 18.25 12.5 17 10.5 17 C 8.5 17 6.8 18.25 6.12 20 L 4 20 L 4 17.88 C 5.75 17.2 7 15.5 7 13.5 C 7 11.5 5.76 9.8 4 9.12 L 4 7 L 9 7 L 9 5.5 C 9 5.102 9.158 4.721 9.439 4.439 C 9.721 4.158 10.102 4 10.5 4 C 10.898 4 11.279 4.158 11.561 4.439 C 11.842 4.721 12 5.102 12 5.5 L 12 7 L 17 7 L 17 12"
|
||||
android:valueType="pathType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
28
app/apk-ng/src/main/res/drawable/avd_module_to_filled.xml
Normal file
28
app/apk-ng/src/main/res/drawable/avd_module_to_filled.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="outlined"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M 22 13.5 C 22 15.26 20.7 16.72 19 16.96 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 13.2 22 L 13.2 21.7 C 13.2 20.984 12.915 20.297 12.409 19.791 C 11.903 19.285 11.216 19 10.5 19 C 9 19 7.8 20.21 7.8 21.7 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 2.3 16.2 C 3.79 16.2 5 15 5 13.5 C 5 12 3.79 10.8 2.3 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 7.04 5 C 7.28 3.3 8.74 2 10.5 2 C 12.26 2 13.72 3.3 13.96 5 L 17 5 C 17.53 5 18.039 5.211 18.414 5.586 C 18.789 5.961 19 6.47 19 7 L 19 10.04 C 20.7 10.28 22 11.74 22 13.5 M 17 15 L 18.5 15 C 18.898 15 19.279 14.842 19.561 14.561 C 19.842 14.279 20 13.898 20 13.5 C 20 13.102 19.842 12.721 19.561 12.439 C 19.279 12.158 18.898 12 18.5 12 L 17 12 L 17 7 L 12 7 L 12 5.5 C 12 5.102 11.842 4.721 11.561 4.439 C 11.279 4.158 10.898 4 10.5 4 C 10.102 4 9.721 4.158 9.439 4.439 C 9.158 4.721 9 5.102 9 5.5 L 9 7 L 4 7 L 4 9.12 C 5.76 9.8 7 11.5 7 13.5 C 7 15.5 5.75 17.2 4 17.88 L 4 20 L 6.12 20 C 6.8 18.25 8.5 17 10.5 17 C 12.5 17 14.2 18.25 14.88 20 L 17 20 L 17 15 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="outlined">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 22 13.5 C 22 14.087 21.856 14.64 21.6 15.126 C 21.344 15.612 20.978 16.03 20.533 16.347 C 20.089 16.664 19.567 16.88 19 16.96 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 13.2 22 L 13.2 21.7 C 13.2 20.984 12.915 20.297 12.409 19.791 C 11.903 19.285 11.216 19 10.5 19 C 9 19 7.8 20.21 7.8 21.7 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 2.3 16.2 C 3.79 16.2 5 15 5 13.5 C 5 12 3.79 10.8 2.3 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 7.04 5 C 7.12 4.433 7.336 3.911 7.653 3.467 C 7.97 3.022 8.388 2.656 8.874 2.4 C 9.36 2.144 9.913 2 10.5 2 C 11.087 2 11.64 2.144 12.126 2.4 C 12.612 2.656 13.03 3.022 13.347 3.467 C 13.664 3.911 13.88 4.433 13.96 5 L 17 5 C 17.53 5 18.039 5.211 18.414 5.586 C 18.789 5.961 19 6.47 19 7 L 19 10.04 C 19.425 10.1 19.825 10.236 20.186 10.434 C 20.547 10.633 20.869 10.893 21.137 11.2 C 21.406 11.508 21.622 11.863 21.77 12.251 C 21.919 12.639 22 13.06 22 13.5 M 17 12 L 18.5 12 C 18.898 12 19.279 12.158 19.561 12.439 C 19.842 12.721 20 13.102 20 13.5 C 20 13.898 19.842 14.279 19.561 14.561 C 19.279 14.842 18.898 15 18.5 15 L 17 15 L 17 15 L 17 20 L 14.88 20 C 14.2 18.25 12.5 17 10.5 17 C 8.5 17 6.8 18.25 6.12 20 L 4 20 L 4 17.88 C 5.75 17.2 7 15.5 7 13.5 C 7 11.5 5.76 9.8 4 9.12 L 4 7 L 9 7 L 9 5.5 C 9 5.102 9.158 4.721 9.439 4.439 C 9.721 4.158 10.102 4 10.5 4 C 10.898 4 11.279 4.158 11.561 4.439 C 11.842 4.721 12 5.102 12 5.5 L 12 7 L 17 7 L 17 12"
|
||||
android:valueTo="M 23 13.5 C 23 14.163 22.736 14.799 22.268 15.268 C 21.799 15.736 21.163 16 20.5 16 C 20 16 19.5 16 19 16 L 19 20 C 19 20.53 18.789 21.039 18.414 21.414 C 18.039 21.789 17.53 22 17 22 L 15.1 22 L 13.2 22 C 13.2 21.5 13.2 21 13.2 20.5 C 13.2 19 12 17.8 10.5 17.8 C 9 17.8 7.8 19 7.8 20.5 L 7.8 22 L 4 22 C 3.47 22 2.961 21.789 2.586 21.414 C 2.211 21.039 2 20.53 2 20 L 2 16.2 L 3.5 16.2 C 5 16.2 6.2 15 6.2 13.5 C 6.2 12 5 10.8 3.5 10.8 L 2 10.8 L 2 7 C 2 6.47 2.211 5.961 2.586 5.586 C 2.961 5.211 3.47 5 4 5 L 8 5 C 8 4.5 8 4 8 3.5 C 8 2.837 8.264 2.201 8.732 1.732 C 9.201 1.264 9.837 1 10.5 1 C 11.163 1 11.799 1.264 12.268 1.732 C 12.736 2.201 13 2.837 13 3.5 C 13 4 13 4.5 13 5 L 17 5 C 17.55 5 18.05 5.223 18.413 5.584 C 18.775 5.945 19 6.445 19 7 L 19 11 C 19.5 11 20 11 20.5 11 C 20.5 11 20.5 11 20.5 11 C 21.163 11 21.799 11.264 22.268 11.732 C 22.736 12.201 23 12.837 23 13.5 M 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12"
|
||||
android:valueType="pathType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
@@ -0,0 +1,28 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M 12 15.5 C 11.072 15.5 10.181 15.131 9.525 14.475 C 8.869 13.819 8.5 12.928 8.5 12 C 8.5 11.072 8.869 10.181 9.525 9.525 C 10.181 8.869 11.072 8.5 12 8.5 C 12.614 8.5 13.218 8.662 13.75 8.969 C 14.282 9.276 14.724 9.718 15.031 10.25 C 15.338 10.782 15.5 11.386 15.5 12 C 15.5 12.614 15.338 13.218 15.031 13.75 C 14.724 14.282 14.282 14.724 13.75 15.031 C 13.218 15.338 12.614 15.5 12 15.5 M 19.43 12.97 C 19.47 12.65 19.5 12.33 19.5 12 C 19.5 11.67 19.47 11.34 19.43 11 L 21.54 9.37 C 21.73 9.22 21.78 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.53 11.34 4.5 11.67 4.5 12 C 4.5 12.33 4.53 12.65 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.67 16.04 18.34 16.56 17.94 L 19.05 18.95 C 19.27 19.03 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.78 15.05 21.73 14.78 21.54 14.63 L 19.43 12.97 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.53 11.34 4.5 11.67 4.5 12 C 4.5 12.33 4.53 12.65 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.67 16.04 18.34 16.56 17.94 L 19.05 18.95 C 19.27 19.03 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.78 15.05 21.73 14.78 21.54 14.63 L 19.43 12.97 L 19.43 12.97 C 19.47 12.65 19.5 12.33 19.5 12 C 19.5 11.67 19.47 11.34 19.43 11 L 21.54 9.37 C 21.73 9.22 21.78 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8.5 C 12.614 8.5 13.218 8.662 13.75 8.969 C 14.282 9.276 14.724 9.718 15.031 10.25 C 15.338 10.782 15.5 11.386 15.5 12 C 15.5 12.614 15.338 13.218 15.031 13.75 C 14.724 14.282 14.282 14.724 13.75 15.031 C 13.218 15.338 12.614 15.5 12 15.5 C 11.072 15.5 10.181 15.131 9.525 14.475 C 8.869 13.819 8.5 12.928 8.5 12 C 8.5 11.072 8.869 10.181 9.525 9.525 C 10.181 8.869 11.072 8.5 12 8.5 M 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 M 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12"
|
||||
android:valueTo="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.547 11.333 4.523 11.667 4.5 12 C 4.523 12.323 4.547 12.647 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.68 16.04 18.34 16.56 17.95 L 19.05 18.95 C 19.27 19.04 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.79 15.05 21.73 14.78 21.54 14.63 L 19.43 13 L 19.465 12.499 C 19.477 12.333 19.488 12.166 19.5 12 C 19.477 11.667 19.453 11.333 19.43 11 L 21.54 9.37 C 21.73 9.22 21.79 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8 C 12.53 8 13.05 8.105 13.531 8.305 C 14.011 8.504 14.454 8.797 14.828 9.172 C 15.578 9.921 16 10.94 16 12 C 16 12.53 15.895 13.05 15.695 13.531 C 15.496 14.011 15.203 14.454 14.828 14.828 C 14.079 15.578 13.06 16 12 16 C 10.94 16 9.921 15.578 9.172 14.828 C 8.422 14.079 8 13.06 8 12 C 8 10.94 8.422 9.921 9.172 9.172 C 9.921 8.422 10.94 8 12 8 M 12 10 C 11.912 10 11.824 10.006 11.737 10.017 C 11.651 10.029 11.565 10.046 11.481 10.069 C 11.397 10.091 11.315 10.119 11.235 10.152 C 11.155 10.186 11.077 10.224 11.001 10.267 C 10.926 10.311 10.854 10.359 10.784 10.412 C 10.715 10.466 10.649 10.524 10.586 10.586 C 10.524 10.649 10.466 10.715 10.412 10.784 C 10.359 10.854 10.311 10.926 10.267 11.001 C 10.224 11.077 10.186 11.155 10.152 11.235 C 10.119 11.315 10.091 11.397 10.069 11.481 C 10.046 11.565 10.029 11.651 10.017 11.737 C 10.006 11.824 10 11.912 10 12 C 10 12.088 10.006 12.176 10.017 12.263 C 10.029 12.349 10.046 12.435 10.069 12.519 C 10.091 12.603 10.119 12.685 10.152 12.765 C 10.186 12.845 10.224 12.923 10.267 12.999 C 10.311 13.074 10.359 13.146 10.412 13.216 C 10.466 13.285 10.524 13.351 10.586 13.414 C 10.649 13.476 10.715 13.534 10.784 13.588 C 10.854 13.641 10.926 13.689 11.001 13.733 C 11.077 13.776 11.155 13.814 11.235 13.848 C 11.315 13.881 11.397 13.909 11.481 13.931 C 11.565 13.954 11.651 13.971 11.737 13.983 C 11.824 13.994 11.912 14 12 14 C 12.53 14 13.039 13.789 13.414 13.414 C 13.468 13.36 13.518 13.304 13.565 13.245 C 13.611 13.187 13.655 13.126 13.694 13.062 C 13.734 12.999 13.77 12.934 13.802 12.867 C 13.834 12.8 13.863 12.731 13.887 12.661 C 13.912 12.591 13.933 12.519 13.949 12.447 C 13.966 12.374 13.979 12.3 13.987 12.226 C 13.996 12.151 14 12.076 14 12 C 14 11.912 13.994 11.824 13.983 11.737 C 13.971 11.651 13.954 11.565 13.931 11.481 C 13.909 11.397 13.881 11.315 13.848 11.235 C 13.814 11.155 13.776 11.077 13.733 11.001 C 13.689 10.926 13.641 10.854 13.588 10.784 C 13.534 10.715 13.476 10.649 13.414 10.586 C 13.039 10.211 12.53 10 12 10 M 11.25 4 L 11.25 4 L 12.75 4 L 13.12 6.62 C 14.32 6.86 15.38 7.5 16.15 8.39 L 18.56 7.35 L 19.31 8.65 L 17.2 10.2 C 17.6 11.37 17.6 12.64 17.2 13.81 L 19.32 15.36 L 18.57 16.66 L 16.14 15.62 C 15.37 16.5 14.32 17.14 13.13 17.39 L 12.76 20 L 11.24 20 L 10.87 17.38 C 9.68 17.14 8.63 16.5 7.86 15.62 L 5.43 16.66 L 4.68 15.36 L 6.8 13.8 C 6.4 12.64 6.4 11.37 6.8 10.2 L 4.69 8.65 L 5.44 7.35 L 7.85 8.39 C 8.62 7.5 9.68 6.86 10.88 6.61 L 11.25 4"
|
||||
android:valueType="pathType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
28
app/apk-ng/src/main/res/drawable/avd_settings_to_filled.xml
Normal file
28
app/apk-ng/src/main/res/drawable/avd_settings_to_filled.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.547 11.333 4.523 11.667 4.5 12 C 4.523 12.323 4.547 12.647 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.68 16.04 18.34 16.56 17.95 L 19.05 18.95 C 19.27 19.04 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.79 15.05 21.73 14.78 21.54 14.63 L 19.43 13 L 19.465 12.499 C 19.477 12.333 19.488 12.166 19.5 12 C 19.477 11.667 19.453 11.333 19.43 11 L 21.54 9.37 C 21.73 9.22 21.79 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8 C 12.53 8 13.05 8.105 13.531 8.305 C 14.011 8.504 14.454 8.797 14.828 9.172 C 15.578 9.921 16 10.94 16 12 C 16 12.53 15.895 13.05 15.695 13.531 C 15.496 14.011 15.203 14.454 14.828 14.828 C 14.079 15.578 13.06 16 12 16 C 10.94 16 9.921 15.578 9.172 14.828 C 8.422 14.079 8 13.06 8 12 C 8 10.94 8.422 9.921 9.172 9.172 C 9.921 8.422 10.94 8 12 8 M 12 10 C 11.912 10 11.824 10.006 11.737 10.017 C 11.651 10.029 11.565 10.046 11.481 10.069 C 11.397 10.091 11.315 10.119 11.235 10.152 C 11.155 10.186 11.077 10.224 11.001 10.267 C 10.926 10.311 10.854 10.359 10.784 10.412 C 10.715 10.466 10.649 10.524 10.586 10.586 C 10.524 10.649 10.466 10.715 10.412 10.784 C 10.359 10.854 10.311 10.926 10.267 11.001 C 10.224 11.077 10.186 11.155 10.152 11.235 C 10.119 11.315 10.091 11.397 10.069 11.481 C 10.046 11.565 10.029 11.651 10.017 11.737 C 10.006 11.824 10 11.912 10 12 C 10 12.088 10.006 12.176 10.017 12.263 C 10.029 12.349 10.046 12.435 10.069 12.519 C 10.091 12.603 10.119 12.685 10.152 12.765 C 10.186 12.845 10.224 12.923 10.267 12.999 C 10.311 13.074 10.359 13.146 10.412 13.216 C 10.466 13.285 10.524 13.351 10.586 13.414 C 10.649 13.476 10.715 13.534 10.784 13.588 C 10.854 13.641 10.926 13.689 11.001 13.733 C 11.077 13.776 11.155 13.814 11.235 13.848 C 11.315 13.881 11.397 13.909 11.481 13.931 C 11.565 13.954 11.651 13.971 11.737 13.983 C 11.824 13.994 11.912 14 12 14 C 12.53 14 13.039 13.789 13.414 13.414 C 13.468 13.36 13.518 13.304 13.565 13.245 C 13.611 13.187 13.655 13.126 13.694 13.062 C 13.734 12.999 13.77 12.934 13.802 12.867 C 13.834 12.8 13.863 12.731 13.887 12.661 C 13.912 12.591 13.933 12.519 13.949 12.447 C 13.966 12.374 13.979 12.3 13.987 12.226 C 13.996 12.151 14 12.076 14 12 C 14 11.912 13.994 11.824 13.983 11.737 C 13.971 11.651 13.954 11.565 13.931 11.481 C 13.909 11.397 13.881 11.315 13.848 11.235 C 13.814 11.155 13.776 11.077 13.733 11.001 C 13.689 10.926 13.641 10.854 13.588 10.784 C 13.534 10.715 13.476 10.649 13.414 10.586 C 13.039 10.211 12.53 10 12 10 M 11.25 4 L 11.25 4 L 12.75 4 L 13.12 6.62 C 14.32 6.86 15.38 7.5 16.15 8.39 L 18.56 7.35 L 19.31 8.65 L 17.2 10.2 C 17.6 11.37 17.6 12.64 17.2 13.81 L 19.32 15.36 L 18.57 16.66 L 16.14 15.62 C 15.37 16.5 14.32 17.14 13.13 17.39 L 12.76 20 L 11.24 20 L 10.87 17.38 C 9.68 17.14 8.63 16.5 7.86 15.62 L 5.43 16.66 L 4.68 15.36 L 6.8 13.8 C 6.4 12.64 6.4 11.37 6.8 10.2 L 4.69 8.65 L 5.44 7.35 L 7.85 8.39 C 8.62 7.5 9.68 6.86 10.88 6.61 L 11.25 4" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.547 11.333 4.523 11.667 4.5 12 C 4.523 12.323 4.547 12.647 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.68 16.04 18.34 16.56 17.95 L 19.05 18.95 C 19.27 19.04 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.79 15.05 21.73 14.78 21.54 14.63 L 19.43 13 L 19.465 12.499 C 19.477 12.333 19.488 12.166 19.5 12 C 19.477 11.667 19.453 11.333 19.43 11 L 21.54 9.37 C 21.73 9.22 21.79 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8 C 12.53 8 13.05 8.105 13.531 8.305 C 14.011 8.504 14.454 8.797 14.828 9.172 C 15.578 9.921 16 10.94 16 12 C 16 12.53 15.895 13.05 15.695 13.531 C 15.496 14.011 15.203 14.454 14.828 14.828 C 14.079 15.578 13.06 16 12 16 C 10.94 16 9.921 15.578 9.172 14.828 C 8.422 14.079 8 13.06 8 12 C 8 10.94 8.422 9.921 9.172 9.172 C 9.921 8.422 10.94 8 12 8 M 12 10 C 11.912 10 11.824 10.006 11.737 10.017 C 11.651 10.029 11.565 10.046 11.481 10.069 C 11.397 10.091 11.315 10.119 11.235 10.152 C 11.155 10.186 11.077 10.224 11.001 10.267 C 10.926 10.311 10.854 10.359 10.784 10.412 C 10.715 10.466 10.649 10.524 10.586 10.586 C 10.524 10.649 10.466 10.715 10.412 10.784 C 10.359 10.854 10.311 10.926 10.267 11.001 C 10.224 11.077 10.186 11.155 10.152 11.235 C 10.119 11.315 10.091 11.397 10.069 11.481 C 10.046 11.565 10.029 11.651 10.017 11.737 C 10.006 11.824 10 11.912 10 12 C 10 12.088 10.006 12.176 10.017 12.263 C 10.029 12.349 10.046 12.435 10.069 12.519 C 10.091 12.603 10.119 12.685 10.152 12.765 C 10.186 12.845 10.224 12.923 10.267 12.999 C 10.311 13.074 10.359 13.146 10.412 13.216 C 10.466 13.285 10.524 13.351 10.586 13.414 C 10.649 13.476 10.715 13.534 10.784 13.588 C 10.854 13.641 10.926 13.689 11.001 13.733 C 11.077 13.776 11.155 13.814 11.235 13.848 C 11.315 13.881 11.397 13.909 11.481 13.931 C 11.565 13.954 11.651 13.971 11.737 13.983 C 11.824 13.994 11.912 14 12 14 C 12.53 14 13.039 13.789 13.414 13.414 C 13.468 13.36 13.518 13.304 13.565 13.245 C 13.611 13.187 13.655 13.126 13.694 13.062 C 13.734 12.999 13.77 12.934 13.802 12.867 C 13.834 12.8 13.863 12.731 13.887 12.661 C 13.912 12.591 13.933 12.519 13.949 12.447 C 13.966 12.374 13.979 12.3 13.987 12.226 C 13.996 12.151 14 12.076 14 12 C 14 11.912 13.994 11.824 13.983 11.737 C 13.971 11.651 13.954 11.565 13.931 11.481 C 13.909 11.397 13.881 11.315 13.848 11.235 C 13.814 11.155 13.776 11.077 13.733 11.001 C 13.689 10.926 13.641 10.854 13.588 10.784 C 13.534 10.715 13.476 10.649 13.414 10.586 C 13.039 10.211 12.53 10 12 10 M 11.25 4 L 11.25 4 L 12.75 4 L 13.12 6.62 C 14.32 6.86 15.38 7.5 16.15 8.39 L 18.56 7.35 L 19.31 8.65 L 17.2 10.2 C 17.6 11.37 17.6 12.64 17.2 13.81 L 19.32 15.36 L 18.57 16.66 L 16.14 15.62 C 15.37 16.5 14.32 17.14 13.13 17.39 L 12.76 20 L 11.24 20 L 10.87 17.38 C 9.68 17.14 8.63 16.5 7.86 15.62 L 5.43 16.66 L 4.68 15.36 L 6.8 13.8 C 6.4 12.64 6.4 11.37 6.8 10.2 L 4.69 8.65 L 5.44 7.35 L 7.85 8.39 C 8.62 7.5 9.68 6.86 10.88 6.61 L 11.25 4"
|
||||
android:valueTo="M 14.87 5.07 L 14.5 2.42 C 14.46 2.18 14.25 2 14 2 L 10 2 C 9.75 2 9.54 2.18 9.5 2.42 L 9.13 5.07 C 8.5 5.32 7.96 5.66 7.44 6.05 L 4.95 5.05 C 4.73 4.96 4.46 5.05 4.34 5.27 L 2.34 8.73 C 2.21 8.95 2.27 9.22 2.46 9.37 L 4.57 11 C 4.53 11.34 4.5 11.67 4.5 12 C 4.5 12.33 4.53 12.65 4.57 12.97 L 2.46 14.63 C 2.27 14.78 2.21 15.05 2.34 15.27 L 4.34 18.73 C 4.46 18.95 4.73 19.03 4.95 18.95 L 7.44 17.94 C 7.96 18.34 8.5 18.68 9.13 18.93 L 9.5 21.58 C 9.54 21.82 9.75 22 10 22 L 14 22 C 14.25 22 14.46 21.82 14.5 21.58 L 14.87 18.93 C 15.5 18.67 16.04 18.34 16.56 17.94 L 19.05 18.95 C 19.27 19.03 19.54 18.95 19.66 18.73 L 21.66 15.27 C 21.78 15.05 21.73 14.78 21.54 14.63 L 19.43 12.97 L 19.43 12.97 C 19.47 12.65 19.5 12.33 19.5 12 C 19.5 11.67 19.47 11.34 19.43 11 L 21.54 9.37 C 21.73 9.22 21.78 8.95 21.66 8.73 L 19.66 5.27 C 19.54 5.05 19.27 4.96 19.05 5.05 L 16.56 6.05 C 16.04 5.66 15.5 5.32 14.87 5.07 M 12 8.5 C 12.614 8.5 13.218 8.662 13.75 8.969 C 14.282 9.276 14.724 9.718 15.031 10.25 C 15.338 10.782 15.5 11.386 15.5 12 C 15.5 12.614 15.338 13.218 15.031 13.75 C 14.724 14.282 14.282 14.724 13.75 15.031 C 13.218 15.338 12.614 15.5 12 15.5 C 11.072 15.5 10.181 15.131 9.525 14.475 C 8.869 13.819 8.5 12.928 8.5 12 C 8.5 11.072 8.869 10.181 9.525 9.525 C 10.181 8.869 11.072 8.5 12 8.5 M 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 C 11.982 12 11.982 12 11.982 12 M 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12 L 12 12 L 12 12 C 12 12 12 12 12 12 L 12 12"
|
||||
android:valueType="pathType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
@@ -0,0 +1,28 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M 12 1 L 3 5 L 3 11 C 3 16.55 6.84 21.74 12 23 C 17.16 21.74 21 16.55 21 11 L 21 5 L 12 1 Z" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 12 1 L 12 1 L 21 5 L 21 11 M 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18"
|
||||
android:valueTo="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 7.5 3 L 12 1 L 21 5 L 21 11 M 12 21 L 12 21 C 8.25 20 5 15.54 5 11.22 L 5 6.3 L 12 3.18 L 19 6.3 L 19 11.22 C 19 15.54 15.75 20 12 21"
|
||||
android:valueType="pathType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
28
app/apk-ng/src/main/res/drawable/avd_superuser_to_filled.xml
Normal file
28
app/apk-ng/src/main/res/drawable/avd_superuser_to_filled.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 7.5 3 L 12 1 L 21 5 L 21 11 M 12 21 L 12 21 C 8.25 20 5 15.54 5 11.22 L 5 6.3 L 12 3.18 L 19 6.3 L 19 11.22 C 19 15.54 15.75 20 12 21" />
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="pathData"
|
||||
android:valueFrom="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 7.5 3 L 12 1 L 21 5 L 21 11 M 12 21 L 12 21 C 8.25 20 5 15.54 5 11.22 L 5 6.3 L 12 3.18 L 19 6.3 L 19 11.22 C 19 15.54 15.75 20 12 21"
|
||||
android:valueTo="M 21 11 C 21 16.55 17.16 21.74 12 23 C 6.84 21.74 3 16.55 3 11 L 3 5 L 12 1 L 12 1 L 21 5 L 21 11 M 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 L 12 10.18 C 12 10.18 12 10.18 12 10.18"
|
||||
android:valueType="pathType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
10
app/apk-ng/src/main/res/drawable/ic_bug_filled_md2.xml
Normal file
10
app/apk-ng/src/main/res/drawable/ic_bug_filled_md2.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
|
||||
</vector>
|
||||
23
app/apk-ng/src/main/res/drawable/ic_bug_md2.xml
Normal file
23
app/apk-ng/src/main/res/drawable/ic_bug_md2.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/checked"
|
||||
android:drawable="@drawable/ic_bug_filled_md2"
|
||||
android:state_checked="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/unchecked"
|
||||
android:drawable="@drawable/ic_bug_outlined_md2" />
|
||||
|
||||
<transition
|
||||
android:drawable="@drawable/avd_bug_from_filled"
|
||||
android:fromId="@+id/checked"
|
||||
android:toId="@+id/unchecked" />
|
||||
|
||||
<transition
|
||||
android:drawable="@drawable/avd_bug_to_filled"
|
||||
android:fromId="@+id/unchecked"
|
||||
android:toId="@id/checked" />
|
||||
|
||||
</animated-selector>
|
||||
10
app/apk-ng/src/main/res/drawable/ic_bug_outlined_md2.xml
Normal file
10
app/apk-ng/src/main/res/drawable/ic_bug_outlined_md2.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M20,8H17.19C16.74,7.2 16.12,6.5 15.37,6L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.05,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6C7.87,6.5 7.26,7.21 6.81,8H4V10H6.09C6.03,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.03,15.67 6.09,16H4V18H6.81C8.47,20.87 12.14,21.84 15,20.18C15.91,19.66 16.67,18.9 17.19,18H20V16H17.91C17.97,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.97,10.33 17.91,10H20V8M16,15A4,4 0 0,1 12,19A4,4 0 0,1 8,15V11A4,4 0 0,1 12,7A4,4 0 0,1 16,11V15M14,10V12H10V10H14M10,14H14V16H10V14Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z" />
|
||||
</vector>
|
||||
25
app/apk-ng/src/main/res/drawable/ic_check_circle_md2.xml
Normal file
25
app/apk-ng/src/main/res/drawable/ic_check_circle_md2.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="NewApi">
|
||||
|
||||
<item
|
||||
android:id="@+id/checked"
|
||||
android:drawable="@drawable/ic_check_circle_checked_md2"
|
||||
android:state_selected="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/unchecked"
|
||||
android:drawable="@drawable/ic_check_circle_unchecked_md2" />
|
||||
|
||||
<transition
|
||||
android:drawable="@drawable/avd_circle_check_from_filled"
|
||||
android:fromId="@+id/checked"
|
||||
android:toId="@+id/unchecked" />
|
||||
|
||||
<transition
|
||||
android:drawable="@drawable/avd_circle_check_to_filled"
|
||||
android:fromId="@+id/unchecked"
|
||||
android:toId="@id/checked" />
|
||||
|
||||
</animated-selector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M7,13H17V11H7" />
|
||||
</vector>
|
||||
12
app/apk-ng/src/main/res/drawable/ic_check_md2.xml
Normal file
12
app/apk-ng/src/main/res/drawable/ic_check_md2.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"
|
||||
tools:fillColor="#000" />
|
||||
</vector>
|
||||
12
app/apk-ng/src/main/res/drawable/ic_delete_md2.xml
Normal file
12
app/apk-ng/src/main/res/drawable/ic_delete_md2.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M18,4h-2.5l-0.7,-0.7C14.6,3.2 14.4,3 14.1,3H9.9C9.6,3 9.4,3.2 9.2,3.3L8.5,4H6C5.4,4 5,4.5 5,5s0.4,1 1,1h12c0.5,0 1,-0.4 1,-1S18.5,4 18,4z" />
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V9c0,-1.1 -0.9,-2 -2,-2H8C6.9,7 6,7.9 6,9V19z" />
|
||||
</vector>
|
||||
12
app/apk-ng/src/main/res/drawable/ic_download_md2.xml
Normal file
12
app/apk-ng/src/main/res/drawable/ic_download_md2.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z"
|
||||
tools:fillColor="#000" />
|
||||
</vector>
|
||||
9
app/apk-ng/src/main/res/drawable/ic_home_filled_md2.xml
Normal file
9
app/apk-ng/src/main/res/drawable/ic_home_filled_md2.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M 9 14 L 9 21 L 4 21 L 4 9 L 12 3 L 12 3 L 20 9 L 20 21 L 15 21 L 15 14 L 9 14 M 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4 L 12 13.4" />
|
||||
</vector>
|
||||
23
app/apk-ng/src/main/res/drawable/ic_home_md2.xml
Normal file
23
app/apk-ng/src/main/res/drawable/ic_home_md2.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/checked"
|
||||
android:drawable="@drawable/ic_home_filled_md2"
|
||||
android:state_checked="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/unchecked"
|
||||
android:drawable="@drawable/ic_home_outlined_md2" />
|
||||
|
||||
<transition
|
||||
android:drawable="@drawable/avd_home_from_filled"
|
||||
android:fromId="@+id/checked"
|
||||
android:toId="@+id/unchecked" />
|
||||
|
||||
<transition
|
||||
android:drawable="@drawable/avd_home_to_filled"
|
||||
android:fromId="@+id/unchecked"
|
||||
android:toId="@id/checked" />
|
||||
|
||||
</animated-selector>
|
||||
11
app/apk-ng/src/main/res/drawable/ic_home_outlined_md2.xml
Normal file
11
app/apk-ng/src/main/res/drawable/ic_home_outlined_md2.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M 9 13 L 9 19 L 6 19 L 6 10 L 12 5.5 L 15 7.75 L 18 10 L 18 19 L 15 19 L 15 13 L 9 13 M 4 21 L 4 9 L 12 3 L 20 9 L 20 21 L 4 21 L 4 21" />
|
||||
</vector>
|
||||
10
app/apk-ng/src/main/res/drawable/ic_install.xml
Normal file
10
app/apk-ng/src/main/res/drawable/ic_install.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M12,18L7,13H10V9H14V13H17L12,18M10,2H14A2,2 0 0,1 16,4V6H20A2,2 0 0,1 22,8V19A2,2 0 0,1 20,21H4C2.89,21 2,20.1 2,19V8C2,6.89 2.89,6 4,6H8V4C8,2.89 8.89,2 10,2M14,6V4H10V6H14M4,8V19H20V8H4Z" />
|
||||
</vector>
|
||||
9
app/apk-ng/src/main/res/drawable/ic_manager.xml
Normal file
9
app/apk-ng/src/main/res/drawable/ic_manager.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M7,3v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1L9,4h10v16L9,20v-1c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v2c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L21,3c0,-1.1 -0.9,-2 -2,-2L9,1c-1.1,0 -2,0.9 -2,2zM9.5,15.5c0.29,-0.12 0.55,-0.29 0.8,-0.48l-0.02,0.03 1.01,0.39c0.23,0.09 0.49,0 0.61,-0.22l0.84,-1.46c0.12,-0.21 0.07,-0.49 -0.12,-0.64l-0.85,-0.68 -0.02,0.03c0.02,-0.16 0.05,-0.32 0.05,-0.48s-0.03,-0.32 -0.05,-0.48l0.02,0.03 0.85,-0.68c0.19,-0.15 0.24,-0.43 0.12,-0.64l-0.84,-1.46c-0.12,-0.21 -0.38,-0.31 -0.61,-0.22l-1.01,0.39 0.02,0.03c-0.25,-0.17 -0.51,-0.34 -0.8,-0.46l-0.17,-1.08C9.3,7.18 9.09,7 8.84,7L7.16,7c-0.25,0 -0.46,0.18 -0.49,0.42L6.5,8.5c-0.29,0.12 -0.55,0.29 -0.8,0.48l0.02,-0.03 -1.02,-0.39c-0.23,-0.09 -0.49,0 -0.61,0.22l-0.84,1.46c-0.12,0.21 -0.07,0.49 0.12,0.64l0.85,0.68 0.02,-0.03c-0.02,0.15 -0.05,0.31 -0.05,0.47s0.03,0.32 0.05,0.48l-0.02,-0.03 -0.85,0.68c-0.19,0.15 -0.24,0.43 -0.12,0.64l0.84,1.46c0.12,0.21 0.38,0.31 0.61,0.22l1.01,-0.39 -0.01,-0.04c0.25,0.19 0.51,0.36 0.8,0.48l0.17,1.07c0.03,0.25 0.24,0.43 0.49,0.43h1.68c0.25,0 0.46,-0.18 0.49,-0.42l0.17,-1.08zM6,12c0,-1.1 0.9,-2 2,-2s2,0.9 2,2 -0.9,2 -2,2 -2,-0.9 -2,-2z" />
|
||||
</vector>
|
||||
10
app/apk-ng/src/main/res/drawable/ic_module_filled_md2.xml
Normal file
10
app/apk-ng/src/main/res/drawable/ic_module_filled_md2.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M20.5,11H19V7C19,5.89 18.1,5 17,5H13V3.5A2.5,2.5 0 0,0 10.5,1A2.5,2.5 0 0,0 8,3.5V5H4A2,2 0 0,0 2,7V10.8H3.5C5,10.8 6.2,12 6.2,13.5C6.2,15 5,16.2 3.5,16.2H2V20A2,2 0 0,0 4,22H7.8V20.5C7.8,19 9,17.8 10.5,17.8C12,17.8 13.2,19 13.2,20.5V22H17A2,2 0 0,0 19,20V16H20.5A2.5,2.5 0 0,0 23,13.5A2.5,2.5 0 0,0 20.5,11Z" />
|
||||
</vector>
|
||||
23
app/apk-ng/src/main/res/drawable/ic_module_md2.xml
Normal file
23
app/apk-ng/src/main/res/drawable/ic_module_md2.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/checked"
|
||||
android:drawable="@drawable/ic_module_filled_md2"
|
||||
android:state_checked="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/unchecked"
|
||||
android:drawable="@drawable/ic_module_outlined_md2" />
|
||||
|
||||
<transition
|
||||
android:drawable="@drawable/avd_module_from_filled"
|
||||
android:fromId="@+id/checked"
|
||||
android:toId="@+id/unchecked" />
|
||||
|
||||
<transition
|
||||
android:drawable="@drawable/avd_module_to_filled"
|
||||
android:fromId="@+id/unchecked"
|
||||
android:toId="@id/checked" />
|
||||
|
||||
</animated-selector>
|
||||
10
app/apk-ng/src/main/res/drawable/ic_module_outlined_md2.xml
Normal file
10
app/apk-ng/src/main/res/drawable/ic_module_outlined_md2.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M22,13.5C22,15.26 20.7,16.72 19,16.96V20A2,2 0 0,1 17,22H13.2V21.7A2.7,2.7 0 0,0 10.5,19C9,19 7.8,20.21 7.8,21.7V22H4A2,2 0 0,1 2,20V16.2H2.3C3.79,16.2 5,15 5,13.5C5,12 3.79,10.8 2.3,10.8H2V7A2,2 0 0,1 4,5H7.04C7.28,3.3 8.74,2 10.5,2C12.26,2 13.72,3.3 13.96,5H17A2,2 0 0,1 19,7V10.04C20.7,10.28 22,11.74 22,13.5M17,15H18.5A1.5,1.5 0 0,0 20,13.5A1.5,1.5 0 0,0 18.5,12H17V7H12V5.5A1.5,1.5 0 0,0 10.5,4A1.5,1.5 0 0,0 9,5.5V7H4V9.12C5.76,9.8 7,11.5 7,13.5C7,15.5 5.75,17.2 4,17.88V20H6.12C6.8,18.25 8.5,17 10.5,17C12.5,17 14.2,18.25 14.88,20H17V15Z" />
|
||||
</vector>
|
||||
10
app/apk-ng/src/main/res/drawable/ic_module_storage_md2.xml
Normal file
10
app/apk-ng/src/main/res/drawable/ic_module_storage_md2.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M2,10.96C1.5,10.68 1.35,10.07 1.63,9.59L3.13,7C3.24,6.8 3.41,6.66 3.6,6.58L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.66,6.72 20.82,6.88 20.91,7.08L22.36,9.6C22.64,10.08 22.47,10.69 22,10.96L21,11.54V16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V10.96C2.7,11.13 2.32,11.14 2,10.96M12,4.15V4.15L12,10.85V10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V12.69L14,15.59C13.67,15.77 13.3,15.76 13,15.6V19.29L19,15.91M13.85,13.36L20.13,9.73L19.55,8.72L13.27,12.35L13.85,13.36Z" />
|
||||
</vector>
|
||||
10
app/apk-ng/src/main/res/drawable/ic_notifications_md2.xml
Normal file
10
app/apk-ng/src/main/res/drawable/ic_notifications_md2.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M16,17H7V10.5C7,8 9,6 11.5,6C14,6 16,8 16,10.5M18,16V10.5C18,7.43 15.86,4.86 13,4.18V3.5A1.5,1.5 0 0,0 11.5,2A1.5,1.5 0 0,0 10,3.5V4.18C7.13,4.86 5,7.43 5,10.5V16L3,18V19H20V18M11.5,22A2,2 0 0,0 13.5,20H9.5A2,2 0 0,0 11.5,22Z" />
|
||||
</vector>
|
||||
10
app/apk-ng/src/main/res/drawable/ic_restart.xml
Normal file
10
app/apk-ng/src/main/res/drawable/ic_restart.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M12,4C14.1,4 16.1,4.8 17.6,6.3C20.7,9.4 20.7,14.5 17.6,17.6C15.8,19.5 13.3,20.2 10.9,19.9L11.4,17.9C13.1,18.1 14.9,17.5 16.2,16.2C18.5,13.9 18.5,10.1 16.2,7.7C15.1,6.6 13.5,6 12,6V10.6L7,5.6L12,0.6V4M6.3,17.6C3.7,15 3.3,11 5.1,7.9L6.6,9.4C5.5,11.6 5.9,14.4 7.8,16.2C8.3,16.7 8.9,17.1 9.6,17.4L9,19.4C8,19 7.1,18.4 6.3,17.6Z" />
|
||||
</vector>
|
||||
10
app/apk-ng/src/main/res/drawable/ic_save_md2.xml
Normal file
10
app/apk-ng/src/main/res/drawable/ic_save_md2.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M17 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V7L17 3M19 19H5V5H16.17L19 7.83V19M12 12C10.34 12 9 13.34 9 15S10.34 18 12 18 15 16.66 15 15 13.66 12 12 12M6 6H15V10H6V6Z" />
|
||||
</vector>
|
||||
9
app/apk-ng/src/main/res/drawable/ic_search_md2.xml
Normal file
9
app/apk-ng/src/main/res/drawable/ic_search_md2.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M15.5,14h-0.79l-0.28,-0.27c1.2,-1.4 1.82,-3.31 1.48,-5.34 -0.47,-2.78 -2.79,-5 -5.59,-5.34 -4.23,-0.52 -7.79,3.04 -7.27,7.27 0.34,2.8 2.56,5.12 5.34,5.59 2.03,0.34 3.94,-0.28 5.34,-1.48l0.27,0.28v0.79l4.25,4.25c0.41,0.41 1.08,0.41 1.49,0 0.41,-0.41 0.41,-1.08 0,-1.49L15.5,14zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
|
||||
</vector>
|
||||
10
app/apk-ng/src/main/res/drawable/ic_settings_filled_md2.xml
Normal file
10
app/apk-ng/src/main/res/drawable/ic_settings_filled_md2.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" />
|
||||
</vector>
|
||||
23
app/apk-ng/src/main/res/drawable/ic_settings_md2.xml
Normal file
23
app/apk-ng/src/main/res/drawable/ic_settings_md2.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/checked"
|
||||
android:drawable="@drawable/ic_settings_filled_md2"
|
||||
android:state_checked="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/unchecked"
|
||||
android:drawable="@drawable/ic_settings_outlined_md2" />
|
||||
|
||||
<transition
|
||||
android:drawable="@drawable/avd_settings_from_filled"
|
||||
android:fromId="@+id/checked"
|
||||
android:toId="@+id/unchecked" />
|
||||
|
||||
<transition
|
||||
android:drawable="@drawable/avd_settings_to_filled"
|
||||
android:fromId="@+id/unchecked"
|
||||
android:toId="@id/checked" />
|
||||
|
||||
</animated-selector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1Z" />
|
||||
</vector>
|
||||
23
app/apk-ng/src/main/res/drawable/ic_superuser_md2.xml
Normal file
23
app/apk-ng/src/main/res/drawable/ic_superuser_md2.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/checked"
|
||||
android:drawable="@drawable/ic_superuser_filled_md2"
|
||||
android:state_checked="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/unchecked"
|
||||
android:drawable="@drawable/ic_superuser_outlined_md2" />
|
||||
|
||||
<transition
|
||||
android:drawable="@drawable/avd_superuser_from_filled"
|
||||
android:fromId="@+id/checked"
|
||||
android:toId="@+id/unchecked" />
|
||||
|
||||
<transition
|
||||
android:drawable="@drawable/avd_superuser_to_filled"
|
||||
android:fromId="@+id/unchecked"
|
||||
android:toId="@id/checked" />
|
||||
|
||||
</animated-selector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21Z" />
|
||||
</vector>
|
||||
10
app/apk-ng/src/main/res/drawable/ic_update_md2.xml
Normal file
10
app/apk-ng/src/main/res/drawable/ic_update_md2.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?colorOnSurface"
|
||||
android:pathData="M17,1H7A2,2 0 0,0 5,3V21A2,2 0 0,0 7,23H17A2,2 0 0,0 19,21V3A2,2 0 0,0 17,1M17,19H7V5H17V19M16,13H13V8H11V13H8L12,17L16,13Z" />
|
||||
</vector>
|
||||
6
app/apk-ng/src/main/res/values-night/styles_md2.xml
Normal file
6
app/apk-ng/src/main/res/values-night/styles_md2.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Foundation" parent="Theme.Foundation" />
|
||||
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user