Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
377ea183a7 | ||
|
|
72361ab8bf | ||
|
|
f708e583c3 | ||
|
|
d753e1dc48 | ||
|
|
315a8a3805 | ||
|
|
129fed9c9f | ||
|
|
0baccb7621 | ||
|
|
842a8aa45a | ||
|
|
d17843479c | ||
|
|
0d70cc8e58 | ||
|
|
4e6cacb206 | ||
|
|
52514ba35b | ||
|
|
4d59ce435e | ||
|
|
b3b7fa6f4d | ||
|
|
c057c16391 | ||
|
|
dee7cc6f2b | ||
|
|
3d0d87cb0c | ||
|
|
6b66d9b3f8 | ||
|
|
a301d94858 | ||
|
|
01199470f2 | ||
|
|
9e7ea19567 | ||
|
|
cdc6a6cb4a | ||
|
|
bb2d8fd7e0 | ||
|
|
2b6d418fe6 | ||
|
|
d8b1126b96 | ||
|
|
2eeddcfa80 | ||
|
|
59e3675a36 | ||
|
|
bc386f080d | ||
|
|
2dc1377154 | ||
|
|
610852e2f2 | ||
|
|
15b19bb8ce | ||
|
|
4a598b1837 | ||
|
|
caee2417d6 | ||
|
|
349ca36d4e | ||
|
|
ec86f5caf2 | ||
|
|
b5a5cdfcd2 | ||
|
|
72d799e065 | ||
|
|
d06f22dcd0 | ||
|
|
cb90630f27 | ||
|
|
59ad9204d0 | ||
|
|
cb97c16f5e | ||
|
|
69b48d5345 | ||
|
|
45ed4708c9 | ||
|
|
f3c77bdb3b | ||
|
|
dc0eb9eec1 | ||
|
|
83dd6443cb | ||
|
|
3d77f2d135 | ||
|
|
1ea219bddc | ||
|
|
39adba62d1 | ||
|
|
3526e84e04 | ||
|
|
bfdb706b60 | ||
|
|
a297e07055 | ||
|
|
56b4664ec7 | ||
|
|
70f7c75a92 | ||
|
|
e414b4de92 | ||
|
|
79e68f473f | ||
|
|
6656604809 | ||
|
|
85b4d11912 | ||
|
|
7769a23f59 | ||
|
|
c442f43090 | ||
|
|
d73670bf43 | ||
|
|
dd1967f0d0 | ||
|
|
e3d2fc64ac | ||
|
|
e07f20bf29 | ||
|
|
34f216181f | ||
|
|
8aef775474 | ||
|
|
f669ad92b6 | ||
|
|
cc0b272770 | ||
|
|
9ea6de340d | ||
|
|
be37f8a2a3 | ||
|
|
8a12fac39f | ||
|
|
0242fe12e3 | ||
|
|
acf2e1a5ec | ||
|
|
626db4be56 | ||
|
|
5941fa1ec7 | ||
|
|
1dd8651a1a | ||
|
|
33dd0ca16b |
137
.github/workflows/avd-kernel.yml
vendored
Normal file
137
.github/workflows/avd-kernel.yml
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
name: GKI Kernel Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version_name:
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
With SUBLEVEL of kernel,
|
||||
for example: android12-5.10.66
|
||||
arch:
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
Build arch: aarch64/x86_64
|
||||
debug:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
manifest_name:
|
||||
required: false
|
||||
type: string
|
||||
description: >
|
||||
Local repo manifest xml path,
|
||||
typically for AVD kernel build.
|
||||
secrets:
|
||||
BOOT_SIGN_KEY:
|
||||
required: false
|
||||
CHAT_ID:
|
||||
required: false
|
||||
BOT_TOKEN:
|
||||
required: false
|
||||
MESSAGE_THREAD_ID:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ inputs.version_name }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
uses: easimon/maximize-build-space@master
|
||||
with:
|
||||
root-reserve-mb: 8192
|
||||
temp-reserve-mb: 2048
|
||||
remove-dotnet: 'true'
|
||||
remove-android: 'true'
|
||||
remove-haskell: 'true'
|
||||
remove-codeql: 'true'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: KernelSU
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup need_upload
|
||||
id: need_upload
|
||||
run: |
|
||||
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
|
||||
echo "UPLOAD=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "UPLOAD=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup kernel source
|
||||
run: |
|
||||
echo "Free space:"
|
||||
df -h
|
||||
cd $GITHUB_WORKSPACE
|
||||
sudo apt-get install repo -y
|
||||
mkdir android-kernel && cd android-kernel
|
||||
repo init --depth=1 -u https://android.googlesource.com/kernel/manifest -m "$GITHUB_WORKSPACE/KernelSU/.github/manifests/${{ inputs.manifest_name }}" --repo-rev=v2.16
|
||||
repo --version
|
||||
repo --trace sync -c -j$(nproc --all) --no-tags
|
||||
df -h
|
||||
|
||||
- name: Setup KernelSU
|
||||
env:
|
||||
PATCH_PATH: ${{ inputs.patch_path }}
|
||||
IS_DEBUG_KERNEL: ${{ inputs.debug }}
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/android-kernel
|
||||
echo "[+] KernelSU setup"
|
||||
GKI_ROOT=$(pwd)
|
||||
echo "[+] GKI_ROOT: $GKI_ROOT"
|
||||
echo "[+] Copy KernelSU driver to $GKI_ROOT/common/drivers"
|
||||
ln -sf $GITHUB_WORKSPACE/KernelSU/kernel $GKI_ROOT/common/drivers/kernelsu
|
||||
echo "[+] Add KernelSU driver to Makefile"
|
||||
DRIVER_MAKEFILE=$GKI_ROOT/common/drivers/Makefile
|
||||
DRIVER_KCONFIG=$GKI_ROOT/common/drivers/Kconfig
|
||||
grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE"
|
||||
grep -q "kernelsu" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG"
|
||||
echo "[+] Apply KernelSU patches"
|
||||
cd $GKI_ROOT/common/ && git apply $GITHUB_WORKSPACE/KernelSU/.github/patches/$PATCH_PATH/*.patch || echo "[-] No patch found"
|
||||
|
||||
if [ "$IS_DEBUG_KERNEL" = "true" ]; then
|
||||
echo "[+] Enable debug features for kernel"
|
||||
printf "\nccflags-y += -DCONFIG_KSU_DEBUG\n" >> $GITHUB_WORKSPACE/KernelSU/kernel/Makefile
|
||||
fi
|
||||
repo status
|
||||
echo "[+] KernelSU setup done."
|
||||
cd $GITHUB_WORKSPACE/KernelSU
|
||||
VERSION=$(($(git rev-list --count HEAD) + 10200))
|
||||
echo "VERSION: $VERSION"
|
||||
echo "kernelsu_version=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Make working directory clean to avoid dirty
|
||||
working-directory: android-kernel
|
||||
run: |
|
||||
rm common/android/abi_gki_protected_exports_* || echo "No protected exports!"
|
||||
git config --global user.email "bot@kernelsu.org"
|
||||
git config --global user.name "KernelSUBot"
|
||||
cd common/ && git add -A && git commit -a -m "Add KernelSU"
|
||||
repo status
|
||||
|
||||
- name: Build kernel
|
||||
working-directory: android-kernel
|
||||
run: |
|
||||
if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then
|
||||
export KSU_EXPECTED_SIZE=${{ vars.EXPECTED_SIZE }}
|
||||
export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }}
|
||||
fi
|
||||
tools/bazel run --config=fast --config=stamp --lto=thin //common-modules/virtual-device:virtual_device_${{ inputs.arch }}_dist -- --dist_dir=dist
|
||||
NAME=kernel-${{ inputs.arch }}-avd-${{ inputs.version_name }}-${{ env.kernelsu_version }}
|
||||
TARGET_IMAGE=dist/bzImage
|
||||
if [ ! -e $TARGET_IMAGE ]; then
|
||||
TARGET_IMAGE=dist/Image
|
||||
fi
|
||||
mv $TARGET_IMAGE $NAME
|
||||
echo "file_path=android-kernel/$NAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload Kernel
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: kernel-${{ inputs.arch }}-avd-${{ inputs.version_name }}-${{ env.kernelsu_version }}
|
||||
path: "${{ env.file_path }}"
|
||||
74
.github/workflows/build-lkm.yml
vendored
Normal file
74
.github/workflows/build-lkm.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
name: Build LKM for KernelSU
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
upload:
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
description: "Whether to upload to branch"
|
||||
secrets:
|
||||
# username:github_pat
|
||||
TOKEN:
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
upload:
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
description: "Whether to upload to branch"
|
||||
jobs:
|
||||
build-lkm:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- version: "android12-5.10"
|
||||
sub_level: 233
|
||||
os_patch_level: 2025-02
|
||||
- version: "android13-5.10"
|
||||
sub_level: 234
|
||||
os_patch_level: 2025-03
|
||||
- version: "android13-5.15"
|
||||
sub_level: 178
|
||||
os_patch_level: 2025-03
|
||||
- version: "android14-5.15"
|
||||
sub_level: 178
|
||||
os_patch_level: 2025-03
|
||||
- version: "android14-6.1"
|
||||
sub_level: 129
|
||||
os_patch_level: 2025-04
|
||||
- version: "android15-6.6"
|
||||
sub_level: 82
|
||||
os_patch_level: 2025-04
|
||||
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
||||
uses: ./.github/workflows/gki-kernel.yml
|
||||
with:
|
||||
version: ${{ matrix.version }}
|
||||
version_name: ${{ matrix.version }}.${{ matrix.sub_level }}
|
||||
tag: ${{ matrix.version }}-${{ matrix.os_patch_level }}
|
||||
os_patch_level: ${{ matrix.os_patch_level }}
|
||||
build_lkm: true
|
||||
|
||||
push-to-branch:
|
||||
needs: [build-lkm]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ inputs.upload }}
|
||||
steps:
|
||||
- name: Download all workflow run artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: bin/
|
||||
merge-multiple: true
|
||||
- name: Push to branch LKM
|
||||
run: |
|
||||
cd bin
|
||||
git config --global init.defaultBranch lkm
|
||||
git init
|
||||
git remote add origin https://${{ secrets.TOKEN }}@github.com/${{ github.repository }}
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
find . -type f
|
||||
git add .
|
||||
git commit -m "Upload LKM from ${{ github.sha }}" -m "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
git push --force --set-upstream origin lkm
|
||||
86
.github/workflows/build-manager.yml
vendored
86
.github/workflows/build-manager.yml
vendored
@@ -16,8 +16,76 @@ on:
|
||||
- 'manager/**'
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build_lkm:
|
||||
required: true
|
||||
type: choice
|
||||
default: "auto"
|
||||
options:
|
||||
- "true"
|
||||
- "false"
|
||||
- "auto"
|
||||
description: "Whether to build lkm"
|
||||
upload_lkm:
|
||||
required: true
|
||||
type: boolean
|
||||
default: true
|
||||
description: "Whether to upload lkm"
|
||||
jobs:
|
||||
check-build-lkm:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
build_lkm: ${{ steps.check-build.outputs.build_lkm }}
|
||||
upload_lkm: ${{ steps.check-build.outputs.upload_lkm }}
|
||||
steps:
|
||||
- name: check build
|
||||
id: check-build
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ "${{ inputs.build_lkm }}" != "auto" ]; then
|
||||
kernel_changed="${{ inputs.build_lkm }}"
|
||||
else
|
||||
kernel_changed=true
|
||||
mkdir tmp
|
||||
cd tmp
|
||||
git config --global init.defaultBranch bot
|
||||
git config --global user.name 'Bot'
|
||||
git config --global user.email 'bot@github.shirkneko.io'
|
||||
git init .
|
||||
git remote add origin https://github.com/${{ github.repository }}
|
||||
CURRENT_COMMIT="${{ github.event.head_commit.id }}"
|
||||
git fetch origin $CURRENT_COMMIT --depth=1
|
||||
git fetch origin lkm --depth=1
|
||||
LKM_COMMIT="$(git log --format=%B -n 1 origin/lkm | head -n 1)"
|
||||
LKM_COMMIT="${LKM_COMMIT#Upload LKM from }"
|
||||
LKM_COMMIT=$(echo "$LKM_COMMIT" | tr -d '[:space:]')
|
||||
echo "LKM_COMMIT=$LKM_COMMIT"
|
||||
git fetch origin "$LKM_COMMIT" --depth=1
|
||||
git diff --quiet "$LKM_COMMIT" "$CURRENT_COMMIT" -- kernel :!kernel/setup.sh .github/workflows/build-lkm.yml .github/workflows/build-kernel-*.yml && kernel_changed=false
|
||||
cd ..
|
||||
rm -rf tmp
|
||||
fi
|
||||
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/main' ]; then
|
||||
need_upload=true
|
||||
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
need_upload="${{ inputs.upload_lkm }}"
|
||||
else
|
||||
need_upload=false
|
||||
fi
|
||||
echo "kernel changed: $kernel_changed"
|
||||
echo "need upload: $need_upload"
|
||||
echo "build_lkm=$kernel_changed" >> "$GITHUB_OUTPUT"
|
||||
echo "upload_lkm=$need_upload" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build-lkm:
|
||||
needs: check-build-lkm
|
||||
uses: ./.github/workflows/build-lkm.yml
|
||||
if: ${{ needs.check-build-lkm.outputs.build_lkm == 'true' }}
|
||||
with:
|
||||
upload: ${{ needs.check-build-lkm.outputs.upload_lkm == 'true' }}
|
||||
secrets: inherit
|
||||
build-susfs:
|
||||
if: ${{ always() }}
|
||||
needs: [ check-build-lkm, build-lkm ]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -29,7 +97,8 @@ jobs:
|
||||
os: ${{ matrix.os }}
|
||||
|
||||
build-kpmmgr:
|
||||
needs: build-susfs
|
||||
if: ${{ always() }}
|
||||
needs: [ check-build-lkm, build-lkm ]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -41,7 +110,8 @@ jobs:
|
||||
os: ${{ matrix.os }}
|
||||
|
||||
build-ksud:
|
||||
needs: build-kpmmgr
|
||||
if: ${{ always() }}
|
||||
needs: [ check-build-lkm, build-lkm ]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -49,10 +119,14 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
- target: x86_64-linux-android
|
||||
os: ubuntu-latest
|
||||
- target: armv7-linux-androideabi
|
||||
os: ubuntu-latest
|
||||
uses: ./.github/workflows/ksud.yml
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
os: ${{ matrix.os }}
|
||||
pack_lkm: true
|
||||
pull_lkm: ${{ needs.check-build-lkm.outputs.build_lkm != 'true' }}
|
||||
|
||||
build-manager:
|
||||
if: ${{ always() }}
|
||||
@@ -126,12 +200,20 @@ jobs:
|
||||
name: ksud-x86_64-linux-android
|
||||
path: .
|
||||
|
||||
- name: Download arm ksud
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ksud-armv7-linux-androideabi
|
||||
path: .
|
||||
|
||||
- name: Copy ksud to app jniLibs
|
||||
run: |
|
||||
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||
mkdir -p app/src/main/jniLibs/x86_64
|
||||
mkdir -p app/src/main/jniLibs/armeabi-v7a
|
||||
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
|
||||
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
|
||||
cp -f ../armv7-linux-androideabi/release/zakozako ../manager/app/src/main/jniLibs/armeabi-v7a/libzakozako.so
|
||||
|
||||
- name: Copy kpmmgr to app jniLibs
|
||||
run: |
|
||||
|
||||
79
.github/workflows/gki-kernel-mock.yml
vendored
Normal file
79
.github/workflows/gki-kernel-mock.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: GKI Kernel Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
Output directory of gki,
|
||||
for example: android12-5.10
|
||||
version_name:
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
With SUBLEVEL of kernel,
|
||||
for example: android12-5.10.66
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
Part of branch name of common kernel manifest,
|
||||
for example: android12-5.10-2021-11
|
||||
os_patch_level:
|
||||
required: false
|
||||
type: string
|
||||
description: >
|
||||
Patch level of common kernel manifest,
|
||||
for example: 2021-11
|
||||
default: 2022-05
|
||||
patch_path:
|
||||
required: false
|
||||
type: string
|
||||
description: >
|
||||
Directory name of .github/patches/<patch_path>
|
||||
for example: 5.10
|
||||
use_cache:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
embed_ksud:
|
||||
required: false
|
||||
type: string
|
||||
default: ksud-aarch64-linux-android
|
||||
description: >
|
||||
Artifact name of prebuilt ksud to be embedded
|
||||
for example: ksud-aarch64-linux-android
|
||||
debug:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
build_lkm:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
BOOT_SIGN_KEY:
|
||||
required: false
|
||||
CHAT_ID:
|
||||
required: false
|
||||
BOT_TOKEN:
|
||||
required: false
|
||||
MESSAGE_THREAD_ID:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
mock_build:
|
||||
name: Mock build ${{ inputs.version_name }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create mocking ko
|
||||
run: |
|
||||
echo "${{ inputs.version }}_kernelsu.ko" > ${{ inputs.version }}_kernelsu.ko
|
||||
- name: Upload LKM
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ inputs.build_lkm == true }}
|
||||
with:
|
||||
name: ${{ inputs.version }}-lkm
|
||||
path: ./*_kernelsu.ko
|
||||
261
.github/workflows/gki-kernel.yml
vendored
Normal file
261
.github/workflows/gki-kernel.yml
vendored
Normal file
@@ -0,0 +1,261 @@
|
||||
name: GKI Kernel Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
Output directory of gki,
|
||||
for example: android12-5.10
|
||||
version_name:
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
With SUBLEVEL of kernel,
|
||||
for example: android12-5.10.66
|
||||
tag:
|
||||
required: true
|
||||
type: string
|
||||
description: >
|
||||
Part of branch name of common kernel manifest,
|
||||
for example: android12-5.10-2021-11
|
||||
os_patch_level:
|
||||
required: false
|
||||
type: string
|
||||
description: >
|
||||
Patch level of common kernel manifest,
|
||||
for example: 2021-11
|
||||
default: 2022-05
|
||||
patch_path:
|
||||
required: false
|
||||
type: string
|
||||
description: >
|
||||
Directory name of .github/patches/<patch_path>
|
||||
for example: 5.10
|
||||
use_cache:
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
embed_ksud:
|
||||
required: false
|
||||
type: string
|
||||
default: ksud-aarch64-linux-android
|
||||
description: >
|
||||
Artifact name of prebuilt ksud to be embedded
|
||||
for example: ksud-aarch64-linux-android
|
||||
debug:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
build_lkm:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
secrets:
|
||||
BOOT_SIGN_KEY:
|
||||
required: false
|
||||
CHAT_ID:
|
||||
required: false
|
||||
BOT_TOKEN:
|
||||
required: false
|
||||
MESSAGE_THREAD_ID:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ inputs.version_name }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
|
||||
CCACHE_NOHASHDIR: "true"
|
||||
CCACHE_HARDLINK: "true"
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
uses: easimon/maximize-build-space@master
|
||||
with:
|
||||
root-reserve-mb: 8192
|
||||
temp-reserve-mb: 2048
|
||||
remove-dotnet: 'true'
|
||||
remove-android: 'true'
|
||||
remove-haskell: 'true'
|
||||
remove-codeql: 'true'
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: KernelSU
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup need_upload
|
||||
id: need_upload
|
||||
run: |
|
||||
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
|
||||
echo "UPLOAD=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "UPLOAD=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Setup kernel source
|
||||
run: |
|
||||
echo "Free space:"
|
||||
df -h
|
||||
cd $GITHUB_WORKSPACE
|
||||
sudo apt-get install repo -y
|
||||
mkdir android-kernel && cd android-kernel
|
||||
repo init --depth=1 --u https://android.googlesource.com/kernel/manifest -b common-${{ inputs.tag }} --repo-rev=v2.35
|
||||
REMOTE_BRANCH=$(git ls-remote https://android.googlesource.com/kernel/common ${{ inputs.tag }})
|
||||
DEFAULT_MANIFEST_PATH=.repo/manifests/default.xml
|
||||
if grep -q deprecated <<< $REMOTE_BRANCH; then
|
||||
echo "Found deprecated branch: ${{ inputs.tag }}"
|
||||
sed -i 's/"${{ inputs.tag }}"/"deprecated\/${{ inputs.tag }}"/g' $DEFAULT_MANIFEST_PATH
|
||||
cat $DEFAULT_MANIFEST_PATH
|
||||
fi
|
||||
repo --version
|
||||
repo --trace sync -c -j$(nproc --all) --no-tags
|
||||
df -h
|
||||
|
||||
- name: Setup KernelSU
|
||||
env:
|
||||
PATCH_PATH: ${{ inputs.patch_path }}
|
||||
IS_DEBUG_KERNEL: ${{ inputs.debug }}
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/android-kernel
|
||||
echo "[+] KernelSU setup"
|
||||
GKI_ROOT=$(pwd)
|
||||
echo "[+] GKI_ROOT: $GKI_ROOT"
|
||||
echo "[+] Copy KernelSU driver to $GKI_ROOT/common/drivers"
|
||||
ln -sf $GITHUB_WORKSPACE/KernelSU/kernel $GKI_ROOT/common/drivers/kernelsu
|
||||
echo "[+] Add KernelSU driver to Makefile"
|
||||
DRIVER_MAKEFILE=$GKI_ROOT/common/drivers/Makefile
|
||||
DRIVER_KCONFIG=$GKI_ROOT/common/drivers/Kconfig
|
||||
grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE"
|
||||
grep -q "kernelsu" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG"
|
||||
echo "[+] Apply Compilation Patches"
|
||||
if [ ! -e build/build.sh ]; then
|
||||
GLIBC_VERSION=$(ldd --version 2>/dev/null | head -n 1 | awk '{print $NF}')
|
||||
echo "GLIBC_VERSION: $GLIBC_VERSION"
|
||||
if [ "$(printf '%s\n' "2.38" "$GLIBC_VERSION" | sort -V | head -n1)" = "2.38" ]; then
|
||||
echo "Patching resolve_btfids/Makefile"
|
||||
cd $GKI_ROOT/common/ && sed -i '/\$(Q)\$(MAKE) -C \$(SUBCMD_SRC) OUTPUT=\$(abspath \$(dir \$@))\/ \$(abspath \$@)/s//$(Q)$(MAKE) -C $(SUBCMD_SRC) EXTRA_CFLAGS="$(CFLAGS)" OUTPUT=$(abspath $(dir $@))\/ $(abspath $@)/' tools/bpf/resolve_btfids/Makefile || echo "No patch needed."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$IS_DEBUG_KERNEL" = "true" ]; then
|
||||
echo "[+] Enable debug features for kernel"
|
||||
printf "\nccflags-y += -DCONFIG_KSU_DEBUG\n" >> $GITHUB_WORKSPACE/KernelSU/kernel/Makefile
|
||||
fi
|
||||
repo status
|
||||
echo "[+] KernelSU setup done."
|
||||
|
||||
- name: Symbol magic
|
||||
run: |
|
||||
echo "[+] Export all symbol from abi_gki_aarch64.xml"
|
||||
COMMON_ROOT=$GITHUB_WORKSPACE/android-kernel/common
|
||||
KSU_ROOT=$GITHUB_WORKSPACE/KernelSU
|
||||
ABI_XML=$COMMON_ROOT/android/abi_gki_aarch64.xml
|
||||
SYMBOL_LIST=$COMMON_ROOT/android/abi_gki_aarch64
|
||||
# python3 $KSU_ROOT/scripts/abi_gki_all.py $ABI_XML > $SYMBOL_LIST
|
||||
echo "[+] Add KernelSU symbols"
|
||||
cat $KSU_ROOT/kernel/export_symbol.txt | awk '{sub("[ \t]+","");print " "$0}' >> $SYMBOL_LIST
|
||||
|
||||
- name: Setup ccache
|
||||
if: inputs.use_cache == true
|
||||
uses: hendrikmuhs/ccache-action@v1
|
||||
with:
|
||||
key: gki-kernel-aarch64-${{ inputs.version_name }}
|
||||
max-size: 2G
|
||||
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
|
||||
- name: Setup for LKM
|
||||
if: ${{ inputs.build_lkm == true }}
|
||||
working-directory: android-kernel
|
||||
run: |
|
||||
pip install ast-grep-cli
|
||||
sudo apt-get install llvm-15 -y
|
||||
ast-grep -U -p '$$$ check_exports($$$) {$$$}' -r '' common/scripts/mod/modpost.c
|
||||
ast-grep -U -p 'check_exports($$$);' -r '' common/scripts/mod/modpost.c
|
||||
sed -i '/config KSU/,/help/{s/default y/default m/}' common/drivers/kernelsu/Kconfig
|
||||
echo "drivers/kernelsu/kernelsu.ko" >> common/android/gki_aarch64_modules
|
||||
|
||||
# bazel build, android14-5.15, android14-6.1 use bazel
|
||||
if [ ! -e build/build.sh ]; then
|
||||
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found"
|
||||
if [ -e common/modules.bzl ]; then
|
||||
sed -i 's/_COMMON_GKI_MODULES_LIST = \[/_COMMON_GKI_MODULES_LIST = \[ "drivers\/kernelsu\/kernelsu.ko",/g' common/modules.bzl
|
||||
fi
|
||||
else
|
||||
TARGET_FILE="build/kernel/build.sh"
|
||||
if [ ! -e "$TARGET_FILE" ]; then
|
||||
TARGET_FILE="build/build.sh"
|
||||
fi
|
||||
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' $TARGET_FILE || echo "No unknown symbol in $TARGET_FILE"
|
||||
sed -i 's/if ! diff -u "\${KERNEL_DIR}\/\${MODULES_ORDER}" "\${OUT_DIR}\/modules\.order"; then/if false; then/g' $TARGET_FILE
|
||||
sed -i 's@${ROOT_DIR}/build/abi/compare_to_symbol_list@echo@g' $TARGET_FILE
|
||||
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found"
|
||||
fi
|
||||
|
||||
- name: Make working directory clean to avoid dirty
|
||||
working-directory: android-kernel
|
||||
run: |
|
||||
if [ -e common/BUILD.bazel ]; then
|
||||
sed -i '/^[[:space:]]*"protected_exports_list"[[:space:]]*:[[:space:]]*"android\/abi_gki_protected_exports_aarch64",$/d' common/BUILD.bazel
|
||||
fi
|
||||
rm common/android/abi_gki_protected_exports_* || echo "No protected exports!"
|
||||
git config --global user.email "bot@kernelsu.org"
|
||||
git config --global user.name "KernelSUBot"
|
||||
cd common/ && git add -A && git commit -a -m "Add KernelSU"
|
||||
repo status
|
||||
|
||||
- name: Build Kernel/LKM
|
||||
working-directory: android-kernel
|
||||
run: |
|
||||
if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then
|
||||
export KSU_EXPECTED_SIZE=${{ vars.EXPECTED_SIZE }}
|
||||
export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }}
|
||||
fi
|
||||
if [ -e build/build.sh ]; then
|
||||
LTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh CC="/usr/bin/ccache clang"
|
||||
else
|
||||
tools/bazel run --disk_cache=/home/runner/.cache/bazel --config=fast --config=stamp --lto=thin //common:kernel_aarch64_dist -- --dist_dir=dist
|
||||
fi
|
||||
|
||||
- name: Prepare artifacts
|
||||
id: prepareArtifacts
|
||||
run: |
|
||||
OUTDIR=android-kernel/out/${{ inputs.version }}/dist
|
||||
if [ ! -e $OUTDIR ]; then
|
||||
OUTDIR=android-kernel/dist
|
||||
fi
|
||||
mkdir output
|
||||
if [ "${{ inputs.build_lkm}}" = "true" ]; then
|
||||
llvm-strip-15 -d $OUTDIR/kernelsu.ko
|
||||
mv $OUTDIR/kernelsu.ko ./output/${{ inputs.version }}_kernelsu.ko
|
||||
else
|
||||
cp $OUTDIR/Image ./output/
|
||||
cp $OUTDIR/Image.lz4 ./output/
|
||||
git clone https://github.com/Kernel-SU/AnyKernel3
|
||||
rm -rf ./AnyKernel3/.git
|
||||
cp $OUTDIR/Image ./AnyKernel3/
|
||||
fi
|
||||
|
||||
- name: Upload Image and Image.gz
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ inputs.build_lkm == false }}
|
||||
with:
|
||||
name: Image-${{ inputs.version_name }}_${{ inputs.os_patch_level }}
|
||||
path: ./output/*
|
||||
|
||||
- name: Upload AnyKernel3
|
||||
if: ${{ inputs.build_lkm == false }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AnyKernel3-${{ inputs.version_name }}_${{ inputs.os_patch_level }}
|
||||
path: ./AnyKernel3/*
|
||||
|
||||
- name: Upload LKM
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ inputs.build_lkm == true }}
|
||||
with:
|
||||
name: ${{ inputs.version }}-lkm
|
||||
path: ./output/*_kernelsu.ko
|
||||
@@ -1,50 +1,65 @@
|
||||
# SukiSU
|
||||
# SukiSU Ultra
|
||||
|
||||
**English** | [简体中文](README.md) | [日本語](README-ja.md)
|
||||
|
||||
|
||||
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
|
||||
|
||||
**Experimental! Use at your own risk!** This solution is based on [KernelSU](https://github.com/tiann/KernelSU) and is experimental!
|
||||
|
||||
>
|
||||
> This is an unofficial fork. All rights are reserved to [@tiann](https://github.com/tiann)
|
||||
> However, we will be a separately maintained branch of KSU in the future
|
||||
>
|
||||
|
||||
- Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only)
|
||||
> However, we will be a separately maintained branch of KSU in the future
|
||||
|
||||
## How to add
|
||||
|
||||
Use the susfs-stable or susfs-dev branch (integrated susfs with support for non-GKI devices)
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
|
||||
Use the main branch
|
||||
Using main branching (non-GKI device builds are not supported)
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
||||
```
|
||||
|
||||
Using branches that support non-GKI devices
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||
```
|
||||
|
||||
## How to use integrated susfs
|
||||
|
||||
1. Use the susfs-dev branch directly without any patching
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
|
||||
## KPM Support
|
||||
|
||||
## KPM support
|
||||
|
||||
- We have removed duplicate KSU functions based on KernelPatch and retained KPM support.
|
||||
- Based on KernelPatch, we have removed duplicates of KSU and kept only KPM support.
|
||||
- We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality.
|
||||
|
||||
We will introduce more APatch-compatible functions to ensure the completeness of KPM functionality.
|
||||
|
||||
Open source address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||
KPM templates: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
> [!Note]
|
||||
> 1. `CONFIG_KPM=y` needs to be added.
|
||||
> 2. Non-GKI devices need to add `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=y` as well.
|
||||
> 3. Some kernel source code below `4.19` also needs to be backport from `4.19` to the header file `set_memory.h`.
|
||||
|
||||
## How to do a system update to retain ROOT
|
||||
- After OTA, don't reboot first, go to the manager flashing/patching kernel interface, find `GKI/non_GKI install` and select the Anykernel3 kernel zip file that needs to be flashed, select the slot that is opposite to the current running slot of the system for flashing, and then reboot to retain the GKI mode update (This method is not supported for all non-GKI devices, so please try it yourself. It is the safest way to use TWRP for non-GKI devices.)
|
||||
- Or use LKM mode to install to the unused slot (after OTA).
|
||||
|
||||
## Compatibility Status
|
||||
- KernelSU (versions prior to v1.0.0) officially supports Android GKI 2.0 devices (kernel 5.10+)
|
||||
|
||||
- Older kernels (4.4+) are also compatible, but the kernel must be built manually
|
||||
|
||||
- KernelSU can support 3.x kernels (3.4-3.18) through additional reverse ports
|
||||
|
||||
- Currently supports `arm64-v8a`, `armeabi-v7a (bare)` and some `X86_64`
|
||||
|
||||
KPM template address: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
## More links
|
||||
|
||||
Projects compiled based on Sukisu and susfs
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
@@ -53,32 +68,37 @@ Projects compiled based on Sukisu and susfs
|
||||
- This method references the hook method from (https://github.com/rsuntk/KernelSU)
|
||||
|
||||
1. **KPROBES hook:**
|
||||
- This method only supports GKI (5.10 - 6.x) kernels, and all non-GKI kernels must use manual hooks.
|
||||
- For Loadable Kernel Modules (LKM)
|
||||
- Default hooking method for GKI kernels
|
||||
- Requires `CONFIG_KPROBES=y`.
|
||||
2. **Manual hooks:**
|
||||
- For GKI (5.10 - 6.x) kernels, add `CONFIG_KSU_MANUAL_HOOK=y` to the kernel defconfig and make sure to protect KernelSU hooks by using `#ifdef CONFIG_KSU_MANUAL_HOOK` instead of `#ifdef CONFIG_KSU`.
|
||||
- Standard KernelSU hooks: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx syscall hooks: https://github.com/backslashxx/KernelSU/issues/5
|
||||
- Some non-GKI devices that manually integrate KPROBES do not require the manual VFS hook `new_hook.patch` patch
|
||||
- Also used for Loadable Kernel Module (LKM)
|
||||
- Default hook method on GKI kernels.
|
||||
- Need `CONFIG_KPROBES=y`
|
||||
|
||||
2. **Manual hook:**
|
||||
- Standard KernelSU hook: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx's syscall manual hook: https://github.com/backslashxx/KernelSU/issues/5
|
||||
- Default hook method on Non-GKI kernels.
|
||||
- Need `CONFIG_KSU_MANUAL_HOOK=y`
|
||||
|
||||
## Usage
|
||||
### GKI
|
||||
1. such as Xiaomi, Redmi, Samsung, and other devices (does not include manufacturers that modified the kernel like Meizu, OnePlus, RealMe, and OPPO)
|
||||
2. Use the prebuilt GKI kernel, the ones with their name ending with AnyKernel3, mentioned in the 'More Links' section, and then flash it with recoveries like TWRP
|
||||
3. Generally, packages with a plain .zip suffix are universal. However, if your device has a MediaTek processor, you should use the ones with .gz suffix, and packages with .lz4 suffix are dedicated to Google devices.
|
||||
|
||||
### Universal GKI
|
||||
|
||||
Please **all** refer to https://kernelsu.org/zh_CN/guide/installation.html
|
||||
|
||||
> [!Note]
|
||||
> 1. for devices with GKI 2.0 such as Xiaomi, Redmi, Samsung, etc. (excludes kernel-modified manufacturers such as Meizu, OnePlus, Zenith, and oppo)
|
||||
> 2. Find the GKI build in [more links](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5). Find the device kernel version. Then download it and use TWRP or kernel flashing tool to flash the zip file with AnyKernel3 suffix.
|
||||
> 3. The .zip archive without suffix is uncompressed, the gz suffix is the compression used by Tenguet models.
|
||||
|
||||
|
||||
### OnePlus
|
||||
|
||||
1. Use the link mentioned in the 'More Links' section to create a customized build with your device information, and then flash the zip file with the AnyKernel3 suffix.
|
||||
|
||||
> [!Note]
|
||||
> - You only need to fill in the first two parts of kernel versions, such as 5.10, 5.15, 6.1, or 6.6.
|
||||
> - Please search for the processor codename by yourself, usually it is all English without numbers.
|
||||
> - You can find the branch and configuration files from the OnePlus open-source kernel repository.
|
||||
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
1. Kernel-based `su` and root access management.
|
||||
@@ -88,21 +108,18 @@ Projects compiled based on Sukisu and susfs
|
||||
5. More customization
|
||||
6. Support for KPM kernel modules
|
||||
|
||||
|
||||
|
||||
## License
|
||||
|
||||
- The file in the “kernel” directory is under [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license.
|
||||
- All other parts except the “kernel” directory are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||
|
||||
## Sponsorship list
|
||||
|
||||
- [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support
|
||||
- [zaoqi123](https://github.com/zaoqi123) It's not a bad idea to buy me a milk tea
|
||||
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks
|
||||
|
||||
|
||||
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao
|
||||
|
||||
If the above list does not have your name, I will update it as soon as possible, and thanks again for your support!
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# SukiSU
|
||||
# SukiSU Ultra
|
||||
|
||||
**日本語** | [简体中文](README.md) | [English](README-en.md)
|
||||
|
||||
@@ -7,71 +7,98 @@
|
||||
**試験中なビルドです!自己責任で使用してください!**<br>
|
||||
このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。
|
||||
|
||||
>
|
||||
> これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。
|
||||
>
|
||||
>ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
||||
|
||||
- GKI 非対応なデバイスに完全に適応 (susfs-dev と unsusfs-patched dev ブランチのみ)
|
||||
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
||||
|
||||
## 追加方法
|
||||
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。
|
||||
|
||||
メイン分岐の使用(GKI デバイス以外のビルドはサポートされていません。)
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
||||
```
|
||||
|
||||
GKI以外のデバイスをサポートするブランチを使用する
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||
```
|
||||
|
||||
## 統合された susfs の使い方
|
||||
|
||||
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
|
||||
メインブランチを使用する場合
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
||||
```
|
||||
## 統合された susfs の使い方
|
||||
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
|
||||
|
||||
## KPM に対応
|
||||
|
||||
- KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。
|
||||
- KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。
|
||||
|
||||
|
||||
オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||
|
||||
|
||||
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
> [!Note]
|
||||
> 1. `CONFIG_KPM=y` が必要である。
|
||||
> 2.非 GKI デバイスには `CONFIG_KALLSYMS=y` と `CONFIG_KALLSYMS_ALL=y` も必要です。
|
||||
> 3.いくつかのカーネル `4.19` およびそれ以降のソースコードでは、 `4.19` からバックポートされた `set_memory.h` ヘッダーファイルも必要です。
|
||||
|
||||
|
||||
## ROOT を保持するシステムアップデートの方法
|
||||
- OTAの後、最初に再起動せず、マネージャのフラッシュ/パッチカーネルインターフェイスに移動し、`GKI/non_GKI 取り付け`を見つけ、フラッシュする必要があるAnykernel3カーネルzipファイルを選択し、フラッシュするためにシステムの現在の実行スロットと反対のスロットを選択し、GKIモードアップデートを保持するために再起動します(この方法は、現時点ではすべてのnon_GKIデバイスでサポートされていませんので、各自でお試しください。 (この方法は、すべての非GKIデバイスでサポートされていませんので、ご自身でお試しください)。
|
||||
- または、LKMモードを使用して未使用のスロットにインストールします(OTA後)。
|
||||
|
||||
## 互換性ステータス
|
||||
- KernelSU(v1.0.0より前のバージョン)はAndroid GKI 2.0デバイス(カーネル5.10以上)を公式にサポートしています。
|
||||
|
||||
- 古いカーネル(4.4+)も互換性がありますが、カーネルは手動でビルドする必要があります。
|
||||
|
||||
- KernelSU は追加のリバースポートを通じて 3.x カーネル (3.4-3.18) をサポートしています。
|
||||
|
||||
- 現在は `arm64-v8a`、`armeabi-v7a (bare)`、いくつかの `X86_64` をサポートしています。
|
||||
|
||||
## その他のリンク
|
||||
|
||||
SukiSU と susfs をベースにコンパイルされたプロジェクトです。
|
||||
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
|
||||
## フックの方式
|
||||
|
||||
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
|
||||
|
||||
1. **KPROBES フック:**
|
||||
- この方式は GKI (5.10 - 6.x) のカーネルのみに対応しています。GKI 以外のカーネルは手動でフックを使用する必要があります。
|
||||
- 読み込み可能なカーネルモジュールの場合 (LKM)
|
||||
- GKI カーネルのデフォルトとなるフック方式
|
||||
- `CONFIG_KPROBES=y` が必要です。
|
||||
- `CONFIG_KPROBES=y` が必要です
|
||||
|
||||
2. **手動でフック:**
|
||||
- GKI (5.10 - 6.x) のカーネルの場合、カーネルの defconfig に `CONFIG_KSU_MANUAL_HOOK=y` を追加して `#ifdef CONFIG_KSU` ではなく `#ifdef CONFIG_KSU_MANUAL_HOOK` を使用して KernelSU フックを保護するようにしてください。
|
||||
- 標準の KernelSU フック: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx syscall フック: https://github.com/backslashxx/KernelSU/issues/5
|
||||
- KPROBES を手動で統合する一部の非 GKI デバイスでは手動の VFS フック `new_hook.patch` パッチは不要です。
|
||||
|
||||
- 非 GKI カーネル用のデフォルトフッキングメソッド
|
||||
- `CONFIG_KSU_MANUAL_HOOK=y` が必要です
|
||||
|
||||
## 使い方
|
||||
### GKI
|
||||
1. Xiaomi、Redmi、Samsung などのデバイス (Meizu、OnePlus、Realme、OPPO などのカーネルを変更したメーカー以外)
|
||||
2. `その他のリンク`の項目で言及されているカーネル名が、AnyKernel3 で終わるビルド済みの GKI カーネルを TWRP などのリカバリーでフラッシュします。
|
||||
3. 一般的な .zip の接頭辞を持つパッケージは汎用的になります。ただし、デバイスに MediaTek 製の SoC が搭載されている場合は、.gz の接頭辞を持つパッケージを使用する必要があります。その他に .lz4 の接頭辞を持つパッケージは Google 製デバイス専用です。
|
||||
|
||||
### ユニバーサルGKI
|
||||
|
||||
https://kernelsu.org/zh_CN/guide/installation.html をご参照ください。
|
||||
|
||||
> [!Note]
|
||||
> 1.Xiaomi、Redmi、Samsung などの GKI 2.0 を搭載したデバイス用 (Meizu、Yiga、Zenith、oppo などのマジックカーネルを搭載したメーカーは除く)。
|
||||
> 2. [more links](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5) で GKI ビルドを検索します。 デバイスのカーネルバージョンを検索します。 次に、それをダウンロードし、TWRPまたはカーネルフラッシングツールを使用して、AnyKernel3の接尾辞が付いたzipファイルをフラッシュします。
|
||||
> 接尾辞なしの.zipアーカイブは非圧縮で、接尾辞gzはTenguetモデルで使用されている圧縮方法です。
|
||||
### OnePlus
|
||||
|
||||
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
|
||||
|
||||
> [!Note]
|
||||
> - 5.10、5.15、6.1、6.6 などのカーネルバージョンの最初の 2 文字のみを入力する必要があります。
|
||||
> - SoC のコードネームは自分で検索してください。通常は、数字がなく英語表記のみです。
|
||||
> - ブランチと構成ファイルは、OnePlus オープンソースカーネルリポジトリから見つけることができます。
|
||||
|
||||
|
||||
## 機能
|
||||
|
||||
1. カーネルベースな `su` および root アクセスの管理。
|
||||
@@ -81,21 +108,18 @@ SukiSU と susfs をベースにコンパイルされたプロジェクトです
|
||||
5. その他のカスタマイズ
|
||||
6. KPM カーネルモジュールに対応
|
||||
|
||||
|
||||
|
||||
## ライセンス
|
||||
|
||||
- “kernel” ディレクトリ内のファイルは [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.ja.html) のみライセンス下にあります。
|
||||
- “kernel” ディレクトリを除くその他すべての部分は [GPL-3.0 またはそれ以降](https://www.gnu.org/licenses/gpl-3.0.html) のライセンス下にあります。
|
||||
|
||||
## スポンサーシップの一覧
|
||||
|
||||
- [Ktouls](https://github.com/Ktouls) 応援をしてくれたことに感謝。
|
||||
- [zaoqi123](https://github.com/zaoqi123) ミルクティーを買ってあげるのも良い考えですね。
|
||||
- [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。
|
||||
|
||||
|
||||
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう!
|
||||
|
||||
上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。
|
||||
|
||||
|
||||
@@ -6,73 +6,95 @@
|
||||
|
||||
**实验性! 使用风险自负!**
|
||||
|
||||
|
||||
>
|
||||
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann)
|
||||
> 但是,我们将会在未来成为一个单独维护的KSU分支
|
||||
>
|
||||
|
||||
> 但是,我们将会在未来成为一个单独维护的 KSU 分支
|
||||
|
||||
## 如何添加
|
||||
|
||||
在内核源码的根目录下执行以下命令:
|
||||
|
||||
使用 susfs-dev 分支(已集成susfs,带非GKI设备的支持)
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
|
||||
|
||||
使用 main 分支
|
||||
使用 main 分支 (不支持非GKI设备构建)
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||
```
|
||||
|
||||
使用支持非 GKI 设备的分支
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||
```
|
||||
|
||||
## 如何集成 susfs
|
||||
|
||||
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs
|
||||
|
||||
```
|
||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||
```
|
||||
|
||||
## 钩子方法
|
||||
|
||||
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
|
||||
|
||||
1. **KPROBES 钩子:**
|
||||
- 此方法仅支持 GKI 2.0 (5.10 - 6.x) 内核, 所有非 GKI 2.0 内核都必须使用手动钩子
|
||||
- 用于可加载内核模块 (LKM)
|
||||
- GKI 2.0 内核的默认钩子方法
|
||||
- 需要 `CONFIG_KPROBES=y`
|
||||
|
||||
2. **手动钩子:**
|
||||
- 对于 GKI 2.0 (5.10 - 6.x) 内核,需要在对应设备的 defconfig 文件中添加 `CONFIG_KSU_MANUAL_HOOK=y` 并确保使用 `#ifdef CONFIG_KSU_MANUAL_HOOK` 而不是 `#ifdef CONFIG_KSU` 来保护 KernelSU 钩子
|
||||
- 标准的 KernelSU 钩子:https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||
- backslashxx 的 syscall 手动钩子:https://github.com/backslashxx/KernelSU/issues/5
|
||||
- 部分手动集成 KPROBES 的非 GKI 2.0 设备不需要手动 VFS 钩子 `new_hook.patch` 补丁
|
||||
- 非 GKI 内核的默认挂钩方法
|
||||
- 需要 `CONFIG_KSU_MANUAL_HOOK=y`
|
||||
|
||||
## KPM 支持
|
||||
|
||||
## KPM支持
|
||||
|
||||
- 我们基于KernelPatch去掉了和KSU重复的功能,保留了KPM支持
|
||||
- 我们将会引入更多的兼容APatch的函数来确保KPM功能的完整性
|
||||
|
||||
- 我们基于 KernelPatch 去掉了和 KSU 重复的功能,仅保留了 KPM 支持
|
||||
- 我们将会引入更多的兼容 APatch 的函数来确保 KPM 功能的完整性
|
||||
|
||||
开源地址: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||
|
||||
KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
|
||||
KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
> [!Note]
|
||||
> 1. 需要 `CONFIG_KPM=y`
|
||||
> 2. 非GKI设备还需要 `CONFIG_KALLSYMS=y` 和 `CONFIG_KALLSYMS_ALL=y`
|
||||
> 3. 部分内核 `4.19` 以下源码还需要从 `4.19` 向后移植头文件 `set_memory.h`
|
||||
|
||||
|
||||
## 如何进行系统更新保留ROOT
|
||||
- OTA后先不要重启,进入管理器刷写/修补内核界面,找到 `GKI/non_GKI安装` 选择需要刷写的Anykernel3内核压缩文件,选择与现在系统运行槽位相反的槽位进行刷写并重启即可保留GKI模式更新(暂不支持所有非GKI设备使用这种方法,请自行尝试。非GKI设备使用TWRP刷写是最稳妥的)
|
||||
- 或者使用LKM模式的安装到未使用的槽位(OTA后)
|
||||
|
||||
## 兼容状态
|
||||
- KernelSU(v1.0.0 之前版本)正式支持 Android GKI 2.0 设备(内核 5.10+)
|
||||
|
||||
- 旧内核(4.4+)也兼容,但必须手动构建内核
|
||||
|
||||
- 通过更多的反向移植,KernelSU 可以支持 3.x 内核(3.4-3.18)
|
||||
|
||||
- 目前支持 `arm64-v8a` ,`armeabi-v7a (bare)` 和部分 `X86_64`
|
||||
|
||||
## 更多链接
|
||||
|
||||
基于 SukiSU 和 susfs 编译的项目
|
||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||
|
||||
|
||||
## 使用方法
|
||||
|
||||
### GKI
|
||||
1. 适用于如小米红米三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
||||
2. 找到更多链接里的 GKI 构建的项目找到设备内核版本直接下载用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
||||
3. 一般不带后缀的 .zip 压缩包是通用,gz 后缀的为天玑机型专用,lz4 后缀的为谷歌系机型专用,一般刷不带后缀的即可
|
||||
### 普适的 GKI
|
||||
|
||||
请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html
|
||||
|
||||
> [!Note]
|
||||
> 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
||||
> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来,用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
||||
> 3. 一般不带后缀的 .zip 压缩包是未压缩的,gz 后缀的为天玑机型所使用的压缩方式
|
||||
|
||||
|
||||
### 一加
|
||||
|
||||
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
|
||||
|
||||
> [!Note]
|
||||
@@ -80,7 +102,6 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
|
||||
> - 分支和配置文件请自行到一加内核开源地址进行填写
|
||||
|
||||
|
||||
## 特点
|
||||
|
||||
1. 基于内核的 `su` 和 root 访问管理
|
||||
@@ -88,8 +109,7 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
3. [App Profile](https://kernelsu.org/guide/app-profile.html):将 root 权限锁在笼子里
|
||||
4. 恢复对非 GKI 2.0 内核的支持
|
||||
5. 更多自定义功能
|
||||
6. 对KPM内核模块的支持
|
||||
|
||||
6. 对 KPM 内核模块的支持
|
||||
|
||||
## 许可证
|
||||
|
||||
@@ -97,13 +117,12 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
- 除 `kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
|
||||
|
||||
## 赞助名单
|
||||
|
||||
- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持
|
||||
- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错
|
||||
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
|
||||
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
|
||||
|
||||
|
||||
|
||||
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
||||
|
||||
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
||||
|
||||
@@ -117,4 +136,4 @@ KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
||||
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具
|
||||
- [genuine](https://github.com/brevent/genuine/):APK v2 签名验证
|
||||
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能
|
||||
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch是APatch实现内核模块的关键部分
|
||||
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch 是 APatch 实现内核模块的关键部分
|
||||
@@ -25,11 +25,11 @@ $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin
|
||||
KSU_GIT_VERSION := $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git rev-list --count main)
|
||||
# ksu_version: major * 10000 + git version + 606 for historical reasons
|
||||
$(eval KSU_VERSION=$(shell expr 10000 + $(KSU_GIT_VERSION) + 606))
|
||||
$(info -- SukiSU version: $(KSU_VERSION))
|
||||
$(info -- KernelSU version: $(KSU_VERSION))
|
||||
ccflags-y += -DKSU_VERSION=$(KSU_VERSION)
|
||||
else # If there is no .git file, the default version will be passed.
|
||||
$(warning "KSU_GIT_VERSION not defined! It is better to make SukiSU a git submodule!")
|
||||
ccflags-y += -DKSU_VERSION=12800
|
||||
$(warning "KSU_GIT_VERSION not defined! It is better to make KernelSU a git submodule!")
|
||||
ccflags-y += -DKSU_VERSION=16
|
||||
endif
|
||||
|
||||
ifndef KSU_EXPECTED_SIZE
|
||||
@@ -42,13 +42,26 @@ endif
|
||||
|
||||
ifdef KSU_MANAGER_PACKAGE
|
||||
ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\"
|
||||
$(info -- KernelSU Manager package name: $(KSU_MANAGER_PACKAGE))
|
||||
$(info -- SukiSU Manager package name: $(KSU_MANAGER_PACKAGE))
|
||||
endif
|
||||
|
||||
$(info -- KernelSU Manager signature size: $(KSU_EXPECTED_SIZE))
|
||||
$(info -- KernelSU Manager signature hash: $(KSU_EXPECTED_HASH))
|
||||
$(info -- SukiSU Manager signature size: $(KSU_EXPECTED_SIZE))
|
||||
$(info -- SukiSU Manager signature hash: $(KSU_EXPECTED_HASH))
|
||||
$(info -- Supported Unofficial Manager: 5ec1cff (GKI) ShirkNeko udochina (GKI and KPM))
|
||||
KERNEL_VERSION := $(VERSION).$(PATCHLEVEL)
|
||||
KERNEL_TYPE := Non-GKI
|
||||
# Check for GKI 2.0 (5.10+ or 6.x+)
|
||||
ifneq ($(shell test \( $(VERSION) -ge 5 -a $(PATCHLEVEL) -ge 10 \) -o $(VERSION) -ge 6; echo $$?),0)
|
||||
# Check for GKI 1.0 (5.4)
|
||||
ifeq ($(shell test $(VERSION)-$(PATCHLEVEL) = 5-4; echo $$?),0)
|
||||
KERNEL_TYPE := GKI 1.0
|
||||
endif
|
||||
else
|
||||
KERNEL_TYPE := GKI 2.0
|
||||
endif
|
||||
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
|
||||
$(info -- KERNEL_TYPE: $(KERNEL_TYPE))
|
||||
|
||||
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
|
||||
ifeq ($(CONFIG_KPM),y)
|
||||
$(info -- KPM is enabled)
|
||||
|
||||
@@ -63,6 +63,10 @@ u32 ksu_devpts_sid;
|
||||
// Detect whether it is on or not
|
||||
static bool is_boot_phase = true;
|
||||
|
||||
#ifdef CONFIG_COMPAT
|
||||
bool ksu_is_compat __read_mostly = false;
|
||||
#endif
|
||||
|
||||
void on_post_fs_data(void)
|
||||
{
|
||||
static bool done = false;
|
||||
@@ -107,6 +111,7 @@ static const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr)
|
||||
if (get_user(compat, argv.ptr.compat + nr))
|
||||
return ERR_PTR(-EFAULT);
|
||||
|
||||
ksu_is_compat = true;
|
||||
return compat_ptr(compat);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -137,17 +137,45 @@ void apply_kernelsu_rules()
|
||||
#define CMD_TYPE_CHANGE 8
|
||||
#define CMD_GENFSCON 9
|
||||
|
||||
#ifdef CONFIG_64BIT
|
||||
struct sepol_data {
|
||||
u32 cmd;
|
||||
u32 subcmd;
|
||||
char __user *sepol1;
|
||||
char __user *sepol2;
|
||||
char __user *sepol3;
|
||||
char __user *sepol4;
|
||||
char __user *sepol5;
|
||||
char __user *sepol6;
|
||||
char __user *sepol7;
|
||||
u64 field_sepol1;
|
||||
u64 field_sepol2;
|
||||
u64 field_sepol3;
|
||||
u64 field_sepol4;
|
||||
u64 field_sepol5;
|
||||
u64 field_sepol6;
|
||||
u64 field_sepol7;
|
||||
};
|
||||
#ifdef CONFIG_COMPAT
|
||||
extern bool ksu_is_compat __read_mostly;
|
||||
struct sepol_compat_data {
|
||||
u32 cmd;
|
||||
u32 subcmd;
|
||||
u32 field_sepol1;
|
||||
u32 field_sepol2;
|
||||
u32 field_sepol3;
|
||||
u32 field_sepol4;
|
||||
u32 field_sepol5;
|
||||
u32 field_sepol6;
|
||||
u32 field_sepol7;
|
||||
};
|
||||
#endif // CONFIG_COMPAT
|
||||
#else
|
||||
struct sepol_data {
|
||||
u32 cmd;
|
||||
u32 subcmd;
|
||||
u32 field_sepol1;
|
||||
u32 field_sepol2;
|
||||
u32 field_sepol3;
|
||||
u32 field_sepol4;
|
||||
u32 field_sepol5;
|
||||
u32 field_sepol6;
|
||||
u32 field_sepol7;
|
||||
};
|
||||
#endif // CONFIG_64BIT
|
||||
|
||||
static int get_object(char *buf, char __user *user_object, size_t buf_sz,
|
||||
char **object)
|
||||
@@ -192,14 +220,58 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
pr_info("SELinux permissive or disabled when handle policy!\n");
|
||||
}
|
||||
|
||||
u32 cmd, subcmd;
|
||||
char __user *sepol1, *sepol2, *sepol3, *sepol4, *sepol5, *sepol6, *sepol7;
|
||||
|
||||
#if defined(CONFIG_64BIT) && defined(CONFIG_COMPAT)
|
||||
if (unlikely(ksu_is_compat)) {
|
||||
struct sepol_compat_data compat_data;
|
||||
if (copy_from_user(&compat_data, arg4, sizeof(struct sepol_compat_data))) {
|
||||
pr_err("sepol: copy sepol_data failed.\n");
|
||||
return -1;
|
||||
}
|
||||
sepol1 = compat_ptr(compat_data.field_sepol1);
|
||||
sepol2 = compat_ptr(compat_data.field_sepol2);
|
||||
sepol3 = compat_ptr(compat_data.field_sepol3);
|
||||
sepol4 = compat_ptr(compat_data.field_sepol4);
|
||||
sepol5 = compat_ptr(compat_data.field_sepol5);
|
||||
sepol6 = compat_ptr(compat_data.field_sepol6);
|
||||
sepol7 = compat_ptr(compat_data.field_sepol7);
|
||||
cmd = compat_data.cmd;
|
||||
subcmd = compat_data.subcmd;
|
||||
} else {
|
||||
struct sepol_data data;
|
||||
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
||||
pr_err("sepol: copy sepol_data failed.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
u32 cmd = data.cmd;
|
||||
u32 subcmd = data.subcmd;
|
||||
sepol1 = data.field_sepol1;
|
||||
sepol2 = data.field_sepol2;
|
||||
sepol3 = data.field_sepol3;
|
||||
sepol4 = data.field_sepol4;
|
||||
sepol5 = data.field_sepol5;
|
||||
sepol6 = data.field_sepol6;
|
||||
sepol7 = data.field_sepol7;
|
||||
cmd = data.cmd;
|
||||
subcmd = data.subcmd;
|
||||
}
|
||||
#else
|
||||
// basically for full native, say (64BIT=y COMPAT=n) || (64BIT=n)
|
||||
struct sepol_data data;
|
||||
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
||||
pr_err("sepol: copy sepol_data failed.\n");
|
||||
return -1;
|
||||
}
|
||||
sepol1 = data.field_sepol1;
|
||||
sepol2 = data.field_sepol2;
|
||||
sepol3 = data.field_sepol3;
|
||||
sepol4 = data.field_sepol4;
|
||||
sepol5 = data.field_sepol5;
|
||||
sepol6 = data.field_sepol6;
|
||||
sepol7 = data.field_sepol7;
|
||||
cmd = data.cmd;
|
||||
subcmd = data.subcmd;
|
||||
#endif
|
||||
|
||||
rcu_read_lock();
|
||||
|
||||
@@ -213,22 +285,22 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char perm_buf[MAX_SEPOL_LEN];
|
||||
|
||||
char *s, *t, *c, *p;
|
||||
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) {
|
||||
if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||
if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||
pr_err("sepol: copy tgt failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) {
|
||||
if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) {
|
||||
pr_err("sepol: copy cls failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (get_object(perm_buf, data.sepol4, sizeof(perm_buf), &p) <
|
||||
if (get_object(perm_buf, sepol4, sizeof(perm_buf), &p) <
|
||||
0) {
|
||||
pr_err("sepol: copy perm failed.\n");
|
||||
goto exit;
|
||||
@@ -258,24 +330,24 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char perm_set[MAX_SEPOL_LEN];
|
||||
|
||||
char *s, *t, *c;
|
||||
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) {
|
||||
if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||
if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) {
|
||||
pr_err("sepol: copy tgt failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) {
|
||||
if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) {
|
||||
pr_err("sepol: copy cls failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(operation, data.sepol4,
|
||||
if (strncpy_from_user(operation, sepol4,
|
||||
sizeof(operation)) < 0) {
|
||||
pr_err("sepol: copy operation failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(perm_set, data.sepol5, sizeof(perm_set)) <
|
||||
if (strncpy_from_user(perm_set, sepol5, sizeof(perm_set)) <
|
||||
0) {
|
||||
pr_err("sepol: copy perm_set failed.\n");
|
||||
goto exit;
|
||||
@@ -295,7 +367,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
} else if (cmd == CMD_TYPE_STATE) {
|
||||
char src[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
||||
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
@@ -315,11 +387,11 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char type[MAX_SEPOL_LEN];
|
||||
char attr[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(type, data.sepol1, sizeof(type)) < 0) {
|
||||
if (strncpy_from_user(type, sepol1, sizeof(type)) < 0) {
|
||||
pr_err("sepol: copy type failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(attr, data.sepol2, sizeof(attr)) < 0) {
|
||||
if (strncpy_from_user(attr, sepol2, sizeof(attr)) < 0) {
|
||||
pr_err("sepol: copy attr failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
@@ -339,7 +411,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
} else if (cmd == CMD_ATTR) {
|
||||
char attr[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(attr, data.sepol1, sizeof(attr)) < 0) {
|
||||
if (strncpy_from_user(attr, sepol1, sizeof(attr)) < 0) {
|
||||
pr_err("sepol: copy attr failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
@@ -356,28 +428,28 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char default_type[MAX_SEPOL_LEN];
|
||||
char object[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
||||
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) {
|
||||
if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) {
|
||||
pr_err("sepol: copy tgt failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) {
|
||||
if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) {
|
||||
pr_err("sepol: copy cls failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(default_type, data.sepol4,
|
||||
if (strncpy_from_user(default_type, sepol4,
|
||||
sizeof(default_type)) < 0) {
|
||||
pr_err("sepol: copy default_type failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
char *real_object;
|
||||
if (data.sepol5 == NULL) {
|
||||
if (sepol5 == NULL) {
|
||||
real_object = NULL;
|
||||
} else {
|
||||
if (strncpy_from_user(object, data.sepol5,
|
||||
if (strncpy_from_user(object, sepol5,
|
||||
sizeof(object)) < 0) {
|
||||
pr_err("sepol: copy object failed.\n");
|
||||
goto exit;
|
||||
@@ -396,19 +468,19 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char cls[MAX_SEPOL_LEN];
|
||||
char default_type[MAX_SEPOL_LEN];
|
||||
|
||||
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
|
||||
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
|
||||
pr_err("sepol: copy src failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) {
|
||||
if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) {
|
||||
pr_err("sepol: copy tgt failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) {
|
||||
if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) {
|
||||
pr_err("sepol: copy cls failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(default_type, data.sepol4,
|
||||
if (strncpy_from_user(default_type, sepol4,
|
||||
sizeof(default_type)) < 0) {
|
||||
pr_err("sepol: copy default_type failed.\n");
|
||||
goto exit;
|
||||
@@ -429,15 +501,15 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||
char name[MAX_SEPOL_LEN];
|
||||
char path[MAX_SEPOL_LEN];
|
||||
char context[MAX_SEPOL_LEN];
|
||||
if (strncpy_from_user(name, data.sepol1, sizeof(name)) < 0) {
|
||||
if (strncpy_from_user(name, sepol1, sizeof(name)) < 0) {
|
||||
pr_err("sepol: copy name failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(path, data.sepol2, sizeof(path)) < 0) {
|
||||
if (strncpy_from_user(path, sepol2, sizeof(path)) < 0) {
|
||||
pr_err("sepol: copy path failed.\n");
|
||||
goto exit;
|
||||
}
|
||||
if (strncpy_from_user(context, data.sepol3, sizeof(context)) <
|
||||
if (strncpy_from_user(context, sepol3, sizeof(context)) <
|
||||
0) {
|
||||
pr_err("sepol: copy context failed.\n");
|
||||
goto exit;
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
|
||||
extern void escape_to_root();
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
static bool ksu_sucompat_non_kp __read_mostly = true;
|
||||
#endif
|
||||
|
||||
static void __user *userspace_stack_buffer(const void *d, size_t len)
|
||||
{
|
||||
/* To avoid having to mmap a page in userspace, just write below the stack
|
||||
@@ -50,6 +54,12 @@ int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,
|
||||
{
|
||||
const char su[] = SU_PATH;
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_sucompat_non_kp) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -71,6 +81,11 @@ int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
|
||||
// const char sh[] = SH_PATH;
|
||||
const char su[] = SU_PATH;
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_sucompat_non_kp) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -115,6 +130,11 @@ int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
|
||||
const char sh[] = KSUD_PATH;
|
||||
const char su[] = SU_PATH;
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_sucompat_non_kp) {
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (unlikely(!filename_ptr))
|
||||
return 0;
|
||||
|
||||
@@ -144,6 +164,11 @@ int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
|
||||
const char su[] = SU_PATH;
|
||||
char path[sizeof(su) + 1];
|
||||
|
||||
#ifndef CONFIG_KPROBES
|
||||
if (!ksu_sucompat_non_kp){
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
if (unlikely(!filename_user))
|
||||
return 0;
|
||||
|
||||
@@ -237,6 +262,9 @@ void ksu_sucompat_init()
|
||||
su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre);
|
||||
su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre);
|
||||
su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_handler_pre);
|
||||
#else
|
||||
ksu_sucompat_non_kp = true;
|
||||
pr_info("ksu_sucompat_init: hooks enabled: execve/execveat_su, faccessat, stat\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -246,5 +274,8 @@ void ksu_sucompat_exit()
|
||||
for (int i = 0; i < ARRAY_SIZE(su_kps); i++) {
|
||||
destroy_kprobe(&su_kps[i]);
|
||||
}
|
||||
#else
|
||||
ksu_sucompat_non_kp = false;
|
||||
pr_info("ksu_sucompat_exit: hooks disabled: execve/execveat_su, faccessat, stat\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -358,12 +358,14 @@ void track_throne()
|
||||
if (ksu_is_manager_uid_valid()) {
|
||||
pr_info("manager is uninstalled, invalidate it!\n");
|
||||
ksu_invalidate_manager_uid();
|
||||
goto prune;
|
||||
}
|
||||
pr_info("Searching manager...\n");
|
||||
search_manager("/data/app", 2, &uid_list);
|
||||
pr_info("Search manager finished\n");
|
||||
}
|
||||
|
||||
prune:
|
||||
// then prune the allowlist
|
||||
ksu_prune_allowlist(is_uid_exist, &uid_list);
|
||||
out:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@file:Suppress("UnstableApiUsage")
|
||||
|
||||
import com.android.build.api.dsl.ApkSigningConfig
|
||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
import com.android.build.gradle.tasks.PackageAndroidArtifact
|
||||
|
||||
@@ -24,8 +25,18 @@ apksign {
|
||||
keyPasswordProperty = "KEY_PASSWORD"
|
||||
}
|
||||
|
||||
|
||||
android {
|
||||
namespace = "zako.zako.zako"
|
||||
|
||||
/**signingConfigs {
|
||||
create("Debug") {
|
||||
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
|
||||
storePassword = ""
|
||||
keyAlias = ""
|
||||
keyPassword = ""
|
||||
}
|
||||
}**/
|
||||
namespace = "com.sukisu.ultra"
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -33,6 +44,9 @@ android {
|
||||
isShrinkResources = true
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
/**debug {
|
||||
signingConfig = signingConfigs.named("Debug").get() as ApkSigningConfig
|
||||
}**/
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
@@ -140,4 +154,9 @@ dependencies {
|
||||
|
||||
implementation(libs.com.github.topjohnwu.libsu.core)
|
||||
|
||||
implementation(libs.mmrl.platform)
|
||||
compileOnly(libs.mmrl.hidden.api)
|
||||
implementation(libs.mmrl.webui)
|
||||
implementation(libs.mmrl.ui)
|
||||
|
||||
}
|
||||
47
manager/app/proguard-rules.pro
vendored
47
manager/app/proguard-rules.pro
vendored
@@ -0,0 +1,47 @@
|
||||
-verbose
|
||||
-optimizationpasses 5
|
||||
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn kotlinx.serialization.**
|
||||
|
||||
# Please add these rules to your existing keep rules in order to suppress warnings.
|
||||
# This is generated automatically by the Android Gradle plugin.
|
||||
-dontwarn com.google.auto.service.AutoService
|
||||
-dontwarn com.google.j2objc.annotations.RetainedWith
|
||||
-dontwarn javax.lang.model.SourceVersion
|
||||
-dontwarn javax.lang.model.element.AnnotationMirror
|
||||
-dontwarn javax.lang.model.element.AnnotationValue
|
||||
-dontwarn javax.lang.model.element.Element
|
||||
-dontwarn javax.lang.model.element.ElementKind
|
||||
-dontwarn javax.lang.model.element.ElementVisitor
|
||||
-dontwarn javax.lang.model.element.ExecutableElement
|
||||
-dontwarn javax.lang.model.element.Modifier
|
||||
-dontwarn javax.lang.model.element.Name
|
||||
-dontwarn javax.lang.model.element.PackageElement
|
||||
-dontwarn javax.lang.model.element.TypeElement
|
||||
-dontwarn javax.lang.model.element.TypeParameterElement
|
||||
-dontwarn javax.lang.model.element.VariableElement
|
||||
-dontwarn javax.lang.model.type.ArrayType
|
||||
-dontwarn javax.lang.model.type.DeclaredType
|
||||
-dontwarn javax.lang.model.type.ExecutableType
|
||||
-dontwarn javax.lang.model.type.TypeKind
|
||||
-dontwarn javax.lang.model.type.TypeMirror
|
||||
-dontwarn javax.lang.model.type.TypeVariable
|
||||
-dontwarn javax.lang.model.type.TypeVisitor
|
||||
-dontwarn javax.lang.model.util.AbstractAnnotationValueVisitor8
|
||||
-dontwarn javax.lang.model.util.AbstractTypeVisitor8
|
||||
-dontwarn javax.lang.model.util.ElementFilter
|
||||
-dontwarn javax.lang.model.util.Elements
|
||||
-dontwarn javax.lang.model.util.SimpleElementVisitor8
|
||||
-dontwarn javax.lang.model.util.SimpleTypeVisitor7
|
||||
-dontwarn javax.lang.model.util.SimpleTypeVisitor8
|
||||
-dontwarn javax.lang.model.util.Types
|
||||
-dontwarn javax.tools.Diagnostic$Kind
|
||||
|
||||
|
||||
# MMRL:webui reflection
|
||||
-keep class com.dergoogler.mmrl.webui.model.ModId { *; }
|
||||
-keep class com.dergoogler.mmrl.webui.interfaces.** { *; }
|
||||
-keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; }
|
||||
|
||||
-keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; }
|
||||
@@ -3,8 +3,10 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
|
||||
<application
|
||||
@@ -37,6 +39,13 @@
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.KernelSU.WebUI" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.webui.WebUIXActivity"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:documentLaunchMode="intoExisting"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.KernelSU.WebUI" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako;
|
||||
package com.sukisu.zako;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import rikka.parcelablelist.ParcelableListSlice;
|
||||
BIN
manager/app/src/main/assets/5_15+-mkbootfs
Normal file
BIN
manager/app/src/main/assets/5_15+-mkbootfs
Normal file
Binary file not shown.
@@ -12,7 +12,7 @@
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
|
||||
Java_com_sukisu_ultra_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
|
||||
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
|
||||
auto result = become_manager(cpkg);
|
||||
env->ReleaseStringUTFChars(pkg, cpkg);
|
||||
@@ -21,13 +21,13 @@ Java_zako_zako_zako_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_zako_zako_zako_Natives_getVersion(JNIEnv *env, jobject) {
|
||||
Java_com_sukisu_ultra_Natives_getVersion(JNIEnv *env, jobject) {
|
||||
return get_version();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_zako_zako_zako_Natives_getAllowList(JNIEnv *env, jobject) {
|
||||
Java_com_sukisu_ultra_Natives_getAllowList(JNIEnv *env, jobject) {
|
||||
int uids[1024];
|
||||
int size = 0;
|
||||
bool result = get_allow_list(uids, &size);
|
||||
@@ -42,13 +42,13 @@ Java_zako_zako_zako_Natives_getAllowList(JNIEnv *env, jobject) {
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
|
||||
Java_com_sukisu_ultra_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
|
||||
return is_safe_mode();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
|
||||
Java_com_sukisu_ultra_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
|
||||
return is_lkm_mode();
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_zako_zako_zako_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
|
||||
Java_com_sukisu_ultra_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
|
||||
if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -129,7 +129,7 @@ Java_zako_zako_zako_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jin
|
||||
|
||||
bool useDefaultProfile = !get_app_profile(key, &profile);
|
||||
|
||||
auto cls = env->FindClass("zako/zako/zako/Natives$Profile");
|
||||
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
|
||||
auto constructor = env->GetMethodID(cls, "<init>", "()V");
|
||||
auto obj = env->NewObject(cls, constructor);
|
||||
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
||||
@@ -207,8 +207,8 @@ Java_zako_zako_zako_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jin
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
|
||||
auto cls = env->FindClass("zako/zako/zako/Natives$Profile");
|
||||
Java_com_sukisu_ultra_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
|
||||
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
|
||||
|
||||
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
||||
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
|
||||
@@ -293,16 +293,16 @@ Java_zako_zako_zako_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject pr
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
|
||||
Java_com_sukisu_ultra_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
|
||||
return uid_should_umount(uid);
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
|
||||
Java_com_sukisu_ultra_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
|
||||
return is_su_enabled();
|
||||
}
|
||||
extern "C"
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_zako_zako_zako_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
|
||||
Java_com_sukisu_ultra_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
|
||||
return set_su_enabled(enabled);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.sukisu.ultra
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import com.dergoogler.mmrl.platform.Platform
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
|
||||
lateinit var ksuApp: KernelSUApplication
|
||||
|
||||
class KernelSUApplication : Application() {
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
val prefs = base.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||
|
||||
var context = base
|
||||
if (languageCode.isNotEmpty()) {
|
||||
val locale = Locale.forLanguageTag(languageCode)
|
||||
Locale.setDefault(locale)
|
||||
|
||||
val config = Configuration(base.resources.configuration)
|
||||
config.setLocale(locale)
|
||||
|
||||
context = base.createConfigurationContext(config)
|
||||
}
|
||||
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
override fun getResources(): Resources {
|
||||
val resources = super.getResources()
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||
|
||||
if (languageCode.isNotEmpty()) {
|
||||
val locale = Locale.forLanguageTag(languageCode)
|
||||
val config = Configuration(resources.configuration)
|
||||
config.setLocale(locale)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
return createConfigurationContext(config).resources
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
resources.updateConfiguration(config, resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ksuApp = this
|
||||
|
||||
Platform.setHiddenApiExemptions()
|
||||
|
||||
val context = this
|
||||
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
||||
Coil.setImageLoader(
|
||||
ImageLoader.Builder(context)
|
||||
.components {
|
||||
add(AppIconKeyer())
|
||||
add(AppIconFetcher.Factory(iconSize, false, context))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
|
||||
val webroot = File(dataDir, "webroot")
|
||||
if (!webroot.exists()) {
|
||||
webroot.mkdir()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
applyLanguageSetting()
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
private fun applyLanguageSetting() {
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||
|
||||
if (languageCode.isNotEmpty()) {
|
||||
val locale = Locale.forLanguageTag(languageCode)
|
||||
Locale.setDefault(locale)
|
||||
|
||||
val resources = resources
|
||||
val config = Configuration(resources.configuration)
|
||||
config.setLocale(locale)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
createConfigurationContext(config)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
resources.updateConfiguration(config, resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako
|
||||
package com.sukisu.ultra
|
||||
|
||||
import android.system.Os
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako
|
||||
package com.sukisu.ultra
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.Keep
|
||||
446
manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt
Normal file
446
manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt
Normal file
@@ -0,0 +1,446 @@
|
||||
package com.sukisu.ultra.flash
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Error
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.utils.AssetsUtil
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
data class FlashState(
|
||||
val isFlashing: Boolean = false,
|
||||
val isCompleted: Boolean = false,
|
||||
val progress: Float = 0f,
|
||||
val currentStep: String = "",
|
||||
val logs: List<String> = emptyList(),
|
||||
val error: String = ""
|
||||
)
|
||||
|
||||
class HorizonKernelState {
|
||||
private val _state = MutableStateFlow(FlashState())
|
||||
val state: StateFlow<FlashState> = _state.asStateFlow()
|
||||
|
||||
fun updateProgress(progress: Float) {
|
||||
_state.update { it.copy(progress = progress) }
|
||||
}
|
||||
|
||||
fun updateStep(step: String) {
|
||||
_state.update { it.copy(currentStep = step) }
|
||||
}
|
||||
|
||||
fun addLog(log: String) {
|
||||
_state.update {
|
||||
it.copy(logs = it.logs + log)
|
||||
}
|
||||
}
|
||||
|
||||
fun setError(error: String) {
|
||||
_state.update { it.copy(error = error) }
|
||||
}
|
||||
|
||||
fun startFlashing() {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isFlashing = true,
|
||||
isCompleted = false,
|
||||
progress = 0f,
|
||||
currentStep = "under preparation...",
|
||||
logs = emptyList(),
|
||||
error = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun completeFlashing() {
|
||||
_state.update { it.copy(isCompleted = true, progress = 1f) }
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
_state.value = FlashState()
|
||||
}
|
||||
}
|
||||
|
||||
class HorizonKernelWorker(
|
||||
private val context: Context,
|
||||
private val state: HorizonKernelState,
|
||||
private val slot: String? = null
|
||||
) : Thread() {
|
||||
var uri: Uri? = null
|
||||
private lateinit var filePath: String
|
||||
private lateinit var binaryPath: String
|
||||
|
||||
private var onFlashComplete: (() -> Unit)? = null
|
||||
private var originalSlot: String? = null
|
||||
|
||||
fun setOnFlashCompleteListener(listener: () -> Unit) {
|
||||
onFlashComplete = listener
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
state.startFlashing()
|
||||
state.updateStep(context.getString(R.string.horizon_preparing))
|
||||
|
||||
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
|
||||
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
|
||||
|
||||
try {
|
||||
state.updateStep(context.getString(R.string.horizon_cleaning_files))
|
||||
state.updateProgress(0.1f)
|
||||
cleanup()
|
||||
|
||||
if (!rootAvailable()) {
|
||||
state.setError(context.getString(R.string.root_required))
|
||||
return
|
||||
}
|
||||
|
||||
state.updateStep(context.getString(R.string.horizon_copying_files))
|
||||
state.updateProgress(0.2f)
|
||||
copy()
|
||||
|
||||
if (!File(filePath).exists()) {
|
||||
state.setError(context.getString(R.string.horizon_copy_failed))
|
||||
return
|
||||
}
|
||||
|
||||
state.updateStep(context.getString(R.string.horizon_extracting_tool))
|
||||
state.updateProgress(0.4f)
|
||||
getBinary()
|
||||
|
||||
state.updateStep(context.getString(R.string.horizon_patching_script))
|
||||
state.updateProgress(0.6f)
|
||||
patch()
|
||||
|
||||
state.updateStep(context.getString(R.string.horizon_flashing))
|
||||
state.updateProgress(0.7f)
|
||||
|
||||
val isAbDevice = isAbDevice()
|
||||
|
||||
if (isAbDevice && slot != null) {
|
||||
state.updateStep(context.getString(R.string.horizon_getting_original_slot))
|
||||
state.updateProgress(0.72f)
|
||||
originalSlot = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
|
||||
|
||||
state.updateStep(context.getString(R.string.horizon_setting_target_slot))
|
||||
state.updateProgress(0.74f)
|
||||
runCommand(true, "resetprop -n ro.boot.slot_suffix _$slot")
|
||||
}
|
||||
|
||||
flash()
|
||||
|
||||
if (isAbDevice && !originalSlot.isNullOrEmpty()) {
|
||||
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
||||
state.updateProgress(0.8f)
|
||||
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
||||
}
|
||||
|
||||
state.updateStep(context.getString(R.string.horizon_flash_complete_status))
|
||||
state.completeFlashing()
|
||||
|
||||
(context as? Activity)?.runOnUiThread {
|
||||
onFlashComplete?.invoke()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state.setError(e.message ?: context.getString(R.string.horizon_unknown_error))
|
||||
|
||||
if (isAbDevice() && !originalSlot.isNullOrEmpty()) {
|
||||
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
||||
state.updateProgress(0.8f)
|
||||
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查设备是否为AB分区设备
|
||||
private fun isAbDevice(): Boolean {
|
||||
val abUpdate = runCommandGetOutput(true, "getprop ro.build.ab_update")?.trim() ?: ""
|
||||
if (abUpdate.equals("false", ignoreCase = true) || abUpdate.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val slotSuffix = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
|
||||
return !slotSuffix.isNullOrEmpty()
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
||||
}
|
||||
|
||||
private fun copy() {
|
||||
uri?.let { safeUri ->
|
||||
context.contentResolver.openInputStream(safeUri)?.use { input ->
|
||||
FileOutputStream(File(filePath)).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBinary() {
|
||||
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
|
||||
if (!File(binaryPath).exists()) {
|
||||
throw IOException("Failed to extract update-binary")
|
||||
}
|
||||
}
|
||||
|
||||
private fun patch() {
|
||||
val kernelVersion = runCommandGetOutput(true, "cat /proc/version")
|
||||
val versionRegex = """\d+\.\d+\.\d+""".toRegex()
|
||||
val version = kernelVersion?.let { versionRegex.find(it) }?.value ?: ""
|
||||
val toolName = if (version.isNotEmpty()) {
|
||||
val parts = version.split('.')
|
||||
if (parts.size >= 2) {
|
||||
val major = parts[0].toIntOrNull() ?: 0
|
||||
val minor = parts[1].toIntOrNull() ?: 0
|
||||
if (major < 5 || (major == 5 && minor <= 10)) "5_10" else "5_15+"
|
||||
} else {
|
||||
"5_15+"
|
||||
}
|
||||
} else {
|
||||
"5_15+"
|
||||
}
|
||||
val toolPath = "${context.filesDir.absolutePath}/mkbootfs"
|
||||
AssetsUtil.exportFiles(context, "$toolName-mkbootfs", toolPath)
|
||||
state.addLog("${context.getString(R.string.kernel_version_log, version)} ${context.getString(R.string.tool_version_log, toolName)}")
|
||||
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $toolPath \$AKHOME/tools;' $binaryPath")
|
||||
}
|
||||
|
||||
private fun flash() {
|
||||
val process = ProcessBuilder("su")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
|
||||
try {
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
|
||||
|
||||
// 写入槽位信息到临时文件
|
||||
slot?.let { selectedSlot ->
|
||||
writer.write("echo \"$selectedSlot\" > ${context.filesDir.absolutePath}/bootslot\n")
|
||||
}
|
||||
|
||||
// 构建刷写命令
|
||||
val flashCommand = buildString {
|
||||
append("sh $binaryPath 3 1 \"$filePath\"")
|
||||
if (slot != null) {
|
||||
append(" \"$(cat ${context.filesDir.absolutePath}/bootslot)\"")
|
||||
}
|
||||
append(" && touch ${context.filesDir.absolutePath}/done\n")
|
||||
}
|
||||
|
||||
writer.write(flashCommand)
|
||||
writer.write("exit\n")
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
process.inputStream.bufferedReader().use { reader ->
|
||||
reader.lineSequence().forEach { line ->
|
||||
if (line.startsWith("ui_print")) {
|
||||
val logMessage = line.removePrefix("ui_print").trim()
|
||||
state.addLog(logMessage)
|
||||
|
||||
when {
|
||||
logMessage.contains("extracting", ignoreCase = true) -> {
|
||||
state.updateProgress(0.75f)
|
||||
}
|
||||
logMessage.contains("installing", ignoreCase = true) -> {
|
||||
state.updateProgress(0.85f)
|
||||
}
|
||||
logMessage.contains("complete", ignoreCase = true) -> {
|
||||
state.updateProgress(0.95f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
process.destroy()
|
||||
}
|
||||
|
||||
if (!File("${context.filesDir.absolutePath}/done").exists()) {
|
||||
throw IOException(context.getString(R.string.flash_failed_message))
|
||||
}
|
||||
}
|
||||
|
||||
private fun runCommand(su: Boolean, cmd: String): Int {
|
||||
val process = ProcessBuilder(if (su) "su" else "sh")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
|
||||
return try {
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("$cmd\n")
|
||||
writer.write("exit\n")
|
||||
writer.flush()
|
||||
}
|
||||
process.waitFor()
|
||||
} finally {
|
||||
process.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun runCommandGetOutput(su: Boolean, cmd: String): String? {
|
||||
val process = ProcessBuilder(if (su) "su" else "sh")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
|
||||
return try {
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("$cmd\n")
|
||||
writer.write("exit\n")
|
||||
writer.flush()
|
||||
}
|
||||
process.inputStream.bufferedReader().use { reader ->
|
||||
reader.readText().trim()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
} finally {
|
||||
process.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun rootAvailable(): Boolean {
|
||||
return try {
|
||||
val process = Runtime.getRuntime().exec("su -c id")
|
||||
val exitValue = process.waitFor()
|
||||
exitValue == 0
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HorizonKernelFlashProgress(state: FlashState) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.horizon_flash_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
progress = { state.progress },
|
||||
)
|
||||
|
||||
Text(
|
||||
text = state.currentStep,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
|
||||
if (state.logs.isNotEmpty()) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.horizon_logs_label),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Start)
|
||||
.padding(top = 8.dp, bottom = 4.dp)
|
||||
)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 230.dp)
|
||||
.padding(vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 1.dp,
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
state.logs.forEach { log ->
|
||||
Text(
|
||||
text = log,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier.padding(vertical = 2.dp),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.error.isNotEmpty()) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Error,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = state.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
} else if (state.isCompleted) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.horizon_flash_complete),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.profile
|
||||
package com.sukisu.ultra.profile
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.profile
|
||||
package com.sukisu.ultra.profile
|
||||
|
||||
/**
|
||||
* https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/private/android_filesystem_config.h
|
||||
333
manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt
Normal file
333
manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt
Normal file
@@ -0,0 +1,333 @@
|
||||
package com.sukisu.ultra.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.database.ContentObserver
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
|
||||
import com.ramcosta.composedestinations.spec.RouteOrDirection
|
||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||
import io.sukisu.ultra.UltraToolInstall
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
||||
import com.sukisu.ultra.ui.theme.*
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import androidx.core.content.edit
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import com.sukisu.ultra.ui.webui.initPlatform
|
||||
import java.util.Locale
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private inner class ThemeChangeContentObserver(
|
||||
handler: Handler,
|
||||
private val onThemeChanged: () -> Unit
|
||||
) : ContentObserver(handler) {
|
||||
override fun onChange(selfChange: Boolean) {
|
||||
super.onChange(selfChange)
|
||||
onThemeChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// 应用保存的语言设置
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
private fun applyLanguageSetting() {
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||
|
||||
if (languageCode.isNotEmpty()) {
|
||||
val locale = Locale.forLanguageTag(languageCode)
|
||||
Locale.setDefault(locale)
|
||||
|
||||
val resources = resources
|
||||
val config = Configuration(resources.configuration)
|
||||
config.setLocale(locale)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
createConfigurationContext(config)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
resources.updateConfiguration(config, resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
val prefs = newBase.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||
|
||||
var context = newBase
|
||||
if (languageCode.isNotEmpty()) {
|
||||
val locale = Locale.forLanguageTag(languageCode)
|
||||
Locale.setDefault(locale)
|
||||
|
||||
val config = Configuration(newBase.resources.configuration)
|
||||
config.setLocale(locale)
|
||||
context = newBase.createConfigurationContext(config)
|
||||
}
|
||||
|
||||
super.attachBaseContext(context)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// 确保应用正确的语言设置
|
||||
applyLanguageSetting()
|
||||
|
||||
applyCustomDpi()
|
||||
|
||||
// Enable edge to edge
|
||||
enableEdgeToEdge()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
||||
|
||||
if (isFirstRun) {
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
}
|
||||
prefs.edit { putBoolean("is_first_run", false) }
|
||||
}
|
||||
|
||||
// 加载保存的背景设置
|
||||
loadThemeMode()
|
||||
loadThemeColors()
|
||||
loadDynamicColorState()
|
||||
CardConfig.load(applicationContext)
|
||||
|
||||
val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) {
|
||||
runOnUiThread {
|
||||
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
loadCustomBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentResolver.registerContentObserver(
|
||||
android.provider.Settings.System.getUriFor("ui_night_mode"),
|
||||
false,
|
||||
contentObserver
|
||||
)
|
||||
|
||||
val destroyListeners = mutableListOf<() -> Unit>()
|
||||
destroyListeners.add {
|
||||
contentResolver.unregisterContentObserver(contentObserver)
|
||||
}
|
||||
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
if (isManager) {
|
||||
install()
|
||||
UltraToolInstall.tryToInstall()
|
||||
}
|
||||
|
||||
setContent {
|
||||
KernelSUTheme {
|
||||
val navController = rememberNavController()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
|
||||
// pre-init platform to faster start WebUI X activities
|
||||
LaunchedEffect(Unit) {
|
||||
initPlatform()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = { BottomBar(navController) },
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||
) { innerPadding ->
|
||||
CompositionLocalProvider(
|
||||
LocalSnackbarHost provides snackBarHostState
|
||||
) {
|
||||
DestinationsNavHost(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
navGraph = NavGraphs.root as NavHostGraphSpec,
|
||||
navController = navController,
|
||||
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
|
||||
get() = { fadeIn(animationSpec = tween(340)) }
|
||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
|
||||
get() = { fadeOut(animationSpec = tween(340)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应用自定义DPI设置
|
||||
private fun applyCustomDpi() {
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val customDpi = prefs.getInt("app_dpi", 0)
|
||||
|
||||
if (customDpi > 0) {
|
||||
try {
|
||||
val resources = resources
|
||||
val metrics = resources.displayMetrics
|
||||
metrics.density = customDpi / 160f
|
||||
@Suppress("DEPRECATION")
|
||||
metrics.scaledDensity = customDpi / 160f
|
||||
metrics.densityDpi = customDpi
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
CardConfig.save(applicationContext)
|
||||
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
|
||||
putBoolean("prevent_background_refresh", true)
|
||||
}
|
||||
ThemeConfig.preventBackgroundRefresh = true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
applyLanguageSetting()
|
||||
|
||||
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||
loadCustomBackground()
|
||||
}
|
||||
}
|
||||
|
||||
private val destroyListeners = mutableListOf<() -> Unit>()
|
||||
|
||||
override fun onDestroy() {
|
||||
destroyListeners.forEach { it() }
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
applyLanguageSetting()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BottomBar(navController: NavHostController) {
|
||||
val navigator = navController.rememberDestinationsNavigator()
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
||||
val kpmVersion = getKpmVersion()
|
||||
val containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
|
||||
// 检查是否显示KPM
|
||||
val showKpmInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("show_kpm_info", true)
|
||||
|
||||
NavigationBar(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
|
||||
),
|
||||
containerColor = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = containerColor.copy(alpha = cardAlpha)
|
||||
).containerColor,
|
||||
tonalElevation = cardElevation
|
||||
) {
|
||||
BottomBarDestination.entries.forEach { destination ->
|
||||
if (destination == BottomBarDestination.Kpm) {
|
||||
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error") && showKpmInfo) {
|
||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (!isCurrentDestOnBackStack) {
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = if (isCurrentDestOnBackStack) {
|
||||
destination.iconSelected
|
||||
} else {
|
||||
destination.iconNotSelected
|
||||
},
|
||||
contentDescription = stringResource(destination.label),
|
||||
tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(destination.label),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (!isCurrentDestOnBackStack) {
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = if (isCurrentDestOnBackStack) {
|
||||
destination.iconSelected
|
||||
} else {
|
||||
destination.iconNotSelected
|
||||
},
|
||||
contentDescription = stringResource(destination.label),
|
||||
tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(destination.label),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -31,8 +31,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import zako.zako.zako.BuildConfig
|
||||
import zako.zako.zako.R
|
||||
import com.sukisu.ultra.BuildConfig
|
||||
import com.sukisu.ultra.R
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.graphics.text.LineBreaker
|
||||
import android.os.Build
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Fullscreen
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
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.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
fun ImageEditorDialog(
|
||||
imageUri: Uri,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: (Uri) -> Unit
|
||||
) {
|
||||
var scale by remember { mutableFloatStateOf(1f) }
|
||||
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var lastScale by remember { mutableFloatStateOf(1f) }
|
||||
var lastOffsetX by remember { mutableFloatStateOf(0f) }
|
||||
var lastOffsetY by remember { mutableFloatStateOf(0f) }
|
||||
var imageSize by remember { mutableStateOf(Size.Zero) }
|
||||
var screenSize by remember { mutableStateOf(Size.Zero) }
|
||||
val animatedScale by animateFloatAsState(
|
||||
targetValue = scale,
|
||||
label = "ScaleAnimation"
|
||||
)
|
||||
val animatedOffsetX by animateFloatAsState(
|
||||
targetValue = offsetX,
|
||||
label = "OffsetXAnimation"
|
||||
)
|
||||
val animatedOffsetY by animateFloatAsState(
|
||||
targetValue = offsetY,
|
||||
label = "OffsetYAnimation"
|
||||
)
|
||||
val updateTransformation = remember {
|
||||
{ newScale: Float, newOffsetX: Float, newOffsetY: Float ->
|
||||
val scaleDiff = kotlin.math.abs(newScale - lastScale)
|
||||
val offsetXDiff = kotlin.math.abs(newOffsetX - lastOffsetX)
|
||||
val offsetYDiff = kotlin.math.abs(newOffsetY - lastOffsetY)
|
||||
if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) {
|
||||
scale = newScale
|
||||
offsetX = newOffsetX
|
||||
offsetY = newOffsetY
|
||||
lastScale = newScale
|
||||
lastOffsetX = newOffsetX
|
||||
lastOffsetY = newOffsetY
|
||||
}
|
||||
}
|
||||
}
|
||||
val scaleToFullScreen = remember {
|
||||
{
|
||||
if (imageSize.height > 0 && screenSize.height > 0) {
|
||||
val newScale = screenSize.height / imageSize.height
|
||||
updateTransformation(newScale, 0f, 0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = true,
|
||||
dismissOnClickOutside = false,
|
||||
usePlatformDefaultWidth = false
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black.copy(alpha = 0.9f))
|
||||
.onSizeChanged { size ->
|
||||
screenSize = Size(size.width.toFloat(), size.height.toFloat())
|
||||
}
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(imageUri)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = stringResource(R.string.settings_custom_background),
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer(
|
||||
scaleX = animatedScale,
|
||||
scaleY = animatedScale,
|
||||
translationX = animatedOffsetX,
|
||||
translationY = animatedOffsetY
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTransformGestures { _, pan, zoom, _ ->
|
||||
scope.launch {
|
||||
try {
|
||||
val newScale = (scale * zoom).coerceIn(0.5f, 3f)
|
||||
val maxOffsetX = max(0f, size.width * (newScale - 1) / 2)
|
||||
val maxOffsetY = max(0f, size.height * (newScale - 1) / 2)
|
||||
val newOffsetX = if (maxOffsetX > 0) {
|
||||
(offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
val newOffsetY = if (maxOffsetY > 0) {
|
||||
(offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY)
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
updateTransformation(newScale, newOffsetX, newOffsetY)
|
||||
} catch (e: Exception) {
|
||||
updateTransformation(lastScale, lastOffsetX, lastOffsetY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onSizeChanged { size ->
|
||||
imageSize = Size(size.width.toFloat(), size.height.toFloat())
|
||||
}
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.align(Alignment.TopCenter),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.cancel),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = { scaleToFullScreen() },
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Fullscreen,
|
||||
contentDescription = stringResource(R.string.reprovision),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
try {
|
||||
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
||||
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
||||
savedUri?.let { onConfirm(it) }
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.confirm),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(Color.Black.copy(alpha = 0.6f))
|
||||
.padding(16.dp)
|
||||
.align(Alignment.BottomCenter)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.image_editor_hint),
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
|
||||
@Composable
|
||||
fun KsuIsValid(
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val ksuVersion = if (isManager) Natives.version else null
|
||||
|
||||
if (ksuVersion != null) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
@@ -42,7 +42,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import zako.zako.zako.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
|
||||
private const val TAG = "SearchBar"
|
||||
|
||||
@@ -63,7 +63,7 @@ fun SearchAppBar(
|
||||
var onSearch by remember { mutableStateOf(false) }
|
||||
|
||||
// 获取卡片颜色和透明度
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
if (onSearch) {
|
||||
@@ -1,18 +1,22 @@
|
||||
package zako.zako.zako.ui.component
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import com.dergoogler.mmrl.ui.component.LabelItem
|
||||
import com.dergoogler.mmrl.ui.component.text.TextRow
|
||||
|
||||
@Composable
|
||||
fun SwitchItem(
|
||||
@@ -21,9 +25,11 @@ fun SwitchItem(
|
||||
summary: String? = null,
|
||||
checked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
beta: Boolean = false,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val stateAlpha = remember(checked, enabled) { Modifier.alpha(if (enabled) 1f else 0.5f) }
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
@@ -36,10 +42,31 @@ fun SwitchItem(
|
||||
onValueChange = onCheckedChange
|
||||
),
|
||||
headlineContent = {
|
||||
Text(title)
|
||||
TextRow(
|
||||
leadingContent = if (beta) {
|
||||
{
|
||||
LabelItem(
|
||||
modifier = Modifier.then(stateAlpha),
|
||||
text = "Beta"
|
||||
)
|
||||
}
|
||||
} else null
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.then(stateAlpha),
|
||||
text = title,
|
||||
)
|
||||
}
|
||||
},
|
||||
leadingContent = icon?.let {
|
||||
{ Icon(icon, title) }
|
||||
{
|
||||
Icon(
|
||||
modifier = Modifier.then(stateAlpha),
|
||||
imageVector = icon,
|
||||
contentDescription = title,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
@@ -51,7 +78,10 @@ fun SwitchItem(
|
||||
},
|
||||
supportingContent = {
|
||||
if (summary != null) {
|
||||
Text(summary)
|
||||
Text(
|
||||
modifier = Modifier.then(stateAlpha),
|
||||
text = summary
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,244 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.SdStorage
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
/**
|
||||
* 槽位选择对话框组件
|
||||
* 用于Kernel刷写时选择目标槽位
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SlotSelectionDialog(
|
||||
show: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onSlotSelected: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var currentSlot by remember { mutableStateOf<String?>(null) }
|
||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
currentSlot = getCurrentSlot(context)
|
||||
errorMessage = null
|
||||
} catch (e: Exception) {
|
||||
errorMessage = e.message
|
||||
currentSlot = null
|
||||
}
|
||||
}
|
||||
|
||||
if (show) {
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(id = R.string.select_slot_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
if (errorMessage != null) {
|
||||
Text(
|
||||
text = "Error: $errorMessage",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.current_slot,
|
||||
currentSlot ?: "Unknown"
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.select_slot_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Horizontal arrangement for slot options with highlighted current slot
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val slotOptions = listOf(
|
||||
ListOption(
|
||||
titleText = stringResource(id = R.string.slot_a),
|
||||
subtitleText = if (currentSlot == "a" || currentSlot == "_a") stringResource(id = R.string.currently_selected) else null,
|
||||
icon = Icons.Filled.SdStorage
|
||||
),
|
||||
ListOption(
|
||||
titleText = stringResource(id = R.string.slot_b),
|
||||
subtitleText = if (currentSlot == "b" || currentSlot == "_b") stringResource(id = R.string.currently_selected) else null,
|
||||
icon = Icons.Filled.SdStorage
|
||||
)
|
||||
)
|
||||
|
||||
slotOptions.forEachIndexed { index, option ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(
|
||||
color = if (option.subtitleText != null) {
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
}
|
||||
)
|
||||
.clickable {
|
||||
onSlotSelected(
|
||||
when (index) {
|
||||
0 -> "a"
|
||||
else -> "b"
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = option.icon,
|
||||
contentDescription = null,
|
||||
tint = if (option.subtitleText != null) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = option.titleText,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = if (option.subtitleText != null) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
}
|
||||
)
|
||||
option.subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (true) {
|
||||
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
currentSlot?.let { onSlotSelected(it) }
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.ok),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = onDismiss
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = cardColor,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
tonalElevation = 4.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Data class for list options
|
||||
data class ListOption(
|
||||
val titleText: String,
|
||||
val subtitleText: String?,
|
||||
val icon: ImageVector
|
||||
)
|
||||
|
||||
// Utility function to get current slot
|
||||
private fun getCurrentSlot(context: Context): String? {
|
||||
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
|
||||
if (it.startsWith("_")) it.substring(1) else it
|
||||
}
|
||||
}
|
||||
|
||||
private fun runCommandGetOutput(su: Boolean, cmd: String): String? {
|
||||
return try {
|
||||
val process = ProcessBuilder(if (su) "su" else "sh").start()
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("$cmd\n")
|
||||
writer.write("exit\n")
|
||||
writer.flush()
|
||||
}
|
||||
process.inputStream.bufferedReader().use { reader ->
|
||||
reader.readText().trim()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.sukisu.ultra.ui.component
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SwitchItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
checked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
// 颜色动画
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||
animationSpec = tween(300),
|
||||
label = "iconTint"
|
||||
)
|
||||
|
||||
// 开关颜色
|
||||
val switchColors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
checkedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.onPrimary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
disabledCheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||
disabledCheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledCheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledCheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
disabledUncheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||
disabledUncheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledUncheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||
disabledUncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
supportingContent = summary?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = Int.MAX_VALUE,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = iconTint
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = null,
|
||||
enabled = enabled,
|
||||
colors = switchColors
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = enabled) {
|
||||
onCheckedChange(!checked)
|
||||
}
|
||||
.padding(vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component.profile
|
||||
package com.sukisu.ultra.ui.component.profile
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
@@ -11,9 +11,9 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.component.SwitchItem
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.component.SwitchItem
|
||||
|
||||
@Composable
|
||||
fun AppProfileConfig(
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component.profile
|
||||
package com.sukisu.ultra.ui.component.profile
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -42,12 +42,12 @@ import com.maxkeppeler.sheets.input.models.ValidationResult
|
||||
import com.maxkeppeler.sheets.list.ListDialog
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.profile.Capabilities
|
||||
import zako.zako.zako.profile.Groups
|
||||
import zako.zako.zako.ui.component.rememberCustomDialog
|
||||
import zako.zako.zako.ui.util.isSepolicyValid
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.profile.Capabilities
|
||||
import com.sukisu.ultra.profile.Groups
|
||||
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||
import com.sukisu.ultra.ui.util.isSepolicyValid
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.component.profile
|
||||
package com.sukisu.ultra.ui.component.profile
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -23,11 +23,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.util.listAppProfileTemplates
|
||||
import zako.zako.zako.ui.util.setSepolicy
|
||||
import zako.zako.zako.ui.viewmodel.getTemplateInfoById
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.util.listAppProfileTemplates
|
||||
import com.sukisu.ultra.ui.util.setSepolicy
|
||||
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
@@ -0,0 +1,593 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Android
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
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.draw.shadow
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.component.SwitchItem
|
||||
import com.sukisu.ultra.ui.component.profile.AppProfileConfig
|
||||
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
||||
import com.sukisu.ultra.ui.component.profile.TemplateConfig
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.forceStopApp
|
||||
import com.sukisu.ultra.ui.util.getSepolicy
|
||||
import com.sukisu.ultra.ui.util.launchApp
|
||||
import com.sukisu.ultra.ui.util.restartApp
|
||||
import com.sukisu.ultra.ui.util.setSepolicy
|
||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/5/16.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun AppProfileScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
appInfo: SuperUserViewModel.AppInfo,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val scope = rememberCoroutineScope()
|
||||
val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label)
|
||||
val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label)
|
||||
val suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label)
|
||||
|
||||
val packageName = appInfo.packageName
|
||||
val initialProfile = Natives.getAppProfile(packageName, appInfo.uid)
|
||||
if (initialProfile.allowSu) {
|
||||
initialProfile.rules = getSepolicy(packageName)
|
||||
}
|
||||
var profile by rememberSaveable {
|
||||
mutableStateOf(initialProfile)
|
||||
}
|
||||
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
title = appInfo.label,
|
||||
packageName = packageName,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
) { paddingValues ->
|
||||
AppProfileInner(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
packageName = appInfo.packageName,
|
||||
appLabel = appInfo.label,
|
||||
appIcon = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(),
|
||||
contentDescription = appInfo.label,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
},
|
||||
profile = profile,
|
||||
onViewTemplate = {
|
||||
getTemplateInfoById(it)?.let { info ->
|
||||
navigator.navigate(TemplateEditorScreenDestination(info))
|
||||
}
|
||||
},
|
||||
onManageTemplate = {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination())
|
||||
},
|
||||
onProfileChange = {
|
||||
scope.launch {
|
||||
if (it.allowSu) {
|
||||
// sync with allowlist.c - forbid_system_uid
|
||||
if (appInfo.uid < 2000 && appInfo.uid != 1000) {
|
||||
snackBarHost.showSnackbar(suNotAllowed)
|
||||
return@launch
|
||||
}
|
||||
if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) {
|
||||
snackBarHost.showSnackbar(failToUpdateSepolicy)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
if (!Natives.setAppProfile(it)) {
|
||||
snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid))
|
||||
} else {
|
||||
profile = it
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppProfileInner(
|
||||
modifier: Modifier = Modifier,
|
||||
packageName: String,
|
||||
appLabel: String,
|
||||
appIcon: @Composable () -> Unit,
|
||||
profile: Natives.Profile,
|
||||
onViewTemplate: (id: String) -> Unit = {},
|
||||
onManageTemplate: () -> Unit = {},
|
||||
onProfileChange: (Natives.Profile) -> Unit,
|
||||
) {
|
||||
val isRootGranted = profile.allowSu
|
||||
|
||||
Column(modifier = modifier) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
AppMenuBox(packageName) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = appLabel,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = packageName,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
leadingContent = appIcon,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(id = R.string.superuser),
|
||||
checked = isRootGranted,
|
||||
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
||||
)
|
||||
}
|
||||
|
||||
Crossfade(
|
||||
targetState = isRootGranted,
|
||||
label = "RootAccess"
|
||||
) { current ->
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
||||
) {
|
||||
if (current) {
|
||||
val initialMode = if (profile.rootUseDefault) {
|
||||
Mode.Default
|
||||
} else if (profile.rootTemplate != null) {
|
||||
Mode.Template
|
||||
} else {
|
||||
Mode.Custom
|
||||
}
|
||||
var mode by rememberSaveable {
|
||||
mutableStateOf(initialMode)
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
ProfileBox(mode, true) {
|
||||
// template mode shouldn't change profile here!
|
||||
if (it == Mode.Default || it == Mode.Custom) {
|
||||
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
||||
}
|
||||
mode = it
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = mode != Mode.Default,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Crossfade(targetState = mode, label = "ProfileMode") { currentMode ->
|
||||
when (currentMode) {
|
||||
Mode.Template -> {
|
||||
TemplateConfig(
|
||||
profile = profile,
|
||||
onViewTemplate = onViewTemplate,
|
||||
onManageTemplate = onManageTemplate,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
Mode.Custom -> {
|
||||
RootProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
ProfileBox(mode, false) {
|
||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = mode == Mode.Custom,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
AppProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
enabled = mode == Mode.Custom,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Mode(@StringRes private val res: Int) {
|
||||
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
||||
|
||||
val text: String
|
||||
@Composable get() = stringResource(res)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
title: String,
|
||||
packageName: String,
|
||||
onBack: () -> Unit,
|
||||
colors: TopAppBarColors,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = packageName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.alpha(0.8f)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = colors,
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack,
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
modifier = Modifier.shadow(
|
||||
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
|
||||
4.dp else 0.dp,
|
||||
spotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileBox(
|
||||
mode: Mode,
|
||||
hasTemplate: Boolean,
|
||||
onModeChange: (Mode) -> Unit,
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = stringResource(R.string.profile),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
Text(
|
||||
text = mode.text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AccountCircle,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
HorizontalDivider(
|
||||
thickness = Dp.Hairline,
|
||||
color = MaterialTheme.colorScheme.outlineVariant
|
||||
)
|
||||
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
|
||||
) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Default,
|
||||
onClick = { onModeChange(Mode.Default) },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.profile_default),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
|
||||
if (hasTemplate) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Template,
|
||||
onClick = { onModeChange(Mode.Template) },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.profile_template),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
}
|
||||
|
||||
FilterChip(
|
||||
selected = mode == Mode.Custom,
|
||||
onClick = { onModeChange(Mode.Custom) },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.profile_custom),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
shape = MaterialTheme.shapes.small
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||
@Composable
|
||||
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
||||
val density = LocalDensity.current
|
||||
|
||||
BoxWithConstraints(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = {
|
||||
touchPoint = it
|
||||
expanded = true
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
content()
|
||||
|
||||
val (offsetX, offsetY) = with(density) {
|
||||
(touchPoint.x.toDp()) to (touchPoint.y.toDp())
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
offset = DpOffset(offsetX, -offsetY),
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
},
|
||||
) {
|
||||
AppMenuOption(
|
||||
text = stringResource(id = R.string.launch_app),
|
||||
onClick = {
|
||||
expanded = false
|
||||
launchApp(packageName)
|
||||
}
|
||||
)
|
||||
|
||||
AppMenuOption(
|
||||
text = stringResource(id = R.string.force_stop_app),
|
||||
onClick = {
|
||||
expanded = false
|
||||
forceStopApp(packageName)
|
||||
}
|
||||
)
|
||||
|
||||
AppMenuOption(
|
||||
text = stringResource(id = R.string.restart_app),
|
||||
onClick = {
|
||||
expanded = false
|
||||
restartApp(packageName)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppMenuOption(text: String, onClick: () -> Unit) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
},
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AppProfilePreview() {
|
||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||
Surface {
|
||||
AppProfileInner(
|
||||
packageName = "icu.nullptr.test",
|
||||
appLabel = "Test",
|
||||
appIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Android,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
profile = profile,
|
||||
onProfileChange = {
|
||||
profile = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -11,7 +11,7 @@ import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDe
|
||||
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination
|
||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||
import zako.zako.zako.R
|
||||
import com.sukisu.ultra.R
|
||||
|
||||
enum class BottomBarDestination(
|
||||
val direction: DirectionDestinationSpec,
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.os.Environment
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -37,10 +37,10 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.component.KeyEventBlocker
|
||||
import zako.zako.zako.ui.util.LocalSnackbarHost
|
||||
import zako.zako.zako.ui.util.runModuleAction
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.runModuleAction
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
@@ -30,9 +30,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import zako.zako.zako.ui.component.KeyEventBlocker
|
||||
import zako.zako.zako.ui.util.*
|
||||
import zako.zako.zako.R
|
||||
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import com.sukisu.ultra.R
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -45,10 +45,6 @@ enum class FlashingStatus {
|
||||
|
||||
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
||||
|
||||
fun getFlashingStatus(): FlashingStatus {
|
||||
return currentFlashingStatus.value
|
||||
}
|
||||
|
||||
fun setFlashingStatus(status: FlashingStatus) {
|
||||
currentFlashingStatus.value = status
|
||||
}
|
||||
1036
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt
Normal file
1036
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt
Normal file
File diff suppressed because it is too large
Load Diff
788
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt
Normal file
788
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt
Normal file
@@ -0,0 +1,788 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||
import androidx.compose.material.icons.filled.FileUpload
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.RadioButtonDefaults
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.maxkeppeker.sheets.core.models.base.Header
|
||||
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
||||
import com.maxkeppeler.sheets.list.ListDialog
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
|
||||
import com.sukisu.ultra.flash.HorizonKernelState
|
||||
import com.sukisu.ultra.flash.HorizonKernelWorker
|
||||
import com.sukisu.ultra.ui.component.DialogHandle
|
||||
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
||||
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import com.sukisu.ultra.ui.theme.getCardColors
|
||||
import com.sukisu.ultra.ui.util.LkmSelection
|
||||
import com.sukisu.ultra.ui.util.getCurrentKmi
|
||||
import com.sukisu.ultra.ui.util.getSupportedKmis
|
||||
import com.sukisu.ultra.ui.util.isAbDevice
|
||||
import com.sukisu.ultra.ui.util.isInitBoot
|
||||
import com.sukisu.ultra.ui.util.rootAvailable
|
||||
import com.sukisu.ultra.getKernelVersion
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2024/3/12.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
|
||||
val context = LocalContext.current
|
||||
var showRebootDialog by remember { mutableStateOf(false) }
|
||||
var showSlotSelectionDialog by remember { mutableStateOf(false) }
|
||||
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
|
||||
val horizonKernelState = remember { HorizonKernelState() }
|
||||
val flashState by horizonKernelState.state.collectAsState()
|
||||
val summary = stringResource(R.string.horizon_kernel_summary)
|
||||
val kernelVersion = getKernelVersion()
|
||||
val isGKI = kernelVersion.isGKI()
|
||||
val isAbDevice = isAbDevice()
|
||||
|
||||
val onFlashComplete = {
|
||||
showRebootDialog = true
|
||||
}
|
||||
|
||||
if (showRebootDialog) {
|
||||
RebootDialog(
|
||||
show = true,
|
||||
onDismiss = { showRebootDialog = false },
|
||||
onConfirm = {
|
||||
showRebootDialog = false
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("su")
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("svc power reboot\n")
|
||||
writer.write("exit\n")
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val onInstall = {
|
||||
installMethod?.let { method ->
|
||||
when (method) {
|
||||
is InstallMethod.HorizonKernel -> {
|
||||
method.uri?.let { uri ->
|
||||
val worker = HorizonKernelWorker(
|
||||
context = context,
|
||||
state = horizonKernelState,
|
||||
slot = method.slot
|
||||
)
|
||||
worker.uri = uri
|
||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
||||
worker.start()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val flashIt = FlashIt.FlashBoot(
|
||||
boot = if (method is InstallMethod.SelectFile) method.uri else null,
|
||||
lkm = lkmSelection,
|
||||
ota = method is InstallMethod.DirectInstallToInactiveSlot
|
||||
)
|
||||
navigator.navigate(FlashScreenDestination(flashIt))
|
||||
}
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
// 槽位选择
|
||||
SlotSelectionDialog(
|
||||
show = showSlotSelectionDialog && isAbDevice,
|
||||
onDismiss = { showSlotSelectionDialog = false },
|
||||
onSlotSelected = { slot ->
|
||||
showSlotSelectionDialog = false
|
||||
val horizonMethod = InstallMethod.HorizonKernel(
|
||||
uri = tempKernelUri,
|
||||
slot = slot,
|
||||
summary = summary
|
||||
)
|
||||
installMethod = horizonMethod
|
||||
}
|
||||
)
|
||||
|
||||
val currentKmi by produceState(initialValue = "") {
|
||||
value = getCurrentKmi()
|
||||
}
|
||||
|
||||
val selectKmiDialog = rememberSelectKmiDialog { kmi ->
|
||||
kmi?.let {
|
||||
lkmSelection = LkmSelection.KmiString(it)
|
||||
onInstall()
|
||||
}
|
||||
}
|
||||
|
||||
val onClickNext = {
|
||||
if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
||||
selectKmiDialog.show()
|
||||
} else {
|
||||
onInstall()
|
||||
}
|
||||
}
|
||||
|
||||
val selectLkmLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri ->
|
||||
lkmSelection = LkmSelection.LkmUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onLkmUpload = {
|
||||
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/octet-stream"
|
||||
})
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = { navigator.popBackStack() },
|
||||
onLkmUpload = onLkmUpload,
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
)
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(top = 12.dp)
|
||||
) {
|
||||
SelectInstallMethod(
|
||||
isGKI = isGKI,
|
||||
isAbDevice = isAbDevice,
|
||||
onSelected = { method ->
|
||||
if (method is InstallMethod.HorizonKernel && method.uri != null) {
|
||||
if (isAbDevice) {
|
||||
tempKernelUri = method.uri
|
||||
showSlotSelectionDialog = true
|
||||
} else {
|
||||
installMethod = method
|
||||
}
|
||||
} else {
|
||||
installMethod = method
|
||||
}
|
||||
horizonKernelState.reset()
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
HorizonKernelFlashProgress(flashState)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.selected_lkm,
|
||||
it.uri.lastPathSegment ?: "(file)"
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
|
||||
if (method.slot != null) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.selected_slot,
|
||||
if (method.slot == "a") stringResource(id = R.string.slot_a)
|
||||
else stringResource(id = R.string.slot_b)
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = installMethod != null && !flashState.isFlashing,
|
||||
onClick = onClickNext,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
|
||||
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
stringResource(id = R.string.install_next),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RebootDialog(
|
||||
show: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
if (show) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(id = R.string.reboot_complete_title)) },
|
||||
text = { Text(stringResource(id = R.string.reboot_complete_msg)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(stringResource(id = R.string.yes))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(id = R.string.no))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class InstallMethod {
|
||||
data class SelectFile(
|
||||
val uri: Uri? = null,
|
||||
@StringRes override val label: Int = R.string.select_file,
|
||||
override val summary: String?
|
||||
) : InstallMethod()
|
||||
|
||||
data object DirectInstall : InstallMethod() {
|
||||
override val label: Int
|
||||
get() = R.string.direct_install
|
||||
}
|
||||
|
||||
data object DirectInstallToInactiveSlot : InstallMethod() {
|
||||
override val label: Int
|
||||
get() = R.string.install_inactive_slot
|
||||
}
|
||||
|
||||
data class HorizonKernel(
|
||||
val uri: Uri? = null,
|
||||
val slot: String? = null,
|
||||
@StringRes override val label: Int = R.string.horizon_kernel,
|
||||
override val summary: String? = null
|
||||
) : InstallMethod()
|
||||
|
||||
abstract val label: Int
|
||||
open val summary: String? = null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectInstallMethod(
|
||||
isGKI: Boolean = false,
|
||||
isAbDevice: Boolean = false,
|
||||
onSelected: (InstallMethod) -> Unit = {}
|
||||
) {
|
||||
val rootAvailable = rootAvailable()
|
||||
val isAbDevice = isAbDevice()
|
||||
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
|
||||
val selectFileTip = stringResource(
|
||||
id = R.string.select_file_tip,
|
||||
if (isInitBoot()) "init_boot" else "boot"
|
||||
)
|
||||
|
||||
val radioOptions = mutableListOf<InstallMethod>(
|
||||
InstallMethod.SelectFile(summary = selectFileTip)
|
||||
)
|
||||
|
||||
if (rootAvailable) {
|
||||
radioOptions.add(InstallMethod.DirectInstall)
|
||||
if (isAbDevice) {
|
||||
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
|
||||
}
|
||||
radioOptions.add(InstallMethod.HorizonKernel(summary = horizonKernelSummary))
|
||||
}
|
||||
|
||||
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
|
||||
val selectImageLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri ->
|
||||
val option = when (currentSelectingMethod) {
|
||||
is InstallMethod.SelectFile -> InstallMethod.SelectFile(
|
||||
uri,
|
||||
summary = selectFileTip
|
||||
)
|
||||
|
||||
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(
|
||||
uri,
|
||||
summary = horizonKernelSummary
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
option?.let {
|
||||
selectedOption = it
|
||||
onSelected(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val confirmDialog = rememberConfirmDialog(
|
||||
onConfirm = {
|
||||
selectedOption = InstallMethod.DirectInstallToInactiveSlot
|
||||
onSelected(InstallMethod.DirectInstallToInactiveSlot)
|
||||
},
|
||||
onDismiss = null
|
||||
)
|
||||
|
||||
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
|
||||
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
|
||||
|
||||
val onClick = { option: InstallMethod ->
|
||||
currentSelectingMethod = option
|
||||
when (option) {
|
||||
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
||||
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/*"
|
||||
putExtra(
|
||||
Intent.EXTRA_MIME_TYPES,
|
||||
arrayOf("application/octet-stream", "application/zip")
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
is InstallMethod.DirectInstall -> {
|
||||
selectedOption = option
|
||||
onSelected(option)
|
||||
}
|
||||
|
||||
is InstallMethod.DirectInstallToInactiveSlot -> {
|
||||
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var LKMExpanded by remember { mutableStateOf(false) }
|
||||
var GKIExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
) {
|
||||
// LKM 安装/修补
|
||||
if (isGKI) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.AutoFixHigh,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.Lkm_install_methods),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
LKMExpanded = !LKMExpanded
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = LKMExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp
|
||||
)
|
||||
) {
|
||||
radioOptions.take(3).forEach { option ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Surface(
|
||||
color = if (option.javaClass == selectedOption?.javaClass)
|
||||
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = option.javaClass == selectedOption?.javaClass,
|
||||
onValueChange = { onClick(option) },
|
||||
role = Role.RadioButton,
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = option.javaClass == selectedOption?.javaClass,
|
||||
onClick = null,
|
||||
interactionSource = interactionSource,
|
||||
colors = RadioButtonDefaults.colors(
|
||||
selectedColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = option.label),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
option.summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// anykernel3 刷写
|
||||
if (rootAvailable) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.FileUpload,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(
|
||||
stringResource(R.string.GKI_install_methods),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable {
|
||||
GKIExpanded = !GKIExpanded
|
||||
}
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = GKIExpanded,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 16.dp
|
||||
)
|
||||
) {
|
||||
radioOptions.filterIsInstance<InstallMethod.HorizonKernel>().forEach { option ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Surface(
|
||||
color = if (option.javaClass == selectedOption?.javaClass)
|
||||
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = option.javaClass == selectedOption?.javaClass,
|
||||
onValueChange = { onClick(option) },
|
||||
role = Role.RadioButton,
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = option.javaClass == selectedOption?.javaClass,
|
||||
onClick = null,
|
||||
interactionSource = interactionSource,
|
||||
colors = RadioButtonDefaults.colors(
|
||||
selectedColor = MaterialTheme.colorScheme.primary,
|
||||
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = option.label),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
option.summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
||||
return rememberCustomDialog { dismiss ->
|
||||
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
||||
value = getSupportedKmis()
|
||||
}
|
||||
val options = supportedKmi.map { value ->
|
||||
ListOption(
|
||||
titleText = value
|
||||
)
|
||||
}
|
||||
|
||||
var selection by remember { mutableStateOf<String?>(null) }
|
||||
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = MaterialTheme.colorScheme.copy(
|
||||
surface = backgroundColor
|
||||
)
|
||||
) {
|
||||
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||
onSelected(selection)
|
||||
}, onCloseRequest = {
|
||||
dismiss()
|
||||
}), header = Header.Default(
|
||||
title = stringResource(R.string.select_kmi),
|
||||
), selection = ListSelection.Single(
|
||||
showRadioButtons = true,
|
||||
options = options,
|
||||
) { _, option ->
|
||||
selection = option.titleText
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
onBack: () -> Unit = {},
|
||||
onLkmUpload: () -> Unit = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.install),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SelectInstallPreview() {
|
||||
InstallScreen(EmptyDestinationsNavigator)
|
||||
}
|
||||
783
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt
Normal file
783
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt
Normal file
@@ -0,0 +1,783 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import com.sukisu.ultra.ui.component.*
|
||||
import com.sukisu.ultra.ui.theme.*
|
||||
import com.sukisu.ultra.ui.viewmodel.KpmViewModel
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import java.io.File
|
||||
import androidx.core.content.edit
|
||||
import com.sukisu.ultra.R
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.net.*
|
||||
import android.app.Activity
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
|
||||
/**
|
||||
* KPM 管理界面
|
||||
* 以下内核模块功能由KernelPatch开发,经过修改后加入SukiSU Ultra的内核模块功能
|
||||
* 开发者:ShirkNeko, Liaokong
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun KpmScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
viewModel: KpmViewModel = viewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackBarHost = remember { SnackbarHostState() }
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
|
||||
val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
|
||||
val moduleFileName = module.id
|
||||
module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName)
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
val kpmInstallSuccess = stringResource(R.string.kpm_install_success)
|
||||
val kpmInstallFailed = stringResource(R.string.kpm_install_failed)
|
||||
val cancel = stringResource(R.string.cancel)
|
||||
val uninstall = stringResource(R.string.uninstall)
|
||||
val failedToCheckModuleFile = stringResource(R.string.snackbar_failed_to_check_module_file)
|
||||
val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success)
|
||||
val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed)
|
||||
val kpmInstallMode = stringResource(R.string.kpm_install_mode)
|
||||
val kpmInstallModeLoad = stringResource(R.string.kpm_install_mode_load)
|
||||
val kpmInstallModeEmbed = stringResource(R.string.kpm_install_mode_embed)
|
||||
val invalidFileTypeMessage = stringResource(R.string.invalid_file_type)
|
||||
val confirmTitle = stringResource(R.string.confirm_uninstall_title_with_filename)
|
||||
|
||||
var tempFileForInstall by remember { mutableStateOf<File?>(null) }
|
||||
val installModeDialog = rememberCustomDialog { dismiss ->
|
||||
var moduleName by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(tempFileForInstall) {
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
try {
|
||||
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='")
|
||||
val process = Runtime.getRuntime().exec(command)
|
||||
val inputStream = process.inputStream
|
||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||
var line: String?
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
if (line!!.startsWith("name=")) {
|
||||
moduleName = line.substringAfter("name=").trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
process.waitFor()
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to get module name: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
tempFileForInstall?.delete()
|
||||
tempFileForInstall = null
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = kpmInstallMode,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
moduleName?.let {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_install_mode_description, it),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dismiss()
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
handleModuleInstall(
|
||||
tempFile = tempFile,
|
||||
isEmbed = false,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmInstallSuccess = kpmInstallSuccess,
|
||||
kpmInstallFailed = kpmInstallFailed
|
||||
)
|
||||
}
|
||||
tempFileForInstall = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Download,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp).padding(end = 4.dp)
|
||||
)
|
||||
Text(kpmInstallModeLoad)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dismiss()
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
handleModuleInstall(
|
||||
tempFile = tempFile,
|
||||
isEmbed = true,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmInstallSuccess = kpmInstallSuccess,
|
||||
kpmInstallFailed = kpmInstallFailed
|
||||
)
|
||||
}
|
||||
tempFileForInstall = null
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Inventory,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp).padding(end = 4.dp)
|
||||
)
|
||||
Text(kpmInstallModeEmbed)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
},
|
||||
dismissButton = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
tempFileForInstall?.delete()
|
||||
tempFileForInstall = null
|
||||
}
|
||||
) {
|
||||
Text(cancel)
|
||||
}
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = MaterialTheme.shapes.extraLarge
|
||||
)
|
||||
}
|
||||
|
||||
val selectPatchLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
|
||||
|
||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||
|
||||
scope.launch {
|
||||
val fileName = uri.lastPathSegment ?: "unknown.kpm"
|
||||
val encodedFileName = URLEncoder.encode(fileName, "UTF-8")
|
||||
val tempFile = File(context.cacheDir, encodedFileName)
|
||||
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
val mimeType = context.contentResolver.getType(uri)
|
||||
val isCorrectMimeType = mimeType == null || mimeType.contains("application/octet-stream")
|
||||
|
||||
if (!isCorrectMimeType) {
|
||||
var shouldShowSnackbar = true
|
||||
try {
|
||||
val matchCount = checkStringsCommand(tempFile)
|
||||
val isElf = isElfFile(tempFile)
|
||||
|
||||
if (matchCount >= 1 || isElf) {
|
||||
shouldShowSnackbar = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to execute checks: ${e.message}", e)
|
||||
}
|
||||
if (shouldShowSnackbar) {
|
||||
snackBarHost.showSnackbar(
|
||||
message = invalidFileTypeMessage,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
tempFile.delete()
|
||||
return@launch
|
||||
}
|
||||
tempFileForInstall = tempFile
|
||||
installModeDialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
while(true) {
|
||||
viewModel.fetchModuleList()
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
|
||||
val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
|
||||
var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchAppBar(
|
||||
title = { Text(stringResource(R.string.kpm_title)) },
|
||||
searchText = viewModel.search,
|
||||
onSearchTextChange = { viewModel.search = it },
|
||||
onClearClick = { viewModel.search = "" },
|
||||
scrollBehavior = scrollBehavior,
|
||||
dropdownContent = {
|
||||
IconButton(
|
||||
onClick = { viewModel.fetchModuleList() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Refresh,
|
||||
contentDescription = stringResource(R.string.refresh),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
selectPatchLauncher.launch(
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/octet-stream"
|
||||
}
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = stringResource(R.string.kpm_install),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_install),
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
},
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
expanded = true,
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
if (!isNoticeClosed) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Info,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.kernel_module_notice),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
isNoticeClosed = true
|
||||
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
||||
},
|
||||
modifier = Modifier.size(24.dp),
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = stringResource(R.string.close_notice)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.moduleList.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Code,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.kpm_empty),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
items(viewModel.moduleList) { module ->
|
||||
KpmModuleItem(
|
||||
module = module,
|
||||
onUninstall = {
|
||||
scope.launch {
|
||||
val confirmContent = moduleConfirmContentMap[module.id] ?: ""
|
||||
handleModuleUninstall(
|
||||
module = module,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmUninstallSuccess = kpmUninstallSuccess,
|
||||
kpmUninstallFailed = kpmUninstallFailed,
|
||||
failedToCheckModuleFile = failedToCheckModuleFile,
|
||||
uninstall = uninstall,
|
||||
cancel = cancel,
|
||||
confirmDialog = confirmDialog,
|
||||
confirmTitle = confirmTitle,
|
||||
confirmContent = confirmContent
|
||||
)
|
||||
}
|
||||
},
|
||||
onControl = {
|
||||
viewModel.loadModuleDetail(module.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleModuleInstall(
|
||||
tempFile: File,
|
||||
isEmbed: Boolean,
|
||||
viewModel: KpmViewModel,
|
||||
snackBarHost: SnackbarHostState,
|
||||
kpmInstallSuccess: String,
|
||||
kpmInstallFailed: String
|
||||
) {
|
||||
var moduleId: String? = null
|
||||
try {
|
||||
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='")
|
||||
val process = Runtime.getRuntime().exec(command)
|
||||
val inputStream = process.inputStream
|
||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||
var line: String?
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
if (line!!.startsWith("name=")) {
|
||||
moduleId = line.substringAfter("name=").trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
process.waitFor()
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to get module ID from strings command: ${e.message}", e)
|
||||
}
|
||||
|
||||
if (moduleId == null || moduleId.isEmpty()) {
|
||||
Log.e("KsuCli", "Failed to extract module ID from file: ${tempFile.name}")
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
tempFile.delete()
|
||||
return
|
||||
}
|
||||
|
||||
val targetPath = "/data/adb/kpm/$moduleId.kpm"
|
||||
|
||||
try {
|
||||
if (isEmbed) {
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p /data/adb/kpm")).waitFor()
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "cp ${tempFile.absolutePath} $targetPath")).waitFor()
|
||||
}
|
||||
|
||||
val loadResult = loadKpmModule(tempFile.absolutePath)
|
||||
if (loadResult.startsWith("Error")) {
|
||||
Log.e("KsuCli", "Failed to load KPM module: $loadResult")
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
} else {
|
||||
viewModel.fetchModuleList()
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallSuccess,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to load KPM module: ${e.message}", e)
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
tempFile.delete()
|
||||
}
|
||||
|
||||
private suspend fun handleModuleUninstall(
|
||||
module: KpmViewModel.ModuleInfo,
|
||||
viewModel: KpmViewModel,
|
||||
snackBarHost: SnackbarHostState,
|
||||
kpmUninstallSuccess: String,
|
||||
kpmUninstallFailed: String,
|
||||
failedToCheckModuleFile: String,
|
||||
uninstall: String,
|
||||
cancel: String,
|
||||
confirmTitle : String,
|
||||
confirmContent : String,
|
||||
confirmDialog: ConfirmDialogHandle
|
||||
) {
|
||||
val moduleFileName = "${module.id}.kpm"
|
||||
val moduleFilePath = "/data/adb/kpm/$moduleFileName"
|
||||
|
||||
val fileExists = try {
|
||||
val result = Runtime.getRuntime().exec(arrayOf("su", "-c", "ls /data/adb/kpm/$moduleFileName")).waitFor() == 0
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e)
|
||||
snackBarHost.showSnackbar(
|
||||
message = failedToCheckModuleFile,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
false
|
||||
}
|
||||
val confirmResult = confirmDialog.awaitConfirm(
|
||||
title = confirmTitle,
|
||||
content = confirmContent,
|
||||
confirm = uninstall,
|
||||
dismiss = cancel
|
||||
)
|
||||
|
||||
if (confirmResult == ConfirmResult.Confirmed) {
|
||||
try {
|
||||
val unloadResult = unloadKpmModule(module.id)
|
||||
if (unloadResult.startsWith("Error")) {
|
||||
Log.e("KsuCli", "Failed to unload KPM module: $unloadResult")
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (fileExists) {
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm $moduleFilePath")).waitFor()
|
||||
}
|
||||
|
||||
viewModel.fetchModuleList()
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallSuccess,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to unload KPM module: ${e.message}", e)
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KpmModuleItem(
|
||||
module: KpmViewModel.ModuleInfo,
|
||||
onUninstall: () -> Unit,
|
||||
onControl: () -> Unit
|
||||
) {
|
||||
val viewModel: KpmViewModel = viewModel()
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackBarHost = remember { SnackbarHostState() }
|
||||
val successMessage = stringResource(R.string.kpm_control_success)
|
||||
val failureMessage = stringResource(R.string.kpm_control_failed)
|
||||
|
||||
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.hideInputDialog() },
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_control),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = viewModel.inputArgs,
|
||||
onValueChange = { viewModel.updateInputArgs(it) },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(R.string.kpm_args),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = module.args,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val result = viewModel.executeControl()
|
||||
val message = when (result) {
|
||||
0 -> successMessage
|
||||
else -> failureMessage
|
||||
}
|
||||
snackBarHost.showSnackbar(message)
|
||||
onControl()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.confirm),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
||||
Text(
|
||||
text = stringResource(R.string.cancel),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = MaterialTheme.shapes.extraLarge
|
||||
)
|
||||
}
|
||||
|
||||
Card(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = module.name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = module.description,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = { viewModel.showInputDialog(module.id) },
|
||||
enabled = module.hasAction,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Settings,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.kpm_control))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onUninstall,
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Delete,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.kpm_uninstall))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkStringsCommand(tempFile: File): Int {
|
||||
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='")
|
||||
val process = Runtime.getRuntime().exec(command)
|
||||
val inputStream = process.inputStream
|
||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||
var line: String?
|
||||
var matchCount = 0
|
||||
val keywords = listOf("name=", "version=", "license=", "author=")
|
||||
var nameExists = false
|
||||
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
if (!nameExists && line!!.startsWith("name=")) {
|
||||
nameExists = true
|
||||
matchCount++
|
||||
} else if (nameExists) {
|
||||
for (keyword in keywords) {
|
||||
if (line!!.startsWith(keyword)) {
|
||||
matchCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
process.waitFor()
|
||||
|
||||
return if (nameExists) matchCount else 0
|
||||
}
|
||||
|
||||
private fun isElfFile(tempFile: File): Boolean {
|
||||
val elfMagic = byteArrayOf(0x7F, 'E'.code.toByte(), 'L'.code.toByte(), 'F'.code.toByte())
|
||||
val fileBytes = ByteArray(4)
|
||||
FileInputStream(tempFile).use { input ->
|
||||
input.read(fileBytes)
|
||||
}
|
||||
return fileBytes.contentEquals(elfMagic)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.app.Activity.*
|
||||
import android.content.Context
|
||||
@@ -9,22 +9,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.*
|
||||
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.PaddingValues
|
||||
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.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
@@ -32,41 +17,14 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.*
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -88,32 +46,33 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.ui.component.ConfirmResult
|
||||
import zako.zako.zako.ui.component.SearchAppBar
|
||||
import zako.zako.zako.ui.component.rememberConfirmDialog
|
||||
import zako.zako.zako.ui.component.rememberLoadingDialog
|
||||
import zako.zako.zako.ui.util.DownloadListener
|
||||
import zako.zako.zako.ui.util.*
|
||||
import zako.zako.zako.ui.util.download
|
||||
import zako.zako.zako.ui.util.hasMagisk
|
||||
import zako.zako.zako.ui.util.reboot
|
||||
import zako.zako.zako.ui.util.restoreModule
|
||||
import zako.zako.zako.ui.util.toggleModule
|
||||
import zako.zako.zako.ui.util.uninstallModule
|
||||
import zako.zako.zako.ui.webui.WebUIActivity
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ui.component.ConfirmResult
|
||||
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||
import com.sukisu.ultra.ui.component.rememberLoadingDialog
|
||||
import com.sukisu.ultra.ui.util.DownloadListener
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
import com.sukisu.ultra.ui.util.download
|
||||
import com.sukisu.ultra.ui.util.hasMagisk
|
||||
import com.sukisu.ultra.ui.util.reboot
|
||||
import com.sukisu.ultra.ui.util.restoreModule
|
||||
import com.sukisu.ultra.ui.util.toggleModule
|
||||
import com.sukisu.ultra.ui.util.uninstallModule
|
||||
import com.sukisu.ultra.ui.webui.WebUIActivity
|
||||
import okhttp3.OkHttpClient
|
||||
import zako.zako.zako.ui.util.ModuleModify
|
||||
import zako.zako.zako.ui.theme.getCardColors
|
||||
import zako.zako.zako.ui.theme.getCardElevation
|
||||
import zako.zako.zako.ui.viewmodel.ModuleViewModel
|
||||
import com.sukisu.ultra.ui.util.ModuleModify
|
||||
import com.sukisu.ultra.ui.theme.getCardColors
|
||||
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.ZipInputStream
|
||||
import androidx.core.content.edit
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import com.sukisu.ultra.ui.webui.WebUIXActivity
|
||||
import com.dergoogler.mmrl.platform.Platform
|
||||
import androidx.core.net.toUri
|
||||
import zako.zako.zako.ui.theme.ThemeConfig
|
||||
import zako.zako.zako.R
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -239,7 +198,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost)
|
||||
val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost)
|
||||
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
|
||||
@@ -275,7 +234,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(id = R.string.settings)
|
||||
contentDescription = stringResource(id = R.string.settings),
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
@@ -284,7 +243,16 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.module_sort_action_first)) },
|
||||
trailingIcon = { Checkbox(viewModel.sortActionFirst, null) },
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = viewModel.sortActionFirst,
|
||||
onCheckedChange = null,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.sortActionFirst = !viewModel.sortActionFirst
|
||||
prefs.edit {
|
||||
@@ -300,7 +268,16 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.module_sort_enabled_first)) },
|
||||
trailingIcon = { Checkbox(viewModel.sortEnabledFirst, null) },
|
||||
trailingIcon = {
|
||||
Checkbox(
|
||||
checked = viewModel.sortEnabledFirst,
|
||||
onCheckedChange = null,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst
|
||||
prefs.edit {
|
||||
@@ -311,12 +288,13 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
}
|
||||
}
|
||||
)
|
||||
HorizontalDivider(thickness = Dp.Hairline, modifier = Modifier.padding(vertical = 4.dp))
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.backup_modules)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Download,
|
||||
contentDescription = stringResource(R.string.backup)
|
||||
contentDescription = stringResource(R.string.backup),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
@@ -329,7 +307,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = stringResource(R.string.restore)
|
||||
contentDescription = stringResource(R.string.restore),
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
@@ -346,11 +324,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
floatingActionButton = {
|
||||
if (!hideInstallButton) {
|
||||
val moduleInstall = stringResource(id = R.string.module_install)
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
selectZipLauncher.launch(
|
||||
@@ -363,16 +336,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = moduleInstall
|
||||
contentDescription = moduleInstall,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = moduleInstall
|
||||
text = moduleInstall,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
},
|
||||
containerColor = cardColor.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
expanded = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -389,12 +363,27 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Warning,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.module_magisk_conflict),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
ModuleList(
|
||||
navigator = navigator,
|
||||
@@ -407,10 +396,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
||||
onClickModule = { id, name, hasWebUi ->
|
||||
if (hasWebUi) {
|
||||
webUILauncher.launch(
|
||||
if (prefs.getBoolean("use_webuix", false) && Platform.isAlive) {
|
||||
Intent(context, WebUIXActivity::class.java)
|
||||
.setData("kernelsu://webuix/$id".toUri())
|
||||
.putExtra("id", id)
|
||||
.putExtra("name", name)
|
||||
} else {
|
||||
Intent(context, WebUIActivity::class.java)
|
||||
.setData("kernelsu://webui/$id".toUri())
|
||||
.putExtra("id", id)
|
||||
.putExtra("name", name)
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -591,10 +587,25 @@ private fun ModuleList(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.module_empty),
|
||||
textAlign = TextAlign.Center
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Extension,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.module_empty),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -677,8 +688,16 @@ fun ModuleItem(
|
||||
onClick: (ModuleViewModel.ModuleInfo) -> Unit
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.shadow(
|
||||
elevation = cardElevation,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
) {
|
||||
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
@@ -706,6 +725,7 @@ fun ModuleItem(
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val moduleVersion = stringResource(id = R.string.module_version)
|
||||
val moduleAuthor = stringResource(id = R.string.module_author)
|
||||
@@ -720,6 +740,7 @@ fun ModuleItem(
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -727,7 +748,8 @@ fun ModuleItem(
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
textDecoration = textDecoration
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
@@ -735,7 +757,8 @@ fun ModuleItem(
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
textDecoration = textDecoration
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
@@ -749,7 +772,15 @@ fun ModuleItem(
|
||||
enabled = !module.update,
|
||||
checked = module.enabled,
|
||||
onCheckedChange = onCheckChanged,
|
||||
interactionSource = if (!module.hasWebUi) interactionSource else null
|
||||
interactionSource = if (!module.hasWebUi) interactionSource else null,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -764,83 +795,70 @@ fun ModuleItem(
|
||||
fontWeight = MaterialTheme.typography.bodySmall.fontWeight,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 4,
|
||||
textDecoration = textDecoration
|
||||
textDecoration = textDecoration,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
HorizontalDivider(thickness = Dp.Hairline)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
if (module.hasActionScript) {
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
enabled = !module.remove && module.enabled,
|
||||
onClick = {
|
||||
navigator.navigate(ExecuteModuleActionScreenDestination(module.dirId))
|
||||
viewModel.markNeedRefresh()
|
||||
},
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
colors = if (!ThemeConfig.useDynamicColor) {
|
||||
ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
||||
)
|
||||
} else {
|
||||
ButtonDefaults.filledTonalButtonColors()
|
||||
}
|
||||
colors = ButtonDefaults.filledTonalButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.Outlined.PlayArrow,
|
||||
contentDescription = null
|
||||
)
|
||||
if (!module.hasWebUi && updateUrl.isEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 7.dp),
|
||||
text = stringResource(R.string.action),
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize
|
||||
)
|
||||
//if (!module.hasWebUi && updateUrl.isEmpty()) {
|
||||
//Text(
|
||||
// modifier = Modifier.padding(start = 7.dp),
|
||||
// text = stringResource(R.string.action),
|
||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.1f, true))
|
||||
}
|
||||
|
||||
if (module.hasWebUi) {
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
enabled = !module.remove && module.enabled,
|
||||
onClick = { onClick(module) },
|
||||
interactionSource = interactionSource,
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
colors = if (!ThemeConfig.useDynamicColor) {
|
||||
ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
||||
)
|
||||
} else {
|
||||
ButtonDefaults.filledTonalButtonColors()
|
||||
}
|
||||
colors = ButtonDefaults.filledTonalButtonColors()
|
||||
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
|
||||
contentDescription = null
|
||||
)
|
||||
if (!module.hasActionScript && updateUrl.isEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 7.dp),
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
text = stringResource(R.string.open)
|
||||
)
|
||||
}
|
||||
//if (!module.hasActionScript && updateUrl.isEmpty()) {
|
||||
//Text(
|
||||
// modifier = Modifier.padding(start = 7.dp),
|
||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
// text = stringResource(R.string.open)
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -848,7 +866,7 @@ fun ModuleItem(
|
||||
|
||||
if (updateUrl.isNotEmpty()) {
|
||||
Button(
|
||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
enabled = !module.remove,
|
||||
onClick = { onUpdate(module) },
|
||||
shape = ButtonDefaults.textShape,
|
||||
@@ -859,30 +877,23 @@ fun ModuleItem(
|
||||
imageVector = Icons.Outlined.Download,
|
||||
contentDescription = null
|
||||
)
|
||||
if (!module.hasActionScript || !module.hasWebUi) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 7.dp),
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
text = stringResource(R.string.module_update)
|
||||
)
|
||||
//if (!module.hasActionScript || !module.hasWebUi) {
|
||||
//Text(
|
||||
// modifier = Modifier.padding(start = 7.dp),
|
||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
// text = stringResource(R.string.module_update)
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(0.1f, true))
|
||||
}
|
||||
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
||||
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||
onClick = { onUninstallClicked(module) },
|
||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||
colors = if (!ThemeConfig.useDynamicColor) {
|
||||
ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
||||
)
|
||||
} else {
|
||||
ButtonDefaults.filledTonalButtonColors()
|
||||
}
|
||||
colors = ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer)
|
||||
) {
|
||||
if (!module.remove) {
|
||||
Icon(
|
||||
@@ -894,18 +905,18 @@ fun ModuleItem(
|
||||
Icon(
|
||||
modifier = Modifier.size(20.dp).rotate(180f),
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = null,
|
||||
|
||||
)
|
||||
}
|
||||
if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 7.dp),
|
||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore)
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
//if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
|
||||
//Text(
|
||||
// modifier = Modifier.padding(start = 7.dp),
|
||||
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||
// text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore),
|
||||
// color = if (!module.remove) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onSecondaryContainer
|
||||
//)
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -932,4 +943,3 @@ fun ModuleItemPreview() {
|
||||
)
|
||||
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
|
||||
}
|
||||
|
||||
1376
manager/app/src/main/java/com/sukisu/ultra/ui/screen/MoreSettings.kt
Normal file
1376
manager/app/src/main/java/com/sukisu/ultra/ui/screen/MoreSettings.kt
Normal file
File diff suppressed because it is too large
Load Diff
769
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt
Normal file
769
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt
Normal file
@@ -0,0 +1,769 @@
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
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.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.edit
|
||||
import com.maxkeppeker.sheets.core.models.base.IconSource
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.sukisu.ultra.BuildConfig
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.*
|
||||
import com.sukisu.ultra.ui.component.*
|
||||
import com.sukisu.ultra.ui.theme.*
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||
import com.sukisu.ultra.ui.util.getBugreportFile
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import com.sukisu.ultra.ui.component.KsuIsValid
|
||||
import com.dergoogler.mmrl.platform.Platform
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
) { paddingValues ->
|
||||
val aboutDialog = rememberCustomDialog {
|
||||
AboutDialog(it)
|
||||
}
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/gzip")
|
||||
) { uri: Uri? ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
scope.launch(Dispatchers.IO) {
|
||||
loadingDialog.show()
|
||||
context.contentResolver.openOutputStream(uri)?.use { output ->
|
||||
getBugreportFile(context).inputStream().use {
|
||||
it.copyTo(output)
|
||||
}
|
||||
}
|
||||
loadingDialog.hide()
|
||||
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
||||
}
|
||||
}
|
||||
|
||||
// 配置
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.configuration),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
// 配置文件模板入口
|
||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
||||
KsuIsValid {
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Fence,
|
||||
title = profileTemplate,
|
||||
summary = stringResource(id = R.string.settings_profile_template_summary),
|
||||
onClick = {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 卸载模块开关
|
||||
var umountChecked by rememberSaveable {
|
||||
mutableStateOf(Natives.isDefaultUmountModules())
|
||||
}
|
||||
|
||||
KsuIsValid {
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.FolderDelete,
|
||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
||||
checked = umountChecked,
|
||||
onCheckedChange = {
|
||||
if (Natives.setDefaultUmountModules(it)) {
|
||||
umountChecked = it
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// SU 禁用开关(仅在兼容版本显示)
|
||||
KsuIsValid {
|
||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
||||
var isSuDisabled by rememberSaveable {
|
||||
mutableStateOf(!Natives.isSuEnabled())
|
||||
}
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.RemoveModerator,
|
||||
title = stringResource(id = R.string.settings_disable_su),
|
||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
||||
checked = isSuDisabled,
|
||||
onCheckedChange = { checked ->
|
||||
val shouldEnable = !checked
|
||||
if (Natives.setSuEnabled(shouldEnable)) {
|
||||
isSuDisabled = !shouldEnable
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应用设置
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.app_settings),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
|
||||
// 更新检查开关
|
||||
var checkUpdate by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("check_update", true)
|
||||
)
|
||||
}
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.Update,
|
||||
title = stringResource(id = R.string.settings_check_update),
|
||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
||||
checked = checkUpdate,
|
||||
onCheckedChange = {
|
||||
prefs.edit {putBoolean("check_update", it) }
|
||||
checkUpdate = it
|
||||
}
|
||||
)
|
||||
|
||||
// Web调试开关
|
||||
var enableWebDebugging by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("enable_web_debugging", false)
|
||||
)
|
||||
}
|
||||
KsuIsValid {
|
||||
SwitchSettingItem(
|
||||
icon = Icons.Filled.DeveloperMode,
|
||||
title = stringResource(id = R.string.enable_web_debugging),
|
||||
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
||||
checked = enableWebDebugging,
|
||||
onCheckedChange = {
|
||||
prefs.edit { putBoolean("enable_web_debugging", it) }
|
||||
enableWebDebugging = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Web X 开关
|
||||
var useWebUIX by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("use_webuix", false)
|
||||
)
|
||||
}
|
||||
KsuIsValid {
|
||||
SwitchItem(
|
||||
beta = true,
|
||||
enabled = Platform.isAlive,
|
||||
icon = Icons.Filled.WebAsset,
|
||||
title = stringResource(id = R.string.use_webuix),
|
||||
summary = stringResource(id = R.string.use_webuix_summary),
|
||||
checked = useWebUIX
|
||||
) {
|
||||
prefs.edit { putBoolean("use_webuix", it) }
|
||||
useWebUIX = it
|
||||
}
|
||||
}
|
||||
|
||||
// Web X Eruda 开关
|
||||
var useWebUIXEruda by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("use_webuix_eruda", false)
|
||||
)
|
||||
}
|
||||
KsuIsValid {
|
||||
AnimatedVisibility(
|
||||
visible = useWebUIX && enableWebDebugging,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically()
|
||||
) {
|
||||
SwitchItem(
|
||||
beta = true,
|
||||
enabled = Platform.isAlive && useWebUIX && enableWebDebugging,
|
||||
icon = Icons.Filled.FormatListNumbered,
|
||||
title = stringResource(id = R.string.use_webuix_eruda),
|
||||
summary = stringResource(id = R.string.use_webuix_eruda_summary),
|
||||
checked = useWebUIXEruda
|
||||
) {
|
||||
prefs.edit { putBoolean("use_webuix_eruda", it) }
|
||||
useWebUIXEruda = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更多设置
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Settings,
|
||||
title = stringResource(id = R.string.more_settings),
|
||||
summary = stringResource(id = R.string.more_settings),
|
||||
onClick = {
|
||||
navigator.navigate(MoreSettingsScreenDestination)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 工具
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.tools),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
var showBottomsheet by remember { mutableStateOf(false) }
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.BugReport,
|
||||
title = stringResource(id = R.string.send_log),
|
||||
onClick = {
|
||||
showBottomsheet = true
|
||||
}
|
||||
)
|
||||
|
||||
if (showBottomsheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomsheet = false },
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
LogActionButton(
|
||||
icon = Icons.Filled.Save,
|
||||
text = stringResource(R.string.save_log),
|
||||
onClick = {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||
val current = LocalDateTime.now().format(formatter)
|
||||
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
||||
showBottomsheet = false
|
||||
}
|
||||
)
|
||||
|
||||
LogActionButton(
|
||||
icon = Icons.Filled.Share,
|
||||
text = stringResource(R.string.send_log),
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val bugreport = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
getBugreportFile(context)
|
||||
}
|
||||
}
|
||||
|
||||
val uri: Uri =
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||
bugreport
|
||||
)
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
setDataAndType(uri, "application/gzip")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
shareIntent,
|
||||
context.getString(R.string.send_log)
|
||||
)
|
||||
)
|
||||
|
||||
showBottomsheet = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
||||
if (lkmMode) {
|
||||
UninstallItem(navigator) {
|
||||
loadingDialog.withLoading(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置分组卡片 - 关于
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.about),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Info,
|
||||
title = stringResource(R.string.about),
|
||||
onClick = {
|
||||
aboutDialog.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogActionButton(
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = text,
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
if (summary != null) {
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwitchSettingItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
summary: String? = null,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onCheckedChange(!checked) }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
if (summary != null) {
|
||||
Text(
|
||||
text = summary,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UninstallItem(
|
||||
navigator: DestinationsNavigator,
|
||||
withLoading: suspend (suspend () -> Unit) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val uninstallConfirmDialog = rememberConfirmDialog()
|
||||
val showTodo = {
|
||||
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val uninstallDialog = rememberUninstallDialog { uninstallType ->
|
||||
scope.launch {
|
||||
val result = uninstallConfirmDialog.awaitConfirm(
|
||||
title = context.getString(uninstallType.title),
|
||||
content = context.getString(uninstallType.message)
|
||||
)
|
||||
if (result == ConfirmResult.Confirmed) {
|
||||
withLoading {
|
||||
when (uninstallType) {
|
||||
UninstallType.TEMPORARY -> showTodo()
|
||||
UninstallType.PERMANENT -> navigator.navigate(
|
||||
FlashScreenDestination(FlashIt.FlashUninstall)
|
||||
)
|
||||
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
|
||||
FlashScreenDestination(FlashIt.FlashRestore)
|
||||
)
|
||||
UninstallType.NONE -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingItem(
|
||||
icon = Icons.Filled.Delete,
|
||||
title = stringResource(id = R.string.settings_uninstall),
|
||||
onClick = {
|
||||
uninstallDialog.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) {
|
||||
TEMPORARY(
|
||||
R.string.settings_uninstall_temporary,
|
||||
R.string.settings_uninstall_temporary_message,
|
||||
Icons.Filled.Delete
|
||||
),
|
||||
PERMANENT(
|
||||
R.string.settings_uninstall_permanent,
|
||||
R.string.settings_uninstall_permanent_message,
|
||||
Icons.Filled.DeleteForever
|
||||
),
|
||||
RESTORE_STOCK_IMAGE(
|
||||
R.string.settings_restore_stock_image,
|
||||
R.string.settings_restore_stock_image_message,
|
||||
Icons.AutoMirrored.Filled.Undo
|
||||
),
|
||||
NONE(0, 0, Icons.Filled.Delete)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
return rememberCustomDialog { dismiss ->
|
||||
val options = listOf(
|
||||
// UninstallType.TEMPORARY,
|
||||
UninstallType.PERMANENT,
|
||||
UninstallType.RESTORE_STOCK_IMAGE
|
||||
)
|
||||
val listOptions = options.map {
|
||||
ListOption(
|
||||
titleText = stringResource(it.title),
|
||||
subtitleText = if (it.message != 0) stringResource(it.message) else null,
|
||||
icon = IconSource(it.icon)
|
||||
)
|
||||
}
|
||||
|
||||
var selection = UninstallType.NONE
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.settings_uninstall),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
listOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.clickable {
|
||||
selection = options[index]
|
||||
}
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = options[index].icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = option.titleText,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
option.subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (selection != UninstallType.NONE) {
|
||||
onSelected(selection)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.ok),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.cancel),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
containerColor = cardColor,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
tonalElevation = 4.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val systemIsDark = isSystemInDarkTheme()
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = if (ThemeConfig.customBackgroundUri != null) {
|
||||
cardAlpha
|
||||
} else {
|
||||
if (systemIsDark) 0.8f else 1f
|
||||
}
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.settings),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,878 @@
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.*
|
||||
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.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.sukisu.ultra.R
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||
import com.sukisu.ultra.ui.util.ModuleModify
|
||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
val viewModel = viewModel<SuperUserViewModel>()
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val listState = rememberLazyListState()
|
||||
val context = LocalContext.current
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
|
||||
// 添加备份和还原启动器
|
||||
val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState)
|
||||
val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState)
|
||||
|
||||
LaunchedEffect(key1 = navigator) {
|
||||
viewModel.search = ""
|
||||
if (viewModel.appList.isEmpty()) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(viewModel.search) {
|
||||
if (viewModel.search.isEmpty()) {
|
||||
// 取消自动滚动到顶部的行为
|
||||
// listState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchAppBar(
|
||||
title = { Text(stringResource(R.string.superuser)) },
|
||||
searchText = viewModel.search,
|
||||
onSearchTextChange = { viewModel.search = it },
|
||||
onClearClick = { viewModel.search = "" },
|
||||
dropdownContent = {
|
||||
var showDropdown by remember { mutableStateOf(false) }
|
||||
|
||||
IconButton(
|
||||
onClick = { showDropdown = true },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(id = R.string.settings),
|
||||
)
|
||||
|
||||
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
||||
showDropdown = false
|
||||
}) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.refresh)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Refresh,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
if (viewModel.showSystemApps) {
|
||||
stringResource(R.string.hide_system_apps)
|
||||
} else {
|
||||
stringResource(R.string.show_system_apps)
|
||||
}
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = if (viewModel.showSystemApps)
|
||||
Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
HorizontalDivider(thickness = 0.5.dp, modifier = Modifier.padding(vertical = 4.dp))
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.backup_allowlist)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.restore_allowlist)) },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.RestoreFromTrash,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
|
||||
showDropdown = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHostState) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
floatingActionButton = {
|
||||
// 侧边悬浮按钮集合
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
// 批量操作相关按钮
|
||||
// 只有在批量模式且有选中应用时才显示批量操作按钮
|
||||
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
||||
// 取消按钮
|
||||
val cancelInteractionSource = remember { MutableInteractionSource() }
|
||||
val isCancelPressed by cancelInteractionSource.collectIsPressedAsState()
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
viewModel.selectedApps = emptySet()
|
||||
viewModel.showBatchActions = false
|
||||
},
|
||||
modifier = Modifier.size(if (isCancelPressed) 56.dp else 40.dp),
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
shape = CircleShape,
|
||||
interactionSource = cancelInteractionSource,
|
||||
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Close,
|
||||
contentDescription = stringResource(android.R.string.cancel),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isCancelPressed,
|
||||
enter = expandHorizontally() + fadeIn(),
|
||||
exit = shrinkHorizontally() + fadeOut()
|
||||
) {
|
||||
Text(
|
||||
stringResource(android.R.string.cancel),
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取消授权按钮
|
||||
val unauthorizeInteractionSource = remember { MutableInteractionSource() }
|
||||
val isUnauthorizePressed by unauthorizeInteractionSource.collectIsPressedAsState()
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(false)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(if (isUnauthorizePressed) 56.dp else 40.dp),
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||
shape = CircleShape,
|
||||
interactionSource = unauthorizeInteractionSource,
|
||||
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Block,
|
||||
contentDescription = stringResource(R.string.batch_cancel_authorization),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isUnauthorizePressed,
|
||||
enter = expandHorizontally() + fadeIn(),
|
||||
exit = shrinkHorizontally() + fadeOut()
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.batch_cancel_authorization),
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 授权按钮
|
||||
val authorizeInteractionSource = remember { MutableInteractionSource() }
|
||||
val isAuthorizePressed by authorizeInteractionSource.collectIsPressedAsState()
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(true)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(if (isAuthorizePressed) 56.dp else 40.dp),
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
shape = CircleShape,
|
||||
interactionSource = authorizeInteractionSource,
|
||||
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Check,
|
||||
contentDescription = stringResource(R.string.batch_authorization),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isAuthorizePressed,
|
||||
enter = expandHorizontally() + fadeIn(),
|
||||
exit = shrinkHorizontally() + fadeOut()
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.batch_authorization),
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加分隔
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
||||
|
||||
// 在批量操作按钮组中添加卸载模块的按钮
|
||||
// 卸载模块启用按钮
|
||||
val umountEnableInteractionSource = remember { MutableInteractionSource() }
|
||||
val isUmountEnablePressed by umountEnableInteractionSource.collectIsPressedAsState()
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(
|
||||
allowSu = false, // 不改变ROOT权限状态
|
||||
umountModules = true // 启用卸载模块
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(if (isUmountEnablePressed) 56.dp else 40.dp),
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
shape = CircleShape,
|
||||
interactionSource = umountEnableInteractionSource,
|
||||
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.FolderOff,
|
||||
contentDescription = stringResource(R.string.profile_umount_modules),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isUmountEnablePressed,
|
||||
enter = expandHorizontally() + fadeIn(),
|
||||
exit = shrinkHorizontally() + fadeOut()
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.profile_umount_modules),
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 卸载模块禁用按钮
|
||||
val umountDisableInteractionSource = remember { MutableInteractionSource() }
|
||||
val isUmountDisablePressed by umountDisableInteractionSource.collectIsPressedAsState()
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(
|
||||
allowSu = false, // 不改变ROOT权限状态
|
||||
umountModules = false // 禁用卸载模块
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(if (isUmountDisablePressed) 56.dp else 40.dp),
|
||||
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||
shape = CircleShape,
|
||||
interactionSource = umountDisableInteractionSource,
|
||||
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Folder,
|
||||
contentDescription = stringResource(R.string.profile_umount_modules_disable),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isUmountDisablePressed,
|
||||
enter = expandHorizontally() + fadeIn(),
|
||||
exit = shrinkHorizontally() + fadeOut()
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.profile_umount_modules_disable),
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
// 添加分隔
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
// 向上导航按钮
|
||||
val topBtnInteractionSource = remember { MutableInteractionSource() }
|
||||
val isTopBtnPressed by topBtnInteractionSource.collectIsPressedAsState()
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
listState.animateScrollToItem(0)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(if (isTopBtnPressed) 56.dp else 40.dp),
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
shape = CircleShape,
|
||||
interactionSource = topBtnInteractionSource,
|
||||
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||
contentDescription = stringResource(R.string.scroll_to_top_description),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isTopBtnPressed,
|
||||
enter = expandHorizontally() + fadeIn(),
|
||||
exit = shrinkHorizontally() + fadeOut()
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.scroll_to_top),
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 向下导航按钮
|
||||
val bottomBtnInteractionSource = remember { MutableInteractionSource() }
|
||||
val isBottomBtnPressed by bottomBtnInteractionSource.collectIsPressedAsState()
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val lastIndex = viewModel.appList.size - 1
|
||||
if (lastIndex >= 0) {
|
||||
listState.animateScrollToItem(lastIndex)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(if (isBottomBtnPressed) 56.dp else 40.dp),
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
shape = CircleShape,
|
||||
interactionSource = bottomBtnInteractionSource,
|
||||
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.KeyboardArrowDown,
|
||||
contentDescription = stringResource(R.string.scroll_to_bottom_description),
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isBottomBtnPressed,
|
||||
enter = expandHorizontally() + fadeIn(),
|
||||
exit = shrinkHorizontally() + fadeOut()
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.scroll_to_bottom),
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
PullToRefreshBox(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
onRefresh = {
|
||||
scope.launch { viewModel.fetchAppList() }
|
||||
},
|
||||
isRefreshing = viewModel.isRefreshing
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
contentPadding = PaddingValues(
|
||||
top = 8.dp,
|
||||
bottom = 16.dp
|
||||
)
|
||||
) {
|
||||
// 获取分组后的应用列表
|
||||
val rootApps = viewModel.appList.filter { it.allowSu }
|
||||
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
|
||||
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
|
||||
|
||||
// 显示ROOT权限应用组
|
||||
if (rootApps.isNotEmpty()) {
|
||||
item {
|
||||
GroupHeader(title = stringResource(R.string.apps_with_root))
|
||||
}
|
||||
|
||||
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||
onSwitchChange = { allowSu ->
|
||||
scope.launch {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
// 不重新获取应用列表,避免滚动位置重置
|
||||
// viewModel.fetchAppList()
|
||||
// 仅更新当前应用的配置
|
||||
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.showBatchActions) {
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
} else {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// 长按进入多选模式
|
||||
if (!viewModel.showBatchActions) {
|
||||
viewModel.toggleBatchMode()
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
}
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示自定义配置应用组
|
||||
if (customApps.isNotEmpty()) {
|
||||
item {
|
||||
GroupHeader(title = stringResource(R.string.apps_with_custom_profile))
|
||||
}
|
||||
|
||||
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||
onSwitchChange = { allowSu ->
|
||||
scope.launch {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
// 不重新获取应用列表,避免滚动位置重置
|
||||
// viewModel.fetchAppList()
|
||||
// 仅更新当前应用的配置
|
||||
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.showBatchActions) {
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
} else {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// 长按进入多选模式
|
||||
if (!viewModel.showBatchActions) {
|
||||
viewModel.toggleBatchMode()
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
}
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示其他应用组
|
||||
if (otherApps.isNotEmpty()) {
|
||||
item {
|
||||
GroupHeader(title = stringResource(R.string.other_apps))
|
||||
}
|
||||
|
||||
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||
onSwitchChange = { allowSu ->
|
||||
scope.launch {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
// 不重新获取应用列表,避免滚动位置重置
|
||||
// viewModel.fetchAppList()
|
||||
// 仅更新当前应用的配置
|
||||
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.showBatchActions) {
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
} else {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// 长按进入多选模式
|
||||
if (!viewModel.showBatchActions) {
|
||||
viewModel.toggleBatchMode()
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
}
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 当没有应用显示时显示空状态
|
||||
if (viewModel.appList.isEmpty()) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(400.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Apps,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||
modifier = Modifier
|
||||
.size(96.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.no_apps_found),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupHeader(title: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.7f))
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun AppItem(
|
||||
app: SuperUserViewModel.AppInfo,
|
||||
isSelected: Boolean,
|
||||
onToggleSelection: () -> Unit,
|
||||
onSwitchChange: (Boolean) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
viewModel: SuperUserViewModel
|
||||
) {
|
||||
val cardColor = if (app.allowSu)
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
else if (app.hasCustomProfile)
|
||||
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f)
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceContainerLow
|
||||
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(containerColor = cardColor),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.shadow(
|
||||
elevation = 0.dp,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||
)
|
||||
.then(
|
||||
if (isSelected)
|
||||
Modifier.border(
|
||||
width = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
else
|
||||
Modifier
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = { onLongClick() },
|
||||
onTap = { onClick() }
|
||||
)
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(app.packageInfo)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = app.label,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(48.dp)
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = app.label,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Text(
|
||||
text = app.packageName,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (app.allowSu) {
|
||||
LabelText(label = "ROOT", backgroundColor = MaterialTheme.colorScheme.primary)
|
||||
}
|
||||
if (Natives.uidShouldUmount(app.uid)) {
|
||||
LabelText(label = "UMOUNT", backgroundColor = MaterialTheme.colorScheme.tertiary)
|
||||
}
|
||||
if (app.hasCustomProfile) {
|
||||
LabelText(label = "CUSTOM", backgroundColor = MaterialTheme.colorScheme.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!viewModel.showBatchActions) {
|
||||
// 开关交互源
|
||||
val switchInteractionSource = remember { MutableInteractionSource() }
|
||||
val isSwitchPressed by switchInteractionSource.collectIsPressedAsState()
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isSwitchPressed,
|
||||
enter = expandHorizontally() + fadeIn(),
|
||||
exit = shrinkHorizontally() + fadeOut()
|
||||
) {
|
||||
Text(
|
||||
text = if (app.allowSu) stringResource(R.string.authorized) else stringResource(R.string.unauthorized),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = if (app.allowSu) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
|
||||
modifier = Modifier.padding(end = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Switch(
|
||||
checked = app.allowSu,
|
||||
onCheckedChange = onSwitchChange,
|
||||
interactionSource = switchInteractionSource,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// 复选框交互源
|
||||
val checkboxInteractionSource = remember { MutableInteractionSource() }
|
||||
val isCheckboxPressed by checkboxInteractionSource.collectIsPressedAsState()
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isCheckboxPressed,
|
||||
enter = expandHorizontally() + fadeIn(),
|
||||
exit = shrinkHorizontally() + fadeOut()
|
||||
) {
|
||||
Text(
|
||||
text = if (isSelected) stringResource(R.string.selected) else stringResource(R.string.select),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
|
||||
modifier = Modifier.padding(end = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onToggleSelection() },
|
||||
interactionSource = checkboxInteractionSource,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LabelText(label: String, backgroundColor: Color) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp, end = 2.dp)
|
||||
.background(
|
||||
backgroundColor,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp),
|
||||
style = TextStyle(
|
||||
fontSize = 10.sp,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import LabelText
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -31,6 +34,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
@@ -44,11 +48,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
@@ -59,9 +62,9 @@ import com.ramcosta.composedestinations.result.ResultRecipient
|
||||
import com.ramcosta.composedestinations.result.getOr
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.theme.ThemeConfig
|
||||
import zako.zako.zako.ui.viewmodel.TemplateViewModel
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.theme.CardConfig
|
||||
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
@@ -78,11 +81,6 @@ fun AppProfileTemplateScreen(
|
||||
val viewModel = viewModel<TemplateViewModel>()
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (viewModel.templateList.isEmpty()) {
|
||||
@@ -97,10 +95,13 @@ fun AppProfileTemplateScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val cardColorUse = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = context.getSystemService<ClipboardManager>()
|
||||
val showToast = fun(msg: String) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||
@@ -108,25 +109,29 @@ fun AppProfileTemplateScreen(
|
||||
}
|
||||
TopBar(
|
||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColorUse.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColorUse.copy(alpha = cardAlpha)
|
||||
),
|
||||
onSync = {
|
||||
scope.launch { viewModel.fetchTemplates(true) }
|
||||
},
|
||||
onImport = {
|
||||
clipboardManager.getText()?.text?.let {
|
||||
if (it.isEmpty()) {
|
||||
showToast(context.getString(R.string.app_profile_template_import_empty))
|
||||
return@let
|
||||
}
|
||||
scope.launch {
|
||||
val clipboardText = clipboardManager?.primaryClip?.getItemAt(0)?.text?.toString()
|
||||
if (clipboardText.isNullOrEmpty()) {
|
||||
showToast(context.getString(R.string.app_profile_template_import_empty))
|
||||
return@launch
|
||||
}
|
||||
viewModel.importTemplates(
|
||||
it, {
|
||||
clipboardText,
|
||||
{
|
||||
showToast(context.getString(R.string.app_profile_template_import_success))
|
||||
viewModel.fetchTemplates(false)
|
||||
},
|
||||
showToast
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
onExport = {
|
||||
scope.launch {
|
||||
@@ -134,8 +139,8 @@ fun AppProfileTemplateScreen(
|
||||
{
|
||||
showToast(context.getString(R.string.app_profile_template_export_empty))
|
||||
}
|
||||
) {
|
||||
clipboardManager.setText(AnnotatedString(it))
|
||||
) { text ->
|
||||
clipboardManager?.setPrimaryClip(ClipData.newPlainText("", text))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -154,7 +159,6 @@ fun AppProfileTemplateScreen(
|
||||
},
|
||||
icon = { Icon(Icons.Filled.Add, null) },
|
||||
text = { Text(stringResource(id = R.string.app_profile_template_create)) },
|
||||
containerColor = cardColor.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
},
|
||||
@@ -204,17 +208,17 @@ private fun TemplateItem(
|
||||
)
|
||||
Text(template.description)
|
||||
FlowRow {
|
||||
LabelText(label = "UID: ${template.uid}")
|
||||
LabelText(label = "GID: ${template.gid}")
|
||||
LabelText(label = template.context)
|
||||
LabelText(label = "UID: ${template.uid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = "GID: ${template.gid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
LabelText(label = template.context, backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
if (template.local) {
|
||||
LabelText(label = "local")
|
||||
LabelText(label = "local", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
} else {
|
||||
LabelText(label = "remote")
|
||||
LabelText(label = "remote", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -225,12 +229,20 @@ private fun TopBar(
|
||||
onSync: () -> Unit = {},
|
||||
onImport: () -> Unit = {},
|
||||
onExport: () -> Unit = {},
|
||||
colors: TopAppBarColors,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.settings_profile_template))
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
package com.sukisu.ultra.ui.screen
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
@@ -47,14 +47,14 @@ import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.result.ResultBackNavigator
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.component.profile.RootProfileConfig
|
||||
import zako.zako.zako.ui.util.deleteAppProfileTemplate
|
||||
import zako.zako.zako.ui.util.getAppProfileTemplate
|
||||
import zako.zako.zako.ui.util.setAppProfileTemplate
|
||||
import zako.zako.zako.ui.viewmodel.TemplateViewModel
|
||||
import zako.zako.zako.ui.viewmodel.toJSON
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.R
|
||||
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
||||
import com.sukisu.ultra.ui.util.deleteAppProfileTemplate
|
||||
import com.sukisu.ultra.ui.util.getAppProfileTemplate
|
||||
import com.sukisu.ultra.ui.util.setAppProfileTemplate
|
||||
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
|
||||
import com.sukisu.ultra.ui.viewmodel.toJSON
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.sukisu.ultra.ui.theme
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object CardConfig {
|
||||
val settingElevation: Dp = 4.dp
|
||||
val customBackgroundElevation: Dp = 0.dp
|
||||
|
||||
// 卡片透明度
|
||||
var cardAlpha by mutableFloatStateOf(1f)
|
||||
// 卡片亮度
|
||||
var cardDim by mutableFloatStateOf(0f)
|
||||
// 卡片阴影
|
||||
var cardElevation by mutableStateOf(settingElevation)
|
||||
var isShadowEnabled by mutableStateOf(true)
|
||||
var isCustomAlphaSet by mutableStateOf(false)
|
||||
var isCustomDimSet by mutableStateOf(false)
|
||||
var isUserDarkModeEnabled by mutableStateOf(false)
|
||||
var isUserLightModeEnabled by mutableStateOf(false)
|
||||
var isCustomBackgroundEnabled by mutableStateOf(false)
|
||||
|
||||
/**
|
||||
* 保存卡片配置到SharedPreferences
|
||||
*/
|
||||
fun save(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
prefs.edit().apply {
|
||||
putFloat("card_alpha", cardAlpha)
|
||||
putFloat("card_dim", cardDim)
|
||||
putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
|
||||
putBoolean("is_shadow_enabled", isShadowEnabled)
|
||||
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
||||
putBoolean("is_custom_dim_set", isCustomDimSet)
|
||||
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
||||
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SharedPreferences加载卡片配置
|
||||
*/
|
||||
fun load(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
cardAlpha = prefs.getFloat("card_alpha", 1f)
|
||||
cardDim = prefs.getFloat("card_dim", 0f)
|
||||
isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false)
|
||||
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true)
|
||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
||||
isCustomDimSet = prefs.getBoolean("is_custom_dim_set", false)
|
||||
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
||||
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
||||
updateShadowEnabled(isShadowEnabled)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新阴影启用状态
|
||||
*/
|
||||
fun updateShadowEnabled(enabled: Boolean) {
|
||||
isShadowEnabled = enabled
|
||||
cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) {
|
||||
customBackgroundElevation
|
||||
} else if (enabled) {
|
||||
settingElevation
|
||||
} else {
|
||||
customBackgroundElevation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置深色模式默认值
|
||||
*/
|
||||
fun setDarkModeDefaults() {
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = 0.70f
|
||||
}
|
||||
if (!isCustomDimSet) {
|
||||
cardDim = 0.5f
|
||||
}
|
||||
updateShadowEnabled(isShadowEnabled)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浅色模式默认值
|
||||
*/
|
||||
fun setLightModeDefaults() {
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = 1f
|
||||
}
|
||||
if (!isCustomDimSet) {
|
||||
cardDim = 0f
|
||||
}
|
||||
updateShadowEnabled(isShadowEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取卡片颜色配置
|
||||
*/
|
||||
@Composable
|
||||
fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
|
||||
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
|
||||
contentColor = determineContentColor(originalColor)
|
||||
)
|
||||
|
||||
/**
|
||||
* 根据背景颜色、主题模式和用户设置确定内容颜色
|
||||
*/
|
||||
@Composable
|
||||
private fun determineContentColor(originalColor: Color): Color {
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
if (ThemeConfig.isThemeChanging) {
|
||||
return if (isDarkTheme) Color.White else Color.Black
|
||||
}
|
||||
|
||||
return when {
|
||||
CardConfig.isUserLightModeEnabled -> Color.Black
|
||||
!isDarkTheme && originalColor.luminance() > 0.5f -> Color.Black
|
||||
isDarkTheme -> Color.White
|
||||
else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White
|
||||
}
|
||||
}
|
||||
273
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt
Normal file
273
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt
Normal file
@@ -0,0 +1,273 @@
|
||||
package com.sukisu.ultra.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
sealed class ThemeColors {
|
||||
abstract val Primary: Color
|
||||
abstract val Secondary: Color
|
||||
abstract val Tertiary: Color
|
||||
abstract val OnPrimary: Color
|
||||
abstract val OnSecondary: Color
|
||||
abstract val OnTertiary: Color
|
||||
abstract val PrimaryContainer: Color
|
||||
abstract val SecondaryContainer: Color
|
||||
abstract val TertiaryContainer: Color
|
||||
abstract val OnPrimaryContainer: Color
|
||||
abstract val OnSecondaryContainer: Color
|
||||
abstract val OnTertiaryContainer: Color
|
||||
abstract val ButtonContrast: Color
|
||||
|
||||
// 表面颜色
|
||||
abstract val Surface: Color
|
||||
abstract val SurfaceVariant: Color
|
||||
abstract val OnSurface: Color
|
||||
abstract val OnSurfaceVariant: Color
|
||||
|
||||
// 错误状态颜色
|
||||
abstract val Error: Color
|
||||
abstract val OnError: Color
|
||||
abstract val ErrorContainer: Color
|
||||
abstract val OnErrorContainer: Color
|
||||
|
||||
// 边框和背景色
|
||||
abstract val Outline: Color
|
||||
abstract val OutlineVariant: Color
|
||||
abstract val Background: Color
|
||||
abstract val OnBackground: Color
|
||||
|
||||
// 默认主题 (蓝色)
|
||||
object Default : ThemeColors() {
|
||||
override val Primary = Color(0xFF2196F3)
|
||||
override val Secondary = Color(0xFF64B5F6)
|
||||
override val Tertiary = Color(0xFF0D47A1)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFD6EAFF)
|
||||
override val SecondaryContainer = Color(0xFFE3F2FD)
|
||||
override val TertiaryContainer = Color(0xFFCFD8DC)
|
||||
override val OnPrimaryContainer = Color(0xFF0A3049)
|
||||
override val OnSecondaryContainer = Color(0xFF0D3C61)
|
||||
override val OnTertiaryContainer = Color(0xFF071D41)
|
||||
override val ButtonContrast = Color(0xFF2196F3)
|
||||
|
||||
override val Surface = Color(0xFFF5F9FF)
|
||||
override val SurfaceVariant = Color(0xFFEDF5FE)
|
||||
override val OnSurface = Color(0xFF1A1C1E)
|
||||
override val OnSurfaceVariant = Color(0xFF42474E)
|
||||
|
||||
override val Error = Color(0xFFB00020)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFDE7E9)
|
||||
override val OnErrorContainer = Color(0xFF410008)
|
||||
|
||||
override val Outline = Color(0xFFBAC3CF)
|
||||
override val OutlineVariant = Color(0xFFDFE3EB)
|
||||
override val Background = Color(0xFFFAFCFF)
|
||||
override val OnBackground = Color(0xFF1A1C1E)
|
||||
}
|
||||
|
||||
// 绿色主题
|
||||
object Green : ThemeColors() {
|
||||
override val Primary = Color(0xFF43A047)
|
||||
override val Secondary = Color(0xFF66BB6A)
|
||||
override val Tertiary = Color(0xFF1B5E20)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFD8EFDB)
|
||||
override val SecondaryContainer = Color(0xFFE8F5E9)
|
||||
override val TertiaryContainer = Color(0xFFB9F6CA)
|
||||
override val OnPrimaryContainer = Color(0xFF0A280D)
|
||||
override val OnSecondaryContainer = Color(0xFF0E2912)
|
||||
override val OnTertiaryContainer = Color(0xFF051B07)
|
||||
override val ButtonContrast = Color(0xFF43A047)
|
||||
|
||||
override val Surface = Color(0xFFF6FBF6)
|
||||
override val SurfaceVariant = Color(0xFFEDF7EE)
|
||||
override val OnSurface = Color(0xFF191C19)
|
||||
override val OnSurfaceVariant = Color(0xFF414941)
|
||||
|
||||
override val Error = Color(0xFFC62828)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFF8D7DA)
|
||||
override val OnErrorContainer = Color(0xFF4A0808)
|
||||
|
||||
override val Outline = Color(0xFFBDC9BF)
|
||||
override val OutlineVariant = Color(0xFFDDE6DE)
|
||||
override val Background = Color(0xFFFBFDFB)
|
||||
override val OnBackground = Color(0xFF191C19)
|
||||
}
|
||||
|
||||
// 紫色主题
|
||||
object Purple : ThemeColors() {
|
||||
override val Primary = Color(0xFF9C27B0)
|
||||
override val Secondary = Color(0xFFBA68C8)
|
||||
override val Tertiary = Color(0xFF6A1B9A)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFF3D8F8)
|
||||
override val SecondaryContainer = Color(0xFFF5E9F7)
|
||||
override val TertiaryContainer = Color(0xFFE1BEE7)
|
||||
override val OnPrimaryContainer = Color(0xFF2A0934)
|
||||
override val OnSecondaryContainer = Color(0xFF3C0F50)
|
||||
override val OnTertiaryContainer = Color(0xFF1D0830)
|
||||
override val ButtonContrast = Color(0xFF9C27B0)
|
||||
|
||||
override val Surface = Color(0xFFFCF6FF)
|
||||
override val SurfaceVariant = Color(0xFFF5EEFA)
|
||||
override val OnSurface = Color(0xFF1D1B1E)
|
||||
override val OnSurfaceVariant = Color(0xFF49454E)
|
||||
|
||||
override val Error = Color(0xFFD50000)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDCD5)
|
||||
override val OnErrorContainer = Color(0xFF480000)
|
||||
|
||||
override val Outline = Color(0xFFC9B9D0)
|
||||
override val OutlineVariant = Color(0xFFE8DAED)
|
||||
override val Background = Color(0xFFFFFBFF)
|
||||
override val OnBackground = Color(0xFF1D1B1E)
|
||||
}
|
||||
|
||||
// 橙色主题
|
||||
object Orange : ThemeColors() {
|
||||
override val Primary = Color(0xFFFF9800)
|
||||
override val Secondary = Color(0xFFFFB74D)
|
||||
override val Tertiary = Color(0xFFE65100)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFF000000)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFFECCC)
|
||||
override val SecondaryContainer = Color(0xFFFFF0D9)
|
||||
override val TertiaryContainer = Color(0xFFFFD180)
|
||||
override val OnPrimaryContainer = Color(0xFF351F00)
|
||||
override val OnSecondaryContainer = Color(0xFF3D2800)
|
||||
override val OnTertiaryContainer = Color(0xFF2E1500)
|
||||
override val ButtonContrast = Color(0xFFFF9800)
|
||||
|
||||
override val Surface = Color(0xFFFFF8F3)
|
||||
override val SurfaceVariant = Color(0xFFFFF0E6)
|
||||
override val OnSurface = Color(0xFF1F1B16)
|
||||
override val OnSurfaceVariant = Color(0xFF4E4639)
|
||||
|
||||
override val Error = Color(0xFFD32F2F)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDBC8)
|
||||
override val OnErrorContainer = Color(0xFF490700)
|
||||
|
||||
override val Outline = Color(0xFFD6C3AD)
|
||||
override val OutlineVariant = Color(0xFFEFDFCC)
|
||||
override val Background = Color(0xFFFFFBFF)
|
||||
override val OnBackground = Color(0xFF1F1B16)
|
||||
}
|
||||
|
||||
// 粉色主题
|
||||
object Pink : ThemeColors() {
|
||||
override val Primary = Color(0xFFE91E63)
|
||||
override val Secondary = Color(0xFFF06292)
|
||||
override val Tertiary = Color(0xFF880E4F)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFCE4EC)
|
||||
override val SecondaryContainer = Color(0xFFFCE4EC)
|
||||
override val TertiaryContainer = Color(0xFFF8BBD0)
|
||||
override val OnPrimaryContainer = Color(0xFF3B0819)
|
||||
override val OnSecondaryContainer = Color(0xFF3B0819)
|
||||
override val OnTertiaryContainer = Color(0xFF2B0516)
|
||||
override val ButtonContrast = Color(0xFFE91E63)
|
||||
|
||||
override val Surface = Color(0xFFFFF7F9)
|
||||
override val SurfaceVariant = Color(0xFFFCEEF2)
|
||||
override val OnSurface = Color(0xFF201A1C)
|
||||
override val OnSurfaceVariant = Color(0xFF534347)
|
||||
|
||||
override val Error = Color(0xFFB71C1C)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||
override val OnErrorContainer = Color(0xFF410002)
|
||||
|
||||
override val Outline = Color(0xFFD6BABF)
|
||||
override val OutlineVariant = Color(0xFFEFDDE0)
|
||||
override val Background = Color(0xFFFFFBFF)
|
||||
override val OnBackground = Color(0xFF201A1C)
|
||||
}
|
||||
|
||||
// 灰色主题
|
||||
object Gray : ThemeColors() {
|
||||
override val Primary = Color(0xFF607D8B)
|
||||
override val Secondary = Color(0xFF90A4AE)
|
||||
override val Tertiary = Color(0xFF455A64)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFECEFF1)
|
||||
override val SecondaryContainer = Color(0xFFECEFF1)
|
||||
override val TertiaryContainer = Color(0xFFCFD8DC)
|
||||
override val OnPrimaryContainer = Color(0xFF1A2327)
|
||||
override val OnSecondaryContainer = Color(0xFF1A2327)
|
||||
override val OnTertiaryContainer = Color(0xFF121A1D)
|
||||
override val ButtonContrast = Color(0xFF607D8B)
|
||||
|
||||
override val Surface = Color(0xFFF6F9FB)
|
||||
override val SurfaceVariant = Color(0xFFEEF2F4)
|
||||
override val OnSurface = Color(0xFF191C1E)
|
||||
override val OnSurfaceVariant = Color(0xFF41484D)
|
||||
|
||||
override val Error = Color(0xFFC62828)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||
override val OnErrorContainer = Color(0xFF410002)
|
||||
|
||||
override val Outline = Color(0xFFBDC1C4)
|
||||
override val OutlineVariant = Color(0xFFDDE1E3)
|
||||
override val Background = Color(0xFFFBFCFE)
|
||||
override val OnBackground = Color(0xFF191C1E)
|
||||
}
|
||||
|
||||
// 黄色主题
|
||||
object Yellow : ThemeColors() {
|
||||
override val Primary = Color(0xFFFFC107)
|
||||
override val Secondary = Color(0xFFFFD54F)
|
||||
override val Tertiary = Color(0xFFFF8F00)
|
||||
override val OnPrimary = Color(0xFF000000)
|
||||
override val OnSecondary = Color(0xFF000000)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFFF8E1)
|
||||
override val SecondaryContainer = Color(0xFFFFF8E1)
|
||||
override val TertiaryContainer = Color(0xFFFFECB3)
|
||||
override val OnPrimaryContainer = Color(0xFF332A00)
|
||||
override val OnSecondaryContainer = Color(0xFF332A00)
|
||||
override val OnTertiaryContainer = Color(0xFF221200)
|
||||
override val ButtonContrast = Color(0xFFFFC107)
|
||||
|
||||
override val Surface = Color(0xFFFFFAF3)
|
||||
override val SurfaceVariant = Color(0xFFFFF7E6)
|
||||
override val OnSurface = Color(0xFF1F1C17)
|
||||
override val OnSurfaceVariant = Color(0xFF4E4A3C)
|
||||
|
||||
override val Error = Color(0xFFB71C1C)
|
||||
override val OnError = Color(0xFFFFFFFF)
|
||||
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||
override val OnErrorContainer = Color(0xFF410002)
|
||||
|
||||
override val Outline = Color(0xFFD1C8AF)
|
||||
override val OutlineVariant = Color(0xFFEEE8D7)
|
||||
override val Background = Color(0xFFFFFCF8)
|
||||
override val OnBackground = Color(0xFF1F1C17)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
|
||||
"green" -> Green
|
||||
"purple" -> Purple
|
||||
"orange" -> Orange
|
||||
"pink" -> Pink
|
||||
"gray" -> Gray
|
||||
"yellow" -> Yellow
|
||||
else -> Default
|
||||
}
|
||||
}
|
||||
}
|
||||
584
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt
Normal file
584
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,584 @@
|
||||
package com.sukisu.ultra.ui.theme
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.paint
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.zIndex
|
||||
import coil.compose.AsyncImagePainter
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
|
||||
/**
|
||||
* 主题配置对象,管理应用的主题相关状态
|
||||
*/
|
||||
object ThemeConfig {
|
||||
var customBackgroundUri by mutableStateOf<Uri?>(null)
|
||||
var forceDarkMode by mutableStateOf<Boolean?>(null)
|
||||
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
|
||||
var useDynamicColor by mutableStateOf(false)
|
||||
var backgroundImageLoaded by mutableStateOf(false)
|
||||
var needsResetOnThemeChange by mutableStateOf(false)
|
||||
var isThemeChanging by mutableStateOf(false)
|
||||
var preventBackgroundRefresh by mutableStateOf(false)
|
||||
|
||||
private var lastDarkModeState: Boolean? = null
|
||||
fun detectThemeChange(currentDarkMode: Boolean): Boolean {
|
||||
val isChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode
|
||||
lastDarkModeState = currentDarkMode
|
||||
return isChanged
|
||||
}
|
||||
|
||||
fun resetBackgroundState() {
|
||||
if (!preventBackgroundRefresh) {
|
||||
backgroundImageLoaded = false
|
||||
}
|
||||
isThemeChanging = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用主题
|
||||
*/
|
||||
@Composable
|
||||
fun KernelSUTheme(
|
||||
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
|
||||
true -> true
|
||||
false -> false
|
||||
null -> isSystemInDarkTheme()
|
||||
},
|
||||
dynamicColor: Boolean = ThemeConfig.useDynamicColor,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val systemIsDark = isSystemInDarkTheme()
|
||||
|
||||
// 检测系统主题变化并保存状态
|
||||
val themeChanged = ThemeConfig.detectThemeChange(systemIsDark)
|
||||
LaunchedEffect(systemIsDark, themeChanged) {
|
||||
if (ThemeConfig.forceDarkMode == null && themeChanged) {
|
||||
Log.d("ThemeSystem", "系统主题变化检测: 从 ${!systemIsDark} 变为 $systemIsDark")
|
||||
ThemeConfig.resetBackgroundState()
|
||||
|
||||
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||
context.loadCustomBackground()
|
||||
}
|
||||
|
||||
CardConfig.apply {
|
||||
load(context)
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = if (systemIsDark) 0.50f else 1f
|
||||
}
|
||||
if (!isCustomDimSet) {
|
||||
cardDim = if (systemIsDark) 0.5f else 0f
|
||||
}
|
||||
save(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SystemBarStyle(
|
||||
darkMode = darkTheme
|
||||
)
|
||||
|
||||
// 初始加载配置
|
||||
LaunchedEffect(Unit) {
|
||||
context.loadThemeMode()
|
||||
context.loadThemeColors()
|
||||
context.loadDynamicColorState()
|
||||
CardConfig.load(context)
|
||||
|
||||
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||
context.loadCustomBackground()
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
}
|
||||
|
||||
ThemeConfig.preventBackgroundRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getBoolean("prevent_background_refresh", true)
|
||||
}
|
||||
|
||||
// 创建颜色方案
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> createDarkColorScheme()
|
||||
else -> createLightColorScheme()
|
||||
}
|
||||
|
||||
// 根据暗色模式和自定义背景调整卡片配置
|
||||
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
|
||||
if (darkTheme && !dynamicColor) {
|
||||
CardConfig.setDarkModeDefaults()
|
||||
} else if (!darkTheme && !dynamicColor) {
|
||||
CardConfig.setLightModeDefaults()
|
||||
}
|
||||
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
||||
|
||||
val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) }
|
||||
|
||||
LaunchedEffect(ThemeConfig.customBackgroundUri) {
|
||||
backgroundUri.value = ThemeConfig.customBackgroundUri
|
||||
}
|
||||
|
||||
val bgImagePainter = backgroundUri.value?.let {
|
||||
rememberAsyncImagePainter(
|
||||
model = it,
|
||||
onError = {
|
||||
Log.e("ThemeSystem", "背景图加载失败: ${it.result.throwable.message}")
|
||||
ThemeConfig.customBackgroundUri = null
|
||||
context.saveCustomBackground(null)
|
||||
},
|
||||
onSuccess = {
|
||||
Log.d("ThemeSystem", "背景图加载成功")
|
||||
ThemeConfig.backgroundImageLoaded = true
|
||||
ThemeConfig.isThemeChanging = false
|
||||
|
||||
ThemeConfig.preventBackgroundRefresh = true
|
||||
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit { putBoolean("prevent_background_refresh", true) }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val transition = updateTransition(
|
||||
targetState = ThemeConfig.backgroundImageLoaded,
|
||||
label = "bgTransition"
|
||||
)
|
||||
val bgAlpha by transition.animateFloat(
|
||||
label = "bgAlpha",
|
||||
transitionSpec = {
|
||||
spring(
|
||||
dampingRatio = 0.8f,
|
||||
stiffness = 300f
|
||||
)
|
||||
}
|
||||
) { loaded -> if (loaded) 1f else 0f }
|
||||
|
||||
DisposableEffect(systemIsDark) {
|
||||
onDispose {
|
||||
if (ThemeConfig.isThemeChanging) {
|
||||
ThemeConfig.isThemeChanging = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算适用的暗化值
|
||||
val dimFactor = CardConfig.cardDim
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(-2f)
|
||||
.background(if (darkTheme) Color.Black else Color.White)
|
||||
)
|
||||
|
||||
// 自定义背景层
|
||||
backgroundUri.value?.let { uri ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(-1f)
|
||||
.alpha(bgAlpha)
|
||||
) {
|
||||
// 背景图片
|
||||
bgImagePainter?.let { painter ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.paint(
|
||||
painter = painter,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
.graphicsLayer {
|
||||
alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 亮度调节层 (根据cardDim调整)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.6f + dimFactor * 0.3f)
|
||||
else Color.White.copy(alpha = 0.1f + dimFactor * 0.2f)
|
||||
)
|
||||
)
|
||||
|
||||
// 边缘渐变遮罩
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.5f + dimFactor * 0.2f)
|
||||
else Color.Black.copy(alpha = 0.2f + dimFactor * 0.1f)
|
||||
),
|
||||
radius = 1200f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 内容层
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(1f)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建动态深色颜色方案
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
private fun createDynamicDarkColorScheme(context: Context) =
|
||||
dynamicDarkColorScheme(context).copy(
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.White,
|
||||
onSurface = Color.White
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建动态浅色颜色方案
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
@Composable
|
||||
private fun createDynamicLightColorScheme(context: Context) =
|
||||
dynamicLightColorScheme(context).copy(
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建深色颜色方案
|
||||
*/
|
||||
@Composable
|
||||
private fun createDarkColorScheme() = darkColorScheme(
|
||||
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
|
||||
onPrimary = Color.White,
|
||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
|
||||
onPrimaryContainer = Color.White,
|
||||
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
|
||||
onSecondary = Color.White,
|
||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
|
||||
onSecondaryContainer = Color.White,
|
||||
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
|
||||
onTertiary = Color.White,
|
||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
|
||||
onTertiaryContainer = Color.White,
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.White,
|
||||
onSurface = Color.White,
|
||||
surfaceVariant = Color(0xFF2F2F2F),
|
||||
onSurfaceVariant = Color.White.copy(alpha = 0.7f),
|
||||
outline = Color.White.copy(alpha = 0.12f),
|
||||
outlineVariant = Color.White.copy(alpha = 0.12f),
|
||||
error = ThemeConfig.currentTheme.Error,
|
||||
onError = ThemeConfig.currentTheme.OnError,
|
||||
errorContainer = ThemeConfig.currentTheme.ErrorContainer.copy(alpha = 0.15f),
|
||||
onErrorContainer = Color.White
|
||||
)
|
||||
|
||||
/**
|
||||
* 创建浅色颜色方案
|
||||
*/
|
||||
@Composable
|
||||
private fun createLightColorScheme() = lightColorScheme(
|
||||
primary = ThemeConfig.currentTheme.Primary,
|
||||
onPrimary = ThemeConfig.currentTheme.OnPrimary,
|
||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
|
||||
onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer,
|
||||
secondary = ThemeConfig.currentTheme.Secondary,
|
||||
onSecondary = ThemeConfig.currentTheme.OnSecondary,
|
||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer,
|
||||
onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer,
|
||||
tertiary = ThemeConfig.currentTheme.Tertiary,
|
||||
onTertiary = ThemeConfig.currentTheme.OnTertiary,
|
||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer,
|
||||
onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer,
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.Black.copy(alpha = 0.87f),
|
||||
onSurface = Color.Black.copy(alpha = 0.87f),
|
||||
surfaceVariant = Color(0xFFF5F5F5),
|
||||
onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
|
||||
outline = Color.Black.copy(alpha = 0.12f),
|
||||
outlineVariant = Color.Black.copy(alpha = 0.12f),
|
||||
error = ThemeConfig.currentTheme.Error,
|
||||
onError = ThemeConfig.currentTheme.OnError,
|
||||
errorContainer = ThemeConfig.currentTheme.ErrorContainer,
|
||||
onErrorContainer = ThemeConfig.currentTheme.OnErrorContainer
|
||||
)
|
||||
|
||||
/**
|
||||
* 复制图片到应用内部存储并提升持久性
|
||||
*/
|
||||
private fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
|
||||
return try {
|
||||
val contentResolver: ContentResolver = contentResolver
|
||||
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
|
||||
|
||||
val fileName = "custom_background.jpg"
|
||||
val file = File(filesDir, fileName)
|
||||
|
||||
val backupFile = File(filesDir, "${fileName}.backup")
|
||||
val outputStream = FileOutputStream(backupFile)
|
||||
val buffer = ByteArray(4 * 1024)
|
||||
var read: Int
|
||||
|
||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||
outputStream.write(buffer, 0, read)
|
||||
}
|
||||
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
inputStream.close()
|
||||
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
}
|
||||
backupFile.renameTo(file)
|
||||
|
||||
Uri.fromFile(file)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ImageCopy", "复制图片失败: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存并应用自定义背景
|
||||
*/
|
||||
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
|
||||
val finalUri = if (transformation != null) {
|
||||
saveTransformedBackground(uri, transformation)
|
||||
} else {
|
||||
copyImageToInternalStorage(uri)
|
||||
}
|
||||
|
||||
// 保存到配置文件
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("custom_background", finalUri?.toString())
|
||||
// 设置阻止刷新标志为false,允许新设置的背景加载一次
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
}
|
||||
|
||||
ThemeConfig.customBackgroundUri = finalUri
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
CardConfig.cardElevation = 0.dp
|
||||
CardConfig.isCustomBackgroundEnabled = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存自定义背景
|
||||
*/
|
||||
fun Context.saveCustomBackground(uri: Uri?) {
|
||||
val newUri = uri?.let { copyImageToInternalStorage(it) }
|
||||
|
||||
// 保存到配置文件
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("custom_background", newUri?.toString())
|
||||
if (uri == null) {
|
||||
// 如果清除背景,也重置阻止刷新标志
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
} else {
|
||||
// 设置阻止刷新标志为false,允许新设置的背景加载一次
|
||||
putBoolean("prevent_background_refresh", false)
|
||||
}
|
||||
}
|
||||
|
||||
ThemeConfig.customBackgroundUri = newUri
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
ThemeConfig.preventBackgroundRefresh = false
|
||||
|
||||
if (uri != null) {
|
||||
CardConfig.cardElevation = 0.dp
|
||||
CardConfig.isCustomBackgroundEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载自定义背景
|
||||
*/
|
||||
fun Context.loadCustomBackground() {
|
||||
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("custom_background", null)
|
||||
|
||||
val newUri = uriString?.toUri()
|
||||
val preventRefresh = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getBoolean("prevent_background_refresh", false)
|
||||
|
||||
ThemeConfig.preventBackgroundRefresh = preventRefresh
|
||||
|
||||
if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) {
|
||||
Log.d("ThemeSystem", "加载自定义背景: $uriString, 阻止刷新: $preventRefresh")
|
||||
ThemeConfig.customBackgroundUri = newUri
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存主题模式
|
||||
*/
|
||||
fun Context.saveThemeMode(forceDark: Boolean?) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString(
|
||||
"theme_mode", when (forceDark) {
|
||||
true -> "dark"
|
||||
false -> "light"
|
||||
null -> "system"
|
||||
}
|
||||
)
|
||||
}
|
||||
ThemeConfig.forceDarkMode = forceDark
|
||||
ThemeConfig.needsResetOnThemeChange = forceDark == null
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载主题模式
|
||||
*/
|
||||
fun Context.loadThemeMode() {
|
||||
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("theme_mode", "system")
|
||||
|
||||
ThemeConfig.forceDarkMode = when(mode) {
|
||||
"dark" -> true
|
||||
"light" -> false
|
||||
else -> null
|
||||
}
|
||||
ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存主题颜色
|
||||
*/
|
||||
fun Context.saveThemeColors(themeName: String) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("theme_colors", themeName)
|
||||
}
|
||||
|
||||
ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载主题颜色
|
||||
*/
|
||||
fun Context.loadThemeColors() {
|
||||
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("theme_colors", "default")
|
||||
|
||||
ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default")
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存动态颜色状态
|
||||
*/
|
||||
fun Context.saveDynamicColorState(enabled: Boolean) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putBoolean("use_dynamic_color", enabled)
|
||||
}
|
||||
ThemeConfig.useDynamicColor = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载动态颜色状态
|
||||
*/
|
||||
fun Context.loadDynamicColorState() {
|
||||
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getBoolean("use_dynamic_color", true)
|
||||
|
||||
ThemeConfig.useDynamicColor = enabled
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SystemBarStyle(
|
||||
darkMode: Boolean,
|
||||
statusBarScrim: Color = Color.Transparent,
|
||||
navigationBarScrim: Color = Color.Transparent,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = context as ComponentActivity
|
||||
|
||||
SideEffect {
|
||||
activity.enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(
|
||||
statusBarScrim.toArgb(),
|
||||
statusBarScrim.toArgb(),
|
||||
) { darkMode },
|
||||
navigationBarStyle = when {
|
||||
darkMode -> SystemBarStyle.dark(
|
||||
navigationBarScrim.toArgb()
|
||||
)
|
||||
|
||||
else -> SystemBarStyle.light(
|
||||
navigationBarScrim.toArgb(),
|
||||
navigationBarScrim.toArgb(),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
108
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt
Normal file
108
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt
Normal file
@@ -0,0 +1,108 @@
|
||||
package com.sukisu.ultra.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
val Typography = Typography(
|
||||
// 大标题
|
||||
displayLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 57.sp,
|
||||
lineHeight = 64.sp,
|
||||
letterSpacing = (-0.25).sp
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 45.sp,
|
||||
lineHeight = 52.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
displaySmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 36.sp,
|
||||
lineHeight = 44.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
|
||||
// 标题
|
||||
headlineLarge = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 40.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineMedium = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 36.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
headlineSmall = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 32.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
|
||||
// 标题栏
|
||||
titleLarge = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.15.sp
|
||||
),
|
||||
titleSmall = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
|
||||
// 主体文字
|
||||
bodyLarge = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.25.sp
|
||||
),
|
||||
bodySmall = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.4.sp
|
||||
),
|
||||
|
||||
// 标签
|
||||
labelLarge = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.1.sp
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Matrix
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import androidx.core.graphics.createBitmap
|
||||
|
||||
data class BackgroundTransformation(
|
||||
val scale: Float = 1f,
|
||||
val offsetX: Float = 0f,
|
||||
val offsetY: Float = 0f
|
||||
)
|
||||
|
||||
fun Context.getImageBitmap(uri: Uri): Bitmap? {
|
||||
return try {
|
||||
val contentResolver: ContentResolver = contentResolver
|
||||
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
|
||||
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||
inputStream.close()
|
||||
bitmap
|
||||
} catch (e: Exception) {
|
||||
Log.e("BackgroundUtils", "Failed to get image bitmap: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: BackgroundTransformation): Bitmap {
|
||||
val width = bitmap.width
|
||||
val height = bitmap.height
|
||||
|
||||
// 创建与屏幕比例相同的目标位图
|
||||
val displayMetrics = resources.displayMetrics
|
||||
val screenWidth = displayMetrics.widthPixels
|
||||
val screenHeight = displayMetrics.heightPixels
|
||||
val screenRatio = screenHeight.toFloat() / screenWidth.toFloat()
|
||||
|
||||
// 计算目标宽高
|
||||
val targetWidth: Int
|
||||
val targetHeight: Int
|
||||
if (width.toFloat() / height.toFloat() > screenRatio) {
|
||||
targetHeight = height
|
||||
targetWidth = (height / screenRatio).toInt()
|
||||
} else {
|
||||
targetWidth = width
|
||||
targetHeight = (width * screenRatio).toInt()
|
||||
}
|
||||
|
||||
// 创建与目标相同大小的位图
|
||||
val scaledBitmap = createBitmap(targetWidth, targetHeight)
|
||||
val canvas = Canvas(scaledBitmap)
|
||||
|
||||
val matrix = Matrix()
|
||||
|
||||
// 确保缩放值有效
|
||||
val safeScale = maxOf(0.1f, transformation.scale)
|
||||
matrix.postScale(safeScale, safeScale)
|
||||
|
||||
// 计算偏移量,确保不会出现负最大值的问题
|
||||
val widthDiff = (bitmap.width * safeScale - targetWidth)
|
||||
val heightDiff = (bitmap.height * safeScale - targetHeight)
|
||||
|
||||
// 安全计算偏移量边界
|
||||
val maxOffsetX = maxOf(0f, widthDiff / 2)
|
||||
val maxOffsetY = maxOf(0f, heightDiff / 2)
|
||||
|
||||
// 限制偏移范围
|
||||
val safeOffsetX = if (maxOffsetX > 0)
|
||||
transformation.offsetX.coerceIn(-maxOffsetX, maxOffsetX) else 0f
|
||||
val safeOffsetY = if (maxOffsetY > 0)
|
||||
transformation.offsetY.coerceIn(-maxOffsetY, maxOffsetY) else 0f
|
||||
|
||||
// 应用偏移量到矩阵
|
||||
val translationX = -widthDiff / 2 + safeOffsetX
|
||||
val translationY = -heightDiff / 2 + safeOffsetY
|
||||
|
||||
matrix.postTranslate(translationX, translationY)
|
||||
|
||||
// 将原始位图绘制到新位图上
|
||||
canvas.drawBitmap(bitmap, matrix, null)
|
||||
|
||||
return scaledBitmap
|
||||
}
|
||||
|
||||
fun Context.saveTransformedBackground(uri: Uri, transformation: BackgroundTransformation): Uri? {
|
||||
try {
|
||||
val bitmap = getImageBitmap(uri) ?: return null
|
||||
val transformedBitmap = applyTransformationToBitmap(bitmap, transformation)
|
||||
|
||||
val fileName = "custom_background_transformed.jpg"
|
||||
val file = File(filesDir, fileName)
|
||||
val outputStream = FileOutputStream(file)
|
||||
|
||||
transformedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
|
||||
return Uri.fromFile(file)
|
||||
} catch (e: Exception) {
|
||||
Log.e("BackgroundUtils", "Failed to save transformed image: ${e.message}", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.DownloadManager
|
||||
@@ -12,7 +12,8 @@ import android.util.Log
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.core.content.ContextCompat
|
||||
import zako.zako.zako.ui.util.module.LatestVersionInfo
|
||||
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
||||
import androidx.core.net.toUri
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
@@ -42,14 +43,14 @@ fun download(
|
||||
onDownloading()
|
||||
return
|
||||
} else if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||
onDownloaded(Uri.parse(localUri))
|
||||
onDownloaded(localUri.toUri())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val request = DownloadManager.Request(Uri.parse(url))
|
||||
val request = DownloadManager.Request(url.toUri())
|
||||
.setDestinationInExternalPublicDir(
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
fileName
|
||||
@@ -140,7 +141,7 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
||||
val uri = cursor.getString(
|
||||
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
|
||||
)
|
||||
onDownloaded(Uri.parse(uri))
|
||||
onDownloaded(uri.toUri())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util;
|
||||
package com.sukisu.ultra.ui.util;
|
||||
/*
|
||||
* Copyright (C) 2009 The Android Open Source Project
|
||||
*
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
@@ -16,9 +16,9 @@ import com.topjohnwu.superuser.ShellUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import zako.zako.zako.BuildConfig
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.ksuApp
|
||||
import com.sukisu.ultra.BuildConfig
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import org.json.JSONArray
|
||||
import java.io.File
|
||||
|
||||
@@ -436,7 +436,7 @@ fun restartApp(packageName: String) {
|
||||
launchApp(packageName)
|
||||
}
|
||||
|
||||
private fun getSuSFSDaemonPath(): String {
|
||||
fun getSuSFSDaemonPath(): String {
|
||||
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozakozako.so"
|
||||
}
|
||||
|
||||
@@ -537,7 +537,7 @@ fun getKpmModuleInfo(name: String): String {
|
||||
|
||||
fun controlKpmModule(name: String, args: String? = null): Int {
|
||||
val shell = getRootShell()
|
||||
val cmd = "${getKpmmgrPath()} control $name ${args ?: ""}"
|
||||
val cmd = """${getKpmmgrPath()} control $name "${args ?: ""}""""
|
||||
val result = runCmd(shell, cmd)
|
||||
return result.trim().toIntOrNull() ?: -1
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.system.Os
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.ui.screen.getManagerVersion
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ui.screen.getManagerVersion
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.PrintWriter
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
@@ -16,7 +16,7 @@ import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.R
|
||||
import com.sukisu.ultra.R
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
@@ -1,9 +1,9 @@
|
||||
package zako.zako.zako.ui.util
|
||||
package com.sukisu.ultra.ui.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import zako.zako.zako.R
|
||||
import com.sukisu.ultra.R
|
||||
|
||||
@Composable
|
||||
fun getSELinuxStatus(): String {
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.util.module
|
||||
package com.sukisu.ultra.ui.util.module
|
||||
|
||||
data class LatestVersionInfo(
|
||||
val versionCode : Int = 0,
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.viewmodel
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -9,7 +9,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.ui.util.*
|
||||
import com.sukisu.ultra.ui.util.*
|
||||
|
||||
class KpmViewModel : ViewModel() {
|
||||
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
||||
@@ -143,17 +143,6 @@ class KpmViewModel : ViewModel() {
|
||||
return result
|
||||
}
|
||||
|
||||
fun controlModule(moduleId: String, args: String? = null): Int {
|
||||
return try {
|
||||
val result = controlKpmModule(moduleId, args)
|
||||
Log.d("KsuCli", "Control module $moduleId result: $result")
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to control module $moduleId", e)
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
data class ModuleInfo(
|
||||
val id: String,
|
||||
val name: String,
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.viewmodel
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
@@ -10,8 +10,8 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import zako.zako.zako.ui.util.HanziToPinyin
|
||||
import zako.zako.zako.ui.util.listModules
|
||||
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||
import com.sukisu.ultra.ui.util.listModules
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.Collator
|
||||
@@ -40,13 +40,6 @@ class ModuleViewModel : ViewModel() {
|
||||
val dirId: String, // real module id (dir name)
|
||||
)
|
||||
|
||||
data class ModuleUpdateInfo(
|
||||
val version: String,
|
||||
val versionCode: Int,
|
||||
val zipUrl: String,
|
||||
val changelog: String,
|
||||
)
|
||||
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
var search by mutableStateOf("")
|
||||
@@ -1,11 +1,7 @@
|
||||
package zako.zako.zako.ui.viewmodel
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
@@ -14,22 +10,23 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import zako.zako.zako.IKsuInterface
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.ksuApp
|
||||
import zako.zako.zako.ui.KsuService
|
||||
import zako.zako.zako.ui.util.HanziToPinyin
|
||||
import zako.zako.zako.ui.util.KsuCli
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||
import java.text.Collator
|
||||
import java.util.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import com.dergoogler.mmrl.platform.Platform
|
||||
import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS
|
||||
import com.sukisu.ultra.ui.webui.packageManager
|
||||
import com.sukisu.ultra.ui.webui.userManager
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
class SuperUserViewModel : ViewModel() {
|
||||
val isPlatformAlive get() = Platform.isAlive
|
||||
companion object {
|
||||
private const val TAG = "SuperUserViewModel"
|
||||
private var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
||||
@@ -68,9 +65,9 @@ class SuperUserViewModel : ViewModel() {
|
||||
|
||||
// 批量操作相关状态
|
||||
var showBatchActions by mutableStateOf(false)
|
||||
private set
|
||||
internal set
|
||||
var selectedApps by mutableStateOf<Set<String>>(emptySet())
|
||||
private set
|
||||
internal set
|
||||
|
||||
private val sortedList by derivedStateOf {
|
||||
val comparator = compareBy<AppInfo> {
|
||||
@@ -142,55 +139,65 @@ class SuperUserViewModel : ViewModel() {
|
||||
fetchAppList() // 刷新列表以显示最新状态
|
||||
}
|
||||
|
||||
private suspend fun connectKsuService(
|
||||
onDisconnect: () -> Unit = {}
|
||||
): Pair<IBinder, ServiceConnection> = suspendCoroutine { continuation ->
|
||||
val connection = object : ServiceConnection {
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
onDisconnect()
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||
continuation.resume(binder as IBinder to this)
|
||||
}
|
||||
}
|
||||
|
||||
val intent = Intent(ksuApp, KsuService::class.java)
|
||||
|
||||
val task = KsuService.bindOrTask(
|
||||
intent,
|
||||
Shell.EXECUTOR,
|
||||
connection,
|
||||
// 批量更新权限和umount模块设置
|
||||
suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) {
|
||||
selectedApps.forEach { packageName ->
|
||||
val app = apps.find { it.packageName == packageName }
|
||||
app?.let {
|
||||
val profile = Natives.getAppProfile(packageName, it.uid)
|
||||
val updatedProfile = profile.copy(
|
||||
allowSu = allowSu,
|
||||
umountModules = umountModules ?: profile.umountModules,
|
||||
nonRootUseDefault = false
|
||||
)
|
||||
val shell = KsuCli.SHELL
|
||||
task?.let { it1 -> shell.execTask(it1) }
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
apps = apps.map { app ->
|
||||
if (app.packageName == packageName) {
|
||||
app.copy(profile = updatedProfile)
|
||||
} else {
|
||||
app
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clearSelection()
|
||||
showBatchActions = false // 批量操作完成后退出批量模式
|
||||
fetchAppList() // 刷新列表以显示最新状态
|
||||
}
|
||||
|
||||
private fun stopKsuService() {
|
||||
val intent = Intent(ksuApp, KsuService::class.java)
|
||||
KsuService.stop(intent)
|
||||
// 仅更新本地应用配置,避免重新获取整个列表导致滚动位置重置
|
||||
fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) {
|
||||
apps = apps.map { app ->
|
||||
if (app.packageName == packageName) {
|
||||
app.copy(profile = updatedProfile)
|
||||
} else {
|
||||
app
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchAppList() {
|
||||
isRefreshing = true
|
||||
|
||||
val result = connectKsuService {
|
||||
Log.w(TAG, "KsuService disconnected")
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
withTimeoutOrNull(TIMEOUT_MILLIS) {
|
||||
while (!isPlatformAlive) {
|
||||
delay(500)
|
||||
}
|
||||
} ?: return@withContext // Exit early if timeout
|
||||
val pm = ksuApp.packageManager
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
|
||||
val binder = result.first
|
||||
val allPackages = IKsuInterface.Stub.asInterface(binder).getPackages(0)
|
||||
val userInfos = Platform.userManager.getUsers()
|
||||
val packages = mutableListOf<PackageInfo>()
|
||||
val packageManager = Platform.packageManager
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
stopKsuService()
|
||||
for (userInfo in userInfos) {
|
||||
Log.i(TAG, "fetchAppList: ${userInfo.id}")
|
||||
packages.addAll(packageManager.getInstalledPackages(0, userInfo.id))
|
||||
}
|
||||
|
||||
val packages = allPackages.list
|
||||
|
||||
apps = packages.map {
|
||||
val appInfo = it.applicationInfo
|
||||
val uid = appInfo!!.uid
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.viewmodel
|
||||
package com.sukisu.ultra.ui.viewmodel
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
@@ -10,12 +10,12 @@ import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.profile.Capabilities
|
||||
import zako.zako.zako.profile.Groups
|
||||
import zako.zako.zako.ui.util.getAppProfileTemplate
|
||||
import zako.zako.zako.ui.util.listAppProfileTemplates
|
||||
import zako.zako.zako.ui.util.setAppProfileTemplate
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.profile.Capabilities
|
||||
import com.sukisu.ultra.profile.Groups
|
||||
import com.sukisu.ultra.ui.util.getAppProfileTemplate
|
||||
import com.sukisu.ultra.ui.util.listAppProfileTemplates
|
||||
import com.sukisu.ultra.ui.util.setAppProfileTemplate
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONArray
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.sukisu.ultra.ui.webui
|
||||
|
||||
import android.content.ServiceConnection
|
||||
import android.util.Log
|
||||
import com.dergoogler.mmrl.platform.Platform
|
||||
import com.dergoogler.mmrl.platform.hiddenApi.HiddenPackageManager
|
||||
import com.dergoogler.mmrl.platform.hiddenApi.HiddenUserManager
|
||||
import com.dergoogler.mmrl.platform.model.IProvider
|
||||
import com.dergoogler.mmrl.platform.model.PlatformIntent
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class KsuLibSuProvider : IProvider {
|
||||
override val name = "KsuLibSu"
|
||||
|
||||
override fun isAvailable() = true
|
||||
|
||||
override suspend fun isAuthorized() = Natives.becomeManager(ksuApp.packageName)
|
||||
|
||||
private val serviceIntent
|
||||
get() = PlatformIntent(
|
||||
ksuApp,
|
||||
Platform.KsuNext,
|
||||
SuService::class.java
|
||||
)
|
||||
|
||||
override fun bind(connection: ServiceConnection) {
|
||||
RootService.bind(serviceIntent.intent, connection)
|
||||
}
|
||||
|
||||
override fun unbind(connection: ServiceConnection) {
|
||||
RootService.stop(serviceIntent.intent)
|
||||
}
|
||||
}
|
||||
|
||||
// webui x
|
||||
suspend fun initPlatform() = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val active = Platform.init {
|
||||
this.context = ksuApp
|
||||
this.platform = Platform.KsuNext
|
||||
this.provider = from(KsuLibSuProvider())
|
||||
}
|
||||
|
||||
while (!active) {
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
return@withContext active
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuLibSu", "Failed to initialize platform", e)
|
||||
return@withContext false
|
||||
}
|
||||
}
|
||||
|
||||
val Platform.Companion.packageManager get(): HiddenPackageManager = HiddenPackageManager(this.mService)
|
||||
val Platform.Companion.userManager get(): HiddenUserManager = HiddenUserManager(this.mService)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package zako.zako.zako.ui.webui;
|
||||
package com.sukisu.ultra.ui.webui;
|
||||
|
||||
import java.net.URLConnection;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.webui;
|
||||
package com.sukisu.ultra.ui.webui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.sukisu.ultra.ui.webui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.dergoogler.mmrl.platform.model.PlatformIntent.Companion.getPlatform
|
||||
import com.dergoogler.mmrl.platform.service.ServiceManager
|
||||
import com.topjohnwu.superuser.ipc.RootService
|
||||
|
||||
class SuService : RootService() {
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
val mode = intent.getPlatform()
|
||||
return ServiceManager(mode)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.ui.webui
|
||||
package com.sukisu.ultra.ui.webui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
@@ -15,9 +15,11 @@ import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
import com.dergoogler.mmrl.platform.model.ModId
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import zako.zako.zako.ui.util.createRootShell
|
||||
import com.sukisu.ultra.ui.util.createRootShell
|
||||
import java.io.File
|
||||
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
class WebUIActivity : ComponentActivity() {
|
||||
@@ -41,7 +43,8 @@ class WebUIActivity : ComponentActivity() {
|
||||
@Suppress("DEPRECATION")
|
||||
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
|
||||
} else {
|
||||
val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
||||
val taskDescription =
|
||||
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
||||
setTaskDescription(taskDescription)
|
||||
}
|
||||
|
||||
@@ -82,7 +85,7 @@ class WebUIActivity : ComponentActivity() {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.allowFileAccess = false
|
||||
webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir)
|
||||
webviewInterface = WebViewInterface(WXOptions(this@WebUIActivity, this, ModId(moduleId)))
|
||||
addJavascriptInterface(webviewInterface, "ksu")
|
||||
setWebViewClient(webViewClient)
|
||||
loadUrl("https://mui.kernelsu.org/index.html")
|
||||
@@ -0,0 +1,275 @@
|
||||
package com.sukisu.ultra.ui.webui
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.paint
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.zIndex
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import coil.compose.AsyncImagePainter
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||
import com.sukisu.ultra.ui.theme.Typography
|
||||
import com.sukisu.ultra.ui.theme.loadCustomBackground
|
||||
|
||||
// 提供界面类型的本地组合
|
||||
val LocalIsSecondaryScreen = staticCompositionLocalOf { false }
|
||||
|
||||
/**
|
||||
* WebUI专用主题配置
|
||||
*/
|
||||
@Composable
|
||||
fun WebUIXTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
isSecondaryScreen: Boolean = false,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||
context.loadCustomBackground()
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
}
|
||||
}
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (darkTheme) {
|
||||
dynamicDarkColorScheme(context).let { scheme ->
|
||||
if (isSecondaryScreen) {
|
||||
scheme.copy(
|
||||
background = scheme.surfaceContainerHighest,
|
||||
surface = scheme.surfaceContainerHighest
|
||||
)
|
||||
} else {
|
||||
scheme.copy(
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dynamicLightColorScheme(context).let { scheme ->
|
||||
if (isSecondaryScreen) {
|
||||
scheme.copy(
|
||||
background = scheme.surfaceContainerHighest,
|
||||
surface = scheme.surfaceContainerHighest
|
||||
)
|
||||
} else {
|
||||
scheme.copy(
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
darkTheme -> {
|
||||
if (isSecondaryScreen) {
|
||||
darkColorScheme().copy(
|
||||
background = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
surface = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
)
|
||||
} else {
|
||||
darkColorScheme().copy(
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
if (isSecondaryScreen) {
|
||||
lightColorScheme().copy(
|
||||
background = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
surface = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
)
|
||||
} else {
|
||||
lightColorScheme().copy(
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ConfigureSystemBars(darkTheme)
|
||||
|
||||
val backgroundUri = remember { mutableStateOf(ThemeConfig.customBackgroundUri) }
|
||||
|
||||
LaunchedEffect(ThemeConfig.customBackgroundUri) {
|
||||
backgroundUri.value = ThemeConfig.customBackgroundUri
|
||||
}
|
||||
val bgImagePainter = backgroundUri.value?.let {
|
||||
rememberAsyncImagePainter(
|
||||
model = it,
|
||||
onError = {
|
||||
ThemeConfig.backgroundImageLoaded = false
|
||||
},
|
||||
onSuccess = {
|
||||
ThemeConfig.backgroundImageLoaded = true
|
||||
ThemeConfig.isThemeChanging = false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 背景透明度动画
|
||||
val transition = updateTransition(
|
||||
targetState = ThemeConfig.backgroundImageLoaded,
|
||||
label = "bgTransition"
|
||||
)
|
||||
|
||||
val bgAlpha by transition.animateFloat(
|
||||
label = "bgAlpha",
|
||||
transitionSpec = {
|
||||
spring(
|
||||
dampingRatio = 0.8f,
|
||||
stiffness = 300f
|
||||
)
|
||||
}
|
||||
) { loaded -> if (loaded) 1f else 0f }
|
||||
CompositionLocalProvider(LocalIsSecondaryScreen provides isSecondaryScreen) {
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
) {
|
||||
if (isSecondaryScreen) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(-2f)
|
||||
.background(if (darkTheme) Color.Black else Color.White)
|
||||
)
|
||||
|
||||
backgroundUri.value?.let { uri ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(-1f)
|
||||
.alpha(bgAlpha)
|
||||
) {
|
||||
bgImagePainter?.let { painter ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.paint(
|
||||
painter = painter,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
.graphicsLayer {
|
||||
alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
|
||||
}
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.6f)
|
||||
else Color.White.copy(alpha = 0.1f)
|
||||
)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
if (darkTheme) Color.Black.copy(alpha = 0.5f)
|
||||
else Color.Black.copy(alpha = 0.2f)
|
||||
),
|
||||
radius = 1200f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(1f)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前界面是否为二级界面
|
||||
*/
|
||||
@Composable
|
||||
fun isSecondaryScreen(): Boolean {
|
||||
return LocalIsSecondaryScreen.current
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置WebUI的系统栏样式
|
||||
*/
|
||||
@Composable
|
||||
private fun ConfigureSystemBars(
|
||||
darkMode: Boolean,
|
||||
statusBarScrim: Color = Color.Transparent,
|
||||
navigationBarScrim: Color = Color.Transparent
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val activity = context as ComponentActivity
|
||||
|
||||
SideEffect {
|
||||
activity.enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(
|
||||
statusBarScrim.toArgb(),
|
||||
statusBarScrim.toArgb()
|
||||
) { darkMode },
|
||||
navigationBarStyle = when {
|
||||
darkMode -> SystemBarStyle.dark(
|
||||
navigationBarScrim.toArgb()
|
||||
)
|
||||
else -> SystemBarStyle.light(
|
||||
navigationBarScrim.toArgb(),
|
||||
navigationBarScrim.toArgb()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.sukisu.ultra.ui.webui
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.dergoogler.mmrl.platform.Platform
|
||||
import com.dergoogler.mmrl.platform.model.ModId
|
||||
import com.dergoogler.mmrl.ui.component.Loading
|
||||
import com.dergoogler.mmrl.webui.screen.WebUIScreen
|
||||
import com.dergoogler.mmrl.webui.util.rememberWebUIOptions
|
||||
import com.sukisu.ultra.BuildConfig
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class WebUIXActivity : ComponentActivity() {
|
||||
private lateinit var webView: WebView
|
||||
|
||||
private val userAgent
|
||||
get(): String {
|
||||
val ksuVersion = BuildConfig.VERSION_CODE
|
||||
|
||||
val platform = Platform.get("Unknown") {
|
||||
platform.name
|
||||
}
|
||||
|
||||
val platformVersion = Platform.get(-1) {
|
||||
moduleManager.versionCode
|
||||
}
|
||||
|
||||
val osVersion = Build.VERSION.RELEASE
|
||||
val deviceModel = Build.MODEL
|
||||
|
||||
return "SukiSU /$ksuVersion (Linux; Android $osVersion; $deviceModel; $platform/$platformVersion)"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
webView = WebView(this)
|
||||
|
||||
lifecycleScope.launch {
|
||||
initPlatform()
|
||||
}
|
||||
|
||||
val moduleId = intent.getStringExtra("id")!!
|
||||
val name = intent.getStringExtra("name")!!
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
||||
@Suppress("DEPRECATION")
|
||||
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
|
||||
} else {
|
||||
val taskDescription =
|
||||
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
||||
setTaskDescription(taskDescription)
|
||||
}
|
||||
|
||||
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
|
||||
setContent {
|
||||
WebUIXTheme {
|
||||
var isLoading by remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(Platform.isAlive) {
|
||||
while (!Platform.isAlive) {
|
||||
delay(1000)
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
Loading()
|
||||
return@WebUIXTheme
|
||||
}
|
||||
|
||||
val webDebugging = prefs.getBoolean("enable_web_debugging", false)
|
||||
val erudaInject = prefs.getBoolean("use_webuix_eruda", false)
|
||||
val dark = isSystemInDarkTheme()
|
||||
|
||||
val options = rememberWebUIOptions(
|
||||
modId = ModId(moduleId),
|
||||
debug = webDebugging,
|
||||
appVersionCode = BuildConfig.VERSION_CODE,
|
||||
isDarkMode = dark,
|
||||
enableEruda = erudaInject,
|
||||
cls = WebUIXActivity::class.java,
|
||||
userAgentString = userAgent
|
||||
)
|
||||
|
||||
WebUIScreen(
|
||||
webView = webView,
|
||||
options = options,
|
||||
interfaces = listOf(
|
||||
WebViewInterface.factory()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,47 @@
|
||||
package zako.zako.zako.ui.webui
|
||||
package com.sukisu.ultra.ui.webui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.TextUtils
|
||||
import android.view.Window
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
||||
import com.dergoogler.mmrl.webui.interfaces.WebUIInterface
|
||||
import com.dergoogler.mmrl.webui.model.JavaScriptInterface
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
import zako.zako.zako.ui.util.createRootShell
|
||||
import zako.zako.zako.ui.util.listModules
|
||||
import zako.zako.zako.ui.util.withNewRootShell
|
||||
import com.sukisu.ultra.ui.util.createRootShell
|
||||
import com.sukisu.ultra.ui.util.listModules
|
||||
import com.sukisu.ultra.ui.util.withNewRootShell
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import zako.zako.zako.ui.util.controlKpmModule
|
||||
import zako.zako.zako.ui.util.listKpmModules
|
||||
import com.sukisu.ultra.ui.util.controlKpmModule
|
||||
import com.sukisu.ultra.ui.util.listKpmModules
|
||||
import java.io.File
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
class WebViewInterface(
|
||||
val context: Context,
|
||||
private val webView: WebView,
|
||||
private val modDir: String
|
||||
) {
|
||||
wxOptions: WXOptions,
|
||||
) : WebUIInterface(wxOptions) {
|
||||
override var name: String = "ksu"
|
||||
|
||||
companion object {
|
||||
fun factory() = JavaScriptInterface(WebViewInterface::class.java)
|
||||
}
|
||||
|
||||
private val modDir get() = "/data/adb/modules/${modId.id}"
|
||||
|
||||
@Composable
|
||||
@JavascriptInterface
|
||||
fun isSecondaryPage(): Boolean {
|
||||
return isSecondaryScreen()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun exec(cmd: String): String {
|
||||
@@ -170,9 +183,9 @@ class WebViewInterface(
|
||||
if (context is Activity) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (enable) {
|
||||
hideSystemUI(context.window)
|
||||
hideSystemUI(activity.window)
|
||||
} else {
|
||||
showSystemUI(context.window)
|
||||
showSystemUI(activity.window)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package zako.zako.zako.utils
|
||||
package com.sukisu.ultra.utils
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
@@ -1,8 +1,8 @@
|
||||
package io.zako.zako;
|
||||
package io.sukisu.ultra;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import zako.zako.zako.ui.util.KsuCli;
|
||||
import com.sukisu.ultra.ui.util.KsuCli;
|
||||
|
||||
public class UltraShellHelper {
|
||||
public static String runCmd(String cmds) {
|
||||
@@ -19,7 +19,7 @@ public class UltraShellHelper {
|
||||
}
|
||||
|
||||
public static boolean isPathExists(String path) {
|
||||
return !runCmd("file " + path).contains("No such file or directory");
|
||||
return runCmd("file " + path).contains("No such file or directory");
|
||||
}
|
||||
|
||||
public static void CopyFileTo(String path, String target) {
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.sukisu.ultra;
|
||||
|
||||
import static com.sukisu.ultra.ui.util.KsuCliKt.getKpmmgrPath;
|
||||
import static com.sukisu.ultra.ui.util.KsuCliKt.getSuSFSDaemonPath;
|
||||
|
||||
public class UltraToolInstall {
|
||||
private static final String OUTSIDE_KPMMGR_PATH = "/data/adb/ksu/bin/kpmmgr";
|
||||
private static final String OUTSIDE_SUSFSD_PATH = "/data/adb/ksu/bin/susfsd";
|
||||
public static void tryToInstall() {
|
||||
String kpmmgrPath = getKpmmgrPath();
|
||||
if (UltraShellHelper.isPathExists(OUTSIDE_KPMMGR_PATH)) {
|
||||
UltraShellHelper.CopyFileTo(kpmmgrPath, OUTSIDE_KPMMGR_PATH);
|
||||
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_KPMMGR_PATH);
|
||||
}
|
||||
String SuSFSDaemonPath = getSuSFSDaemonPath();
|
||||
if (UltraShellHelper.isPathExists(OUTSIDE_SUSFSD_PATH)) {
|
||||
UltraShellHelper.CopyFileTo(SuSFSDaemonPath, OUTSIDE_SUSFSD_PATH);
|
||||
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_SUSFSD_PATH);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package io.zako.zako;
|
||||
|
||||
import static zako.zako.zako.ui.util.KsuCliKt.getKpmmgrPath;
|
||||
|
||||
public class UltraToolInstall {
|
||||
private static final String OUTSIDE_KPMMGR_PATH = "/data/adb/ksu/bin/kpmmgr";
|
||||
public static void tryToInstall() {
|
||||
String kpmmgrPath = getKpmmgrPath();
|
||||
if (!UltraShellHelper.isPathExists(OUTSIDE_KPMMGR_PATH)) {
|
||||
UltraShellHelper.CopyFileTo(kpmmgrPath, OUTSIDE_KPMMGR_PATH);
|
||||
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_KPMMGR_PATH);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package zako.zako.zako
|
||||
|
||||
import android.app.Application
|
||||
import coil.Coil
|
||||
import coil.ImageLoader
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||
import java.io.File
|
||||
|
||||
lateinit var ksuApp: KernelSUApplication
|
||||
|
||||
class KernelSUApplication : Application() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ksuApp = this
|
||||
|
||||
val context = this
|
||||
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
||||
Coil.setImageLoader(
|
||||
ImageLoader.Builder(context)
|
||||
.components {
|
||||
add(AppIconKeyer())
|
||||
add(AppIconFetcher.Factory(iconSize, false, context))
|
||||
}
|
||||
.build()
|
||||
)
|
||||
|
||||
val webroot = File(dataDir, "webroot")
|
||||
if (!webroot.exists()) {
|
||||
webroot.mkdir()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package zako.zako.zako.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.IBinder;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.topjohnwu.superuser.ipc.RootService;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import zako.zako.zako.IKsuInterface;
|
||||
import rikka.parcelablelist.ParcelableListSlice;
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/4/18.
|
||||
*/
|
||||
|
||||
public class KsuService extends RootService {
|
||||
|
||||
private static final String TAG = "KsuService";
|
||||
|
||||
class Stub extends IKsuInterface.Stub {
|
||||
@Override
|
||||
public ParcelableListSlice<PackageInfo> getPackages(int flags) {
|
||||
List<PackageInfo> list = getInstalledPackagesAll(flags);
|
||||
Log.i(TAG, "getPackages: " + list.size());
|
||||
return new ParcelableListSlice<>(list);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(@NonNull Intent intent) {
|
||||
return new Stub();
|
||||
}
|
||||
|
||||
List<Integer> getUserIds() {
|
||||
List<Integer> result = new ArrayList<>();
|
||||
UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
|
||||
List<UserHandle> userProfiles = um.getUserProfiles();
|
||||
for (UserHandle userProfile : userProfiles) {
|
||||
int userId = userProfile.hashCode();
|
||||
result.add(userProfile.hashCode());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ArrayList<PackageInfo> getInstalledPackagesAll(int flags) {
|
||||
ArrayList<PackageInfo> packages = new ArrayList<>();
|
||||
for (Integer userId : getUserIds()) {
|
||||
Log.i(TAG, "getInstalledPackagesAll: " + userId);
|
||||
packages.addAll(getInstalledPackagesAsUser(flags, userId));
|
||||
}
|
||||
return packages;
|
||||
}
|
||||
|
||||
List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) {
|
||||
try {
|
||||
PackageManager pm = getPackageManager();
|
||||
Method getInstalledPackagesAsUser = pm.getClass().getDeclaredMethod("getInstalledPackagesAsUser", int.class, int.class);
|
||||
return (List<PackageInfo>) getInstalledPackagesAsUser.invoke(pm, flags, userId);
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, "err", e);
|
||||
}
|
||||
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
package zako.zako.zako.ui
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.displayCutout
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.union
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||
import io.zako.zako.UltraToolInstall
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.ksuApp
|
||||
import zako.zako.zako.ui.screen.BottomBarDestination
|
||||
import zako.zako.zako.ui.theme.CardConfig
|
||||
import zako.zako.zako.ui.theme.KernelSUTheme
|
||||
import zako.zako.zako.ui.theme.loadCustomBackground
|
||||
import zako.zako.zako.ui.theme.loadThemeMode
|
||||
import zako.zako.zako.ui.util.LocalSnackbarHost
|
||||
import zako.zako.zako.ui.util.getKpmVersion
|
||||
import zako.zako.zako.ui.util.rootAvailable
|
||||
import zako.zako.zako.ui.util.install
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
// Enable edge to edge
|
||||
enableEdgeToEdge()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// 加载保存的背景设置
|
||||
loadCustomBackground()
|
||||
loadThemeMode()
|
||||
CardConfig.load(applicationContext)
|
||||
|
||||
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
if (isManager) {
|
||||
install()
|
||||
UltraToolInstall.tryToInstall()
|
||||
}
|
||||
|
||||
setContent {
|
||||
KernelSUTheme {
|
||||
val navController = rememberNavController()
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
Scaffold(
|
||||
bottomBar = { BottomBar(navController) },
|
||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||
) { innerPadding ->
|
||||
CompositionLocalProvider(
|
||||
LocalSnackbarHost provides snackBarHostState,
|
||||
) {
|
||||
DestinationsNavHost(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
navGraph = NavGraphs.root,
|
||||
navController = navController,
|
||||
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
|
||||
get() = { fadeIn(animationSpec = tween(340)) }
|
||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
|
||||
get() = { fadeOut(animationSpec = tween(340)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomBar(navController: NavHostController) {
|
||||
val navigator = navController.rememberDestinationsNavigator()
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
||||
val kpmVersion = getKpmVersion()
|
||||
|
||||
// 获取卡片颜色和透明度
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
val cardElevation = CardConfig.cardElevation
|
||||
|
||||
NavigationBar(
|
||||
tonalElevation = cardElevation, // 动态设置阴影
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(
|
||||
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
|
||||
)
|
||||
) {
|
||||
BottomBarDestination.entries.forEach { destination ->
|
||||
if (destination == BottomBarDestination.Kpm) {
|
||||
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
|
||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
navigator.popBackStack(destination.direction, false)
|
||||
}
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label)) },
|
||||
alwaysShowLabel = false,
|
||||
colors = androidx.compose.material3.NavigationBarItemDefaults.colors(
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
||||
NavigationBarItem(
|
||||
selected = isCurrentDestOnBackStack,
|
||||
onClick = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
navigator.popBackStack(destination.direction, false)
|
||||
}
|
||||
navigator.navigate(destination.direction) {
|
||||
popUpTo(NavGraphs.root) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
if (isCurrentDestOnBackStack) {
|
||||
Icon(destination.iconSelected, stringResource(destination.label))
|
||||
} else {
|
||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label)) },
|
||||
alwaysShowLabel = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package zako.zako.zako.ui.component
|
||||
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
@Composable
|
||||
fun SwitchItem(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
summary: String,
|
||||
checked: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
ListItem(
|
||||
modifier = modifier,
|
||||
leadingContent = { Icon(icon, contentDescription = null) },
|
||||
headlineContent = { Text(title) },
|
||||
supportingContent = { Text(summary) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.AccountCircle
|
||||
import androidx.compose.material.icons.filled.Android
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.component.SwitchItem
|
||||
import zako.zako.zako.ui.component.profile.AppProfileConfig
|
||||
import zako.zako.zako.ui.component.profile.RootProfileConfig
|
||||
import zako.zako.zako.ui.component.profile.TemplateConfig
|
||||
import zako.zako.zako.ui.util.LocalSnackbarHost
|
||||
import zako.zako.zako.ui.util.forceStopApp
|
||||
import zako.zako.zako.ui.util.getSepolicy
|
||||
import zako.zako.zako.ui.util.launchApp
|
||||
import zako.zako.zako.ui.util.restartApp
|
||||
import zako.zako.zako.ui.util.setSepolicy
|
||||
import zako.zako.zako.ui.viewmodel.SuperUserViewModel
|
||||
import zako.zako.zako.ui.viewmodel.getTemplateInfoById
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/5/16.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun AppProfileScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
appInfo: SuperUserViewModel.AppInfo,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val scope = rememberCoroutineScope()
|
||||
val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label)
|
||||
val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label)
|
||||
val suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label)
|
||||
|
||||
val packageName = appInfo.packageName
|
||||
val initialProfile = Natives.getAppProfile(packageName, appInfo.uid)
|
||||
if (initialProfile.allowSu) {
|
||||
initialProfile.rules = getSepolicy(packageName)
|
||||
}
|
||||
var profile by rememberSaveable {
|
||||
mutableStateOf(initialProfile)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
) { paddingValues ->
|
||||
AppProfileInner(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
packageName = appInfo.packageName,
|
||||
appLabel = appInfo.label,
|
||||
appIcon = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(),
|
||||
contentDescription = appInfo.label,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
},
|
||||
profile = profile,
|
||||
onViewTemplate = {
|
||||
getTemplateInfoById(it)?.let { info ->
|
||||
navigator.navigate(TemplateEditorScreenDestination(info))
|
||||
}
|
||||
},
|
||||
onManageTemplate = {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination())
|
||||
},
|
||||
onProfileChange = {
|
||||
scope.launch {
|
||||
if (it.allowSu) {
|
||||
// sync with allowlist.c - forbid_system_uid
|
||||
if (appInfo.uid < 2000 && appInfo.uid != 1000) {
|
||||
snackBarHost.showSnackbar(suNotAllowed)
|
||||
return@launch
|
||||
}
|
||||
if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) {
|
||||
snackBarHost.showSnackbar(failToUpdateSepolicy)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
if (!Natives.setAppProfile(it)) {
|
||||
snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid))
|
||||
} else {
|
||||
profile = it
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppProfileInner(
|
||||
modifier: Modifier = Modifier,
|
||||
packageName: String,
|
||||
appLabel: String,
|
||||
appIcon: @Composable () -> Unit,
|
||||
profile: Natives.Profile,
|
||||
onViewTemplate: (id: String) -> Unit = {},
|
||||
onManageTemplate: () -> Unit = {},
|
||||
onProfileChange: (Natives.Profile) -> Unit,
|
||||
) {
|
||||
val isRootGranted = profile.allowSu
|
||||
|
||||
Column(modifier = modifier) {
|
||||
AppMenuBox(packageName) {
|
||||
ListItem(
|
||||
headlineContent = { Text(appLabel) },
|
||||
supportingContent = { Text(packageName) },
|
||||
leadingContent = appIcon,
|
||||
)
|
||||
}
|
||||
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(id = R.string.superuser),
|
||||
checked = isRootGranted,
|
||||
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
||||
)
|
||||
|
||||
Crossfade(targetState = isRootGranted, label = "") { current ->
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
||||
) {
|
||||
if (current) {
|
||||
val initialMode = if (profile.rootUseDefault) {
|
||||
Mode.Default
|
||||
} else if (profile.rootTemplate != null) {
|
||||
Mode.Template
|
||||
} else {
|
||||
Mode.Custom
|
||||
}
|
||||
var mode by rememberSaveable {
|
||||
mutableStateOf(initialMode)
|
||||
}
|
||||
ProfileBox(mode, true) {
|
||||
// template mode shouldn't change profile here!
|
||||
if (it == Mode.Default || it == Mode.Custom) {
|
||||
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
||||
}
|
||||
mode = it
|
||||
}
|
||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
||||
if (currentMode == Mode.Template) {
|
||||
TemplateConfig(
|
||||
profile = profile,
|
||||
onViewTemplate = onViewTemplate,
|
||||
onManageTemplate = onManageTemplate,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
} else if (mode == Mode.Custom) {
|
||||
RootProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
||||
ProfileBox(mode, false) {
|
||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
||||
}
|
||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
||||
val modifyEnabled = currentMode == Mode.Custom
|
||||
AppProfileConfig(
|
||||
fixedName = true,
|
||||
profile = profile,
|
||||
enabled = modifyEnabled,
|
||||
onProfileChange = onProfileChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Mode(@StringRes private val res: Int) {
|
||||
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
||||
|
||||
val text: String
|
||||
@Composable get() = stringResource(res)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
onBack: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(stringResource(R.string.profile))
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileBox(
|
||||
mode: Mode,
|
||||
hasTemplate: Boolean,
|
||||
onModeChange: (Mode) -> Unit,
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.profile)) },
|
||||
supportingContent = { Text(mode.text) },
|
||||
leadingContent = { Icon(Icons.Filled.AccountCircle, null) },
|
||||
)
|
||||
HorizontalDivider(thickness = Dp.Hairline)
|
||||
ListItem(headlineContent = {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Default,
|
||||
label = { Text(stringResource(R.string.profile_default)) },
|
||||
onClick = { onModeChange(Mode.Default) },
|
||||
)
|
||||
if (hasTemplate) {
|
||||
FilterChip(
|
||||
selected = mode == Mode.Template,
|
||||
label = { Text(stringResource(R.string.profile_template)) },
|
||||
onClick = { onModeChange(Mode.Template) },
|
||||
)
|
||||
}
|
||||
FilterChip(
|
||||
selected = mode == Mode.Custom,
|
||||
label = { Text(stringResource(R.string.profile_custom)) },
|
||||
onClick = { onModeChange(Mode.Custom) },
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
||||
val density = LocalDensity.current
|
||||
|
||||
BoxWithConstraints(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures {
|
||||
touchPoint = it
|
||||
expanded = true
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
content()
|
||||
|
||||
val (offsetX, offsetY) = with(density) {
|
||||
(touchPoint.x.toDp()) to (touchPoint.y.toDp())
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
offset = DpOffset(offsetX, -offsetY),
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
},
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = R.string.launch_app)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
launchApp(packageName)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = R.string.force_stop_app)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
forceStopApp(packageName)
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = R.string.restart_app)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
restartApp(packageName)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun AppProfilePreview() {
|
||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||
AppProfileInner(
|
||||
packageName = "icu.nullptr.test",
|
||||
appLabel = "Test",
|
||||
appIcon = { Icon(Icons.Filled.Android, null) },
|
||||
profile = profile,
|
||||
onProfileChange = {
|
||||
profile = it
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,735 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.system.Os
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.*
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.component.rememberConfirmDialog
|
||||
import zako.zako.zako.ui.util.*
|
||||
import zako.zako.zako.ui.util.module.LatestVersionInfo
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import zako.zako.zako.ui.theme.getCardColors
|
||||
import zako.zako.zako.ui.theme.getCardElevation
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import zako.zako.zako.ui.theme.CardConfig
|
||||
import androidx.core.content.edit
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.util.zip.GZIPInputStream
|
||||
import kotlin.random.Random
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>(start = true)
|
||||
@Composable
|
||||
fun HomeScreen(navigator: DestinationsNavigator) {
|
||||
val context = LocalContext.current
|
||||
var isSimpleMode by rememberSaveable { mutableStateOf(false) }
|
||||
var isHideVersion by rememberSaveable { mutableStateOf(false) }
|
||||
var isHideOtherInfo by rememberSaveable { mutableStateOf(false) }
|
||||
var isHideSusfsStatus by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
// 从 SharedPreferences 加载简洁模式状态
|
||||
LaunchedEffect(Unit) {
|
||||
isSimpleMode = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_simple_mode", false)
|
||||
}
|
||||
// 从 SharedPreferences 加载隐藏 KernelSU 版本号开关状态
|
||||
LaunchedEffect(Unit) {
|
||||
isHideVersion = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_version", false)
|
||||
}
|
||||
// 从 SharedPreferences 加载隐藏模块数量等信息开关状态
|
||||
LaunchedEffect(Unit) {
|
||||
isHideOtherInfo = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_other_info", false)
|
||||
}
|
||||
// 从 SharedPreferences 加载隐藏 SuSFS 状态开关状态
|
||||
LaunchedEffect(Unit) {
|
||||
isHideSusfsStatus = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_susfs_status", false)
|
||||
}
|
||||
val kernelVersion = getKernelVersion()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val deviceModel = getDeviceModel(context)
|
||||
val ksuVersion = if (isManager) Natives.version else null
|
||||
val managerVersion = getManagerVersion(context).second
|
||||
val Zako = "一.*加.*A.*c.*e.*5.*P.*r.*o".toRegex().matches(deviceModel)
|
||||
val isVersion = ksuVersion == 12777
|
||||
val isManagerVersionValid = managerVersion > (ksuVersion ?: 0) + 33
|
||||
val shouldTriggerRestart = Zako && kernelVersion.isGKI() && (isVersion || isManagerVersionValid)
|
||||
|
||||
LaunchedEffect(shouldTriggerRestart) {
|
||||
if (shouldTriggerRestart) {
|
||||
val random = Random.nextInt(0, 100)
|
||||
if (random <= 95) {
|
||||
reboot()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
kernelVersion,
|
||||
onInstallClick = { navigator.navigate(InstallScreenDestination) },
|
||||
onSettingsClick = { navigator.navigate(SettingScreenDestination) },
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
)
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(top = 12.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
if (shouldTriggerRestart) {
|
||||
WarningCard(message = "zakozako")
|
||||
return@Column
|
||||
}
|
||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||
val ksuVersion = if (isManager) Natives.version else null
|
||||
val lkmMode = ksuVersion?.let {
|
||||
if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) Natives.isLkmMode else null
|
||||
}
|
||||
|
||||
StatusCard(kernelVersion, ksuVersion, lkmMode) {
|
||||
navigator.navigate(InstallScreenDestination)
|
||||
}
|
||||
if (isManager && Natives.requireNewKernel()) {
|
||||
WarningCard(
|
||||
stringResource(id = R.string.require_kernel_version).format(
|
||||
ksuVersion, Natives.MINIMAL_SUPPORTED_KERNEL
|
||||
)
|
||||
)
|
||||
}
|
||||
if (ksuVersion != null && !rootAvailable()) {
|
||||
WarningCard(
|
||||
stringResource(id = R.string.grant_root_failed)
|
||||
)
|
||||
}
|
||||
val checkUpdate =
|
||||
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("check_update", true)
|
||||
if (checkUpdate) {
|
||||
UpdateCard()
|
||||
}
|
||||
val prefs = remember { context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) }
|
||||
var clickCount by rememberSaveable { mutableIntStateOf(prefs.getInt("click_count", 0)) }
|
||||
|
||||
if (!isSimpleMode && clickCount < 3) {
|
||||
AnimatedVisibility(
|
||||
visible = clickCount < 3,
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
clickCount++
|
||||
prefs.edit { putInt("click_count", clickCount) }
|
||||
}
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.using_mksu_manager),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
InfoCard()
|
||||
if (!isSimpleMode) {
|
||||
ContributionCard()
|
||||
DonateCard()
|
||||
LearnMoreCard()
|
||||
}
|
||||
Spacer(Modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateCard() {
|
||||
val context = LocalContext.current
|
||||
val latestVersionInfo = LatestVersionInfo()
|
||||
val newVersion by produceState(initialValue = latestVersionInfo) {
|
||||
value = withContext(Dispatchers.IO) {
|
||||
checkNewVersion()
|
||||
}
|
||||
}
|
||||
|
||||
val currentVersionCode = getManagerVersion(context).second
|
||||
val newVersionCode = newVersion.versionCode
|
||||
val newVersionUrl = newVersion.downloadUrl
|
||||
val changelog = newVersion.changelog
|
||||
|
||||
Log.d("UpdateCard", "Current version code: $currentVersionCode")
|
||||
Log.d("UpdateCard", "New version code: $newVersionCode")
|
||||
|
||||
|
||||
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val title = stringResource(id = R.string.module_changelog)
|
||||
val updateText = stringResource(id = R.string.module_update)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = newVersionCode > currentVersionCode,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) })
|
||||
WarningCard(
|
||||
message = stringResource(id = R.string.new_version_available).format(newVersionCode),
|
||||
MaterialTheme.colorScheme.outlineVariant
|
||||
) {
|
||||
if (changelog.isEmpty()) {
|
||||
uriHandler.openUri(newVersionUrl)
|
||||
} else {
|
||||
updateDialog.showConfirm(
|
||||
title = title,
|
||||
content = changelog,
|
||||
markdown = true,
|
||||
confirm = updateText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(id))
|
||||
}, onClick = {
|
||||
reboot(reason)
|
||||
})
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
kernelVersion: KernelVersion,
|
||||
onInstallClick: () -> Unit,
|
||||
onSettingsClick: () -> Unit,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.app_name)) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
actions = {
|
||||
if (kernelVersion.isGKI()) {
|
||||
IconButton(onClick = onInstallClick) {
|
||||
Icon(Icons.Filled.Archive, stringResource(R.string.install))
|
||||
}
|
||||
}
|
||||
|
||||
var showDropdown by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = { showDropdown = true }) {
|
||||
Icon(Icons.Filled.Refresh, stringResource(R.string.reboot))
|
||||
DropdownMenu(expanded = showDropdown, onDismissRequest = { showDropdown = false }
|
||||
) {
|
||||
|
||||
RebootDropdownItem(id = R.string.reboot)
|
||||
|
||||
val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
|
||||
@Suppress("DEPRECATION")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) {
|
||||
RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace")
|
||||
}
|
||||
RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery")
|
||||
RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader")
|
||||
RebootDropdownItem(id = R.string.reboot_download, reason = "download")
|
||||
RebootDropdownItem(id = R.string.reboot_edl, reason = "edl")
|
||||
}
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatusCard(
|
||||
kernelVersion: KernelVersion,
|
||||
ksuVersion: Int?,
|
||||
lkmMode: Boolean?,
|
||||
onClickInstall: () -> Unit = {}
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
if (kernelVersion.isGKI()) {
|
||||
onClickInstall()
|
||||
}
|
||||
}
|
||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
when {
|
||||
ksuVersion != null -> {
|
||||
val safeMode = when {
|
||||
Natives.isSafeMode -> " [${stringResource(id = R.string.safe_mode)}]"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val workingMode = when (lkmMode) {
|
||||
null -> " <Non-GKI>"
|
||||
true -> " <LKM>"
|
||||
else -> " <GKI>"
|
||||
}
|
||||
|
||||
val workingText =
|
||||
"${stringResource(id = R.string.home_working)}$workingMode$safeMode"
|
||||
|
||||
val isHideVersion = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_version", false)
|
||||
|
||||
val isHideOtherInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_other_info", false)
|
||||
|
||||
val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_susfs_status", false)
|
||||
|
||||
Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working))
|
||||
Column(Modifier.padding(start = 20.dp)) {
|
||||
Text(
|
||||
text = workingText,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
if (!isHideVersion) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_working_version, ksuVersion),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
if (!isHideOtherInfo) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.home_superuser_count, getSuperuserCount()
|
||||
), style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_module_count, getModuleCount()),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
val kpmVersion = getKpmVersion()
|
||||
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_kpm_module, getKpmModuleCount()),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!isHideSusfsStatus) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
val suSFS = getSuSFS()
|
||||
if (lkmMode != true) {
|
||||
val translatedStatus = when (suSFS) {
|
||||
"Supported" -> stringResource(R.string.status_supported)
|
||||
"Not Supported" -> stringResource(R.string.status_not_supported)
|
||||
else -> stringResource(R.string.status_unknown)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.home_susfs, translatedStatus),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kernelVersion.isGKI() -> {
|
||||
Icon(Icons.Outlined.Warning, stringResource(R.string.home_not_installed))
|
||||
Column(Modifier.padding(start = 20.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.home_not_installed),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_click_to_install),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Icon(Icons.Outlined.Block, stringResource(R.string.home_unsupported))
|
||||
Column(Modifier.padding(start = 20.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.home_unsupported),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_unsupported_reason),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WarningCard(
|
||||
message: String, color: Color = MaterialTheme.colorScheme.error, onClick: (() -> Unit)? = null
|
||||
) {
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(onClick?.let { Modifier.clickable { it() } } ?: Modifier)
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = message, style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun ContributionCard() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val links = listOf("https://github.com/zako", "https://github.com/udochina")
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
val randomIndex = Random.nextInt(links.size)
|
||||
uriHandler.openUri(links[randomIndex])
|
||||
}
|
||||
.padding(24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.home_ContributionCard_kernelsu),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_click_to_ContributionCard_kernelsu),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LearnMoreCard() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val url = stringResource(R.string.home_learn_kernelsu_url)
|
||||
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
uriHandler.openUri(url)
|
||||
}
|
||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.home_learn_kernelsu),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_click_to_learn_kernelsu),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DonateCard() {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
|
||||
Row(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
uriHandler.openUri("https://patreon.com/weishu")
|
||||
}
|
||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.home_support_title),
|
||||
style = MaterialTheme.typography.titleSmall
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.home_support_content),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoCard() {
|
||||
val context = LocalContext.current
|
||||
val isSimpleMode = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_simple_mode", false)
|
||||
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp)
|
||||
) withContext@{
|
||||
val contents = StringBuilder()
|
||||
val uname = Os.uname()
|
||||
|
||||
@Composable
|
||||
fun InfoCardItem(
|
||||
label: String,
|
||||
content: String,
|
||||
) {
|
||||
contents.appendLine(label).appendLine(content).appendLine()
|
||||
Text(text = label, style = MaterialTheme.typography.bodyLarge)
|
||||
Text(text = content, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
|
||||
InfoCardItem(stringResource(R.string.home_kernel), uname.release)
|
||||
|
||||
if (!isSimpleMode) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
val androidVersion = Build.VERSION.RELEASE
|
||||
InfoCardItem(stringResource(R.string.home_android_version), androidVersion)
|
||||
}
|
||||
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
val deviceModel = getDeviceModel(context)
|
||||
InfoCardItem(stringResource(R.string.home_device_model), deviceModel)
|
||||
|
||||
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
val managerVersion = getManagerVersion(context)
|
||||
InfoCardItem(
|
||||
stringResource(R.string.home_manager_version),
|
||||
"${managerVersion.first} (${managerVersion.second})"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
InfoCardItem(stringResource(R.string.home_selinux_status), getSELinuxStatus())
|
||||
|
||||
|
||||
|
||||
if (!isSimpleMode) {
|
||||
val kpmVersion = getKpmVersion()
|
||||
var displayVersion: String
|
||||
val isKpmConfigured = checkKpmConfigured()
|
||||
|
||||
if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) {
|
||||
val statusText = if (isKpmConfigured) {
|
||||
stringResource(R.string.kernel_patched)
|
||||
} else {
|
||||
stringResource(R.string.kernel_not_enabled)
|
||||
}
|
||||
displayVersion = "${stringResource(R.string.not_supported)} ($statusText)"
|
||||
} else {
|
||||
displayVersion = "${stringResource(R.string.supported)} ($kpmVersion)"
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
InfoCardItem(stringResource(R.string.home_kpm_version), displayVersion)
|
||||
}
|
||||
|
||||
val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
.getBoolean("is_hide_susfs_status", false)
|
||||
|
||||
if ((!isSimpleMode) && (!isHideSusfsStatus)) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
val suSFS = getSuSFS()
|
||||
if (suSFS == "Supported") {
|
||||
val suSFSVersion = getSuSFSVersion()
|
||||
if (suSFSVersion.isEmpty()) return@withContext
|
||||
val isSUS_SU = getSuSFSFeatures() == "CONFIG_KSU_SUSFS_SUS_SU"
|
||||
val infoText = buildString {
|
||||
append(suSFSVersion)
|
||||
append(if (isSUS_SU) " (${getSuSFSVariant()})" else " (${stringResource(R.string.manual_hook)})")
|
||||
if (isSUS_SU) {
|
||||
val susSUMode = try { susfsSUS_SU_Mode().toString() } catch (_: Exception) { "" }
|
||||
if (susSUMode.isNotEmpty()) {
|
||||
append(" ${stringResource(R.string.sus_su_mode)} $susSUMode")
|
||||
}
|
||||
}
|
||||
}
|
||||
InfoCardItem(
|
||||
stringResource(R.string.home_susfs_version),
|
||||
infoText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getManagerVersion(context: Context): Pair<String, Long> {
|
||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!!
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
|
||||
return Pair(packageInfo.versionName!!, versionCode)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun StatusCardPreview() {
|
||||
Column {
|
||||
StatusCard(KernelVersion(5, 10, 101), 1, null)
|
||||
StatusCard(KernelVersion(5, 10, 101), 20000, true)
|
||||
StatusCard(KernelVersion(5, 10, 101), null, true)
|
||||
StatusCard(KernelVersion(4, 10, 101), null, false)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun WarningCardPreview() {
|
||||
Column {
|
||||
WarningCard(message = "Warning message")
|
||||
WarningCard(
|
||||
message = "Warning message ",
|
||||
MaterialTheme.colorScheme.outlineVariant,
|
||||
onClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi")
|
||||
private fun getDeviceModel(context: Context): String {
|
||||
return try {
|
||||
val systemProperties = Class.forName("android.os.SystemProperties")
|
||||
val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java)
|
||||
val marketNameKeys = listOf(
|
||||
"ro.product.marketname", // Xiaomi
|
||||
"ro.vendor.oplus.market.name", // Oppo, OnePlus, Realme
|
||||
"ro.vivo.market.name", // Vivo
|
||||
"ro.config.marketing_name" // Huawei
|
||||
)
|
||||
for (key in marketNameKeys) {
|
||||
val marketName = getMethod.invoke(null, key, "") as String
|
||||
if (marketName.isNotEmpty()) {
|
||||
return marketName
|
||||
}
|
||||
}
|
||||
Build.DEVICE
|
||||
} catch (e: Exception) {
|
||||
Build.DEVICE
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkKpmConfigured(): Boolean {
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("su -c cat /proc/config.gz")
|
||||
val inputStream = process.inputStream
|
||||
val gzipInputStream = GZIPInputStream(inputStream)
|
||||
val reader = BufferedReader(InputStreamReader(gzipInputStream))
|
||||
|
||||
var line: String?
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
if (line?.contains("CONFIG_KPM=y") == true) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
reader.close()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,606 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.FileUpload
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
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.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import zako.zako.zako.ui.component.DialogHandle
|
||||
import zako.zako.zako.ui.component.rememberConfirmDialog
|
||||
import zako.zako.zako.ui.component.rememberCustomDialog
|
||||
import zako.zako.zako.ui.theme.ThemeConfig
|
||||
import zako.zako.zako.ui.theme.getCardColors
|
||||
import zako.zako.zako.ui.theme.getCardElevation
|
||||
import zako.zako.zako.ui.util.*
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.utils.AssetsUtil
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2024/3/12.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun InstallScreen(navigator: DestinationsNavigator) {
|
||||
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
|
||||
val context = LocalContext.current
|
||||
|
||||
var showRebootDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val onFlashComplete = {
|
||||
showRebootDialog = true
|
||||
}
|
||||
|
||||
if (showRebootDialog) {
|
||||
RebootDialog(
|
||||
show = true,
|
||||
onDismiss = { showRebootDialog = false },
|
||||
onConfirm = {
|
||||
showRebootDialog = false
|
||||
try {
|
||||
val process = Runtime.getRuntime().exec("su")
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("svc power reboot\n")
|
||||
writer.write("exit\n")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val onInstall = {
|
||||
installMethod?.let { method ->
|
||||
when (method) {
|
||||
is InstallMethod.HorizonKernel -> {
|
||||
method.uri?.let { uri ->
|
||||
val worker = HorizonKernelWorker(context)
|
||||
worker.uri = uri
|
||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
||||
worker.start()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val flashIt = FlashIt.FlashBoot(
|
||||
boot = if (method is InstallMethod.SelectFile) method.uri else null,
|
||||
lkm = lkmSelection,
|
||||
ota = method is InstallMethod.DirectInstallToInactiveSlot
|
||||
)
|
||||
navigator.navigate(FlashScreenDestination(flashIt))
|
||||
}
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
|
||||
val currentKmi by produceState(initialValue = "") {
|
||||
value = getCurrentKmi()
|
||||
}
|
||||
|
||||
val selectKmiDialog = rememberSelectKmiDialog { kmi ->
|
||||
kmi?.let {
|
||||
lkmSelection = LkmSelection.KmiString(it)
|
||||
onInstall()
|
||||
}
|
||||
}
|
||||
|
||||
val onClickNext = {
|
||||
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
||||
selectKmiDialog.show()
|
||||
} else {
|
||||
onInstall()
|
||||
}
|
||||
}
|
||||
|
||||
val selectLkmLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri ->
|
||||
lkmSelection = LkmSelection.LkmUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onLkmUpload = {
|
||||
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/octet-stream"
|
||||
})
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
onBack = { navigator.popBackStack() },
|
||||
onLkmUpload = onLkmUpload,
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
)
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SelectInstallMethod { method ->
|
||||
installMethod = method
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||
Text(
|
||||
stringResource(
|
||||
id = R.string.selected_lkm,
|
||||
it.uri.lastPathSegment ?: "(file)"
|
||||
)
|
||||
)
|
||||
}
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = installMethod != null,
|
||||
onClick = onClickNext
|
||||
) {
|
||||
Text(
|
||||
stringResource(id = R.string.install_next),
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun RebootDialog(
|
||||
show: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
if (show) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(id = R.string.reboot_complete_title)) },
|
||||
text = { Text(stringResource(id = R.string.reboot_complete_msg)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(stringResource(id = R.string.yes))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(id = R.string.no))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class HorizonKernelWorker(private val context: Context) : Thread() {
|
||||
var uri: Uri? = null
|
||||
private lateinit var filePath: String
|
||||
private lateinit var binaryPath: String
|
||||
|
||||
|
||||
private var onFlashComplete: (() -> Unit)? = null
|
||||
|
||||
fun setOnFlashCompleteListener(listener: () -> Unit) {
|
||||
onFlashComplete = listener
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
|
||||
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
|
||||
|
||||
try {
|
||||
cleanup()
|
||||
if (!rootAvailable()) {
|
||||
showError(context.getString(R.string.root_required))
|
||||
return
|
||||
}
|
||||
|
||||
copy()
|
||||
if (!File(filePath).exists()) {
|
||||
showError(context.getString(R.string.copy_failed))
|
||||
return
|
||||
}
|
||||
|
||||
getBinary()
|
||||
patch()
|
||||
flash()
|
||||
|
||||
(context as? Activity)?.runOnUiThread {
|
||||
onFlashComplete?.invoke()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showError(e.message ?: context.getString(R.string.unknown_error))
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanup() {
|
||||
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
||||
}
|
||||
|
||||
private fun copy() {
|
||||
uri?.let { safeUri ->
|
||||
context.contentResolver.openInputStream(safeUri)?.use { input ->
|
||||
FileOutputStream(File(filePath)).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBinary() {
|
||||
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
|
||||
if (!File(binaryPath).exists()) {
|
||||
throw IOException("Failed to extract update-binary")
|
||||
}
|
||||
}
|
||||
|
||||
private fun patch() {
|
||||
val mkbootfsPath = "${context.filesDir.absolutePath}/mkbootfs"
|
||||
AssetsUtil.exportFiles(context, "mkbootfs", mkbootfsPath)
|
||||
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $mkbootfsPath \$AKHOME/tools;' $binaryPath")
|
||||
}
|
||||
|
||||
private fun flash() {
|
||||
val process = ProcessBuilder("su")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
|
||||
try {
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
|
||||
writer.write("sh $binaryPath 3 1 \"$filePath\" && touch ${context.filesDir.absolutePath}/done\nexit\n")
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
process.inputStream.bufferedReader().use { reader ->
|
||||
reader.lineSequence().forEach { line ->
|
||||
if (line.startsWith("ui_print")) {
|
||||
showLog(line.removePrefix("ui_print"))
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
process.destroy()
|
||||
}
|
||||
|
||||
if (!File("${context.filesDir.absolutePath}/done").exists()) {
|
||||
throw IOException("Flash failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun runCommand(su: Boolean, cmd: String): Int {
|
||||
val process = ProcessBuilder(if (su) "su" else "sh")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
|
||||
return try {
|
||||
process.outputStream.bufferedWriter().use { writer ->
|
||||
writer.write("$cmd\n")
|
||||
writer.write("exit\n")
|
||||
writer.flush()
|
||||
}
|
||||
process.waitFor()
|
||||
} finally {
|
||||
process.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
(context as? Activity)?.runOnUiThread {
|
||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLog(message: String) {
|
||||
(context as? Activity)?.runOnUiThread {
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class InstallMethod {
|
||||
data class SelectFile(
|
||||
val uri: Uri? = null,
|
||||
@StringRes override val label: Int = R.string.select_file,
|
||||
override val summary: String?
|
||||
) : InstallMethod()
|
||||
|
||||
data object DirectInstall : InstallMethod() {
|
||||
override val label: Int
|
||||
get() = R.string.direct_install
|
||||
}
|
||||
|
||||
data object DirectInstallToInactiveSlot : InstallMethod() {
|
||||
override val label: Int
|
||||
get() = R.string.install_inactive_slot
|
||||
}
|
||||
|
||||
data class HorizonKernel(
|
||||
val uri: Uri? = null,
|
||||
@StringRes override val label: Int = R.string.horizon_kernel,
|
||||
override val summary: String? = null
|
||||
) : InstallMethod()
|
||||
|
||||
abstract val label: Int
|
||||
open val summary: String? = null
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
||||
val rootAvailable = rootAvailable()
|
||||
val isAbDevice = isAbDevice()
|
||||
val selectFileTip = stringResource(
|
||||
id = R.string.select_file_tip,
|
||||
if (isInitBoot()) "init_boot" else "boot"
|
||||
)
|
||||
|
||||
val radioOptions = mutableListOf<InstallMethod>(
|
||||
InstallMethod.SelectFile(summary = selectFileTip)
|
||||
)
|
||||
|
||||
if (rootAvailable) {
|
||||
radioOptions.add(InstallMethod.DirectInstall)
|
||||
if (isAbDevice) {
|
||||
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
|
||||
}
|
||||
radioOptions.add(InstallMethod.HorizonKernel(summary = "Flashing the Anykernel3 Kernel"))
|
||||
}
|
||||
|
||||
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
||||
|
||||
val selectImageLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
it.data?.data?.let { uri ->
|
||||
val option = when (currentSelectingMethod) {
|
||||
is InstallMethod.SelectFile -> InstallMethod.SelectFile(uri, summary = selectFileTip)
|
||||
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(uri, summary = " Flashing the Anykernel3 Kernel")
|
||||
else -> null
|
||||
}
|
||||
option?.let {
|
||||
selectedOption = it
|
||||
onSelected(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val confirmDialog = rememberConfirmDialog(
|
||||
onConfirm = {
|
||||
selectedOption = InstallMethod.DirectInstallToInactiveSlot
|
||||
onSelected(InstallMethod.DirectInstallToInactiveSlot)
|
||||
},
|
||||
onDismiss = null
|
||||
)
|
||||
|
||||
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
|
||||
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
|
||||
|
||||
val onClick = { option: InstallMethod ->
|
||||
currentSelectingMethod = option
|
||||
when (option) {
|
||||
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
||||
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "application/*"
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream", "application/zip"))
|
||||
})
|
||||
}
|
||||
is InstallMethod.DirectInstall -> {
|
||||
selectedOption = option
|
||||
onSelected(option)
|
||||
}
|
||||
is InstallMethod.DirectInstallToInactiveSlot -> {
|
||||
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
radioOptions.forEach { option ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.toggleable(
|
||||
value = option.javaClass == selectedOption?.javaClass,
|
||||
onValueChange = { onClick(option) },
|
||||
role = Role.RadioButton,
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
) {
|
||||
RadioButton(
|
||||
selected = option.javaClass == selectedOption?.javaClass,
|
||||
onClick = { onClick(option) },
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = option.label),
|
||||
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||
fontStyle = MaterialTheme.typography.titleMedium.fontStyle
|
||||
)
|
||||
option.summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||
fontStyle = MaterialTheme.typography.bodySmall.fontStyle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
||||
return rememberCustomDialog { dismiss ->
|
||||
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
||||
value = getSupportedKmis()
|
||||
}
|
||||
val listOptions = supportedKmi.map { value ->
|
||||
ListOption(
|
||||
titleText = value,
|
||||
subtitleText = null,
|
||||
icon = null
|
||||
)
|
||||
}
|
||||
|
||||
var selection: String? = null
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.select_kmi))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
listOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
selection = supportedKmi[index]
|
||||
}
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Column {
|
||||
Text(text = option.titleText)
|
||||
option.subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (selection != null) {
|
||||
onSelected(selection)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
tonalElevation = getCardElevation()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
onBack: () -> Unit = {},
|
||||
onLkmUpload: () -> Unit = {},
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.install)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onLkmUpload) {
|
||||
Icon(Icons.Filled.FileUpload, contentDescription = null)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets.safeDrawing.only(
|
||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||
),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SelectInstallPreview() {
|
||||
InstallScreen(EmptyDestinationsNavigator)
|
||||
}
|
||||
@@ -1,615 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import zako.zako.zako.ui.component.SwitchItem
|
||||
import zako.zako.zako.ui.theme.CardConfig
|
||||
import zako.zako.zako.ui.theme.ThemeColors
|
||||
import zako.zako.zako.ui.theme.ThemeConfig
|
||||
import zako.zako.zako.ui.theme.saveCustomBackground
|
||||
import zako.zako.zako.ui.theme.saveThemeColors
|
||||
import zako.zako.zako.ui.theme.saveThemeMode
|
||||
import zako.zako.zako.ui.theme.saveDynamicColorState
|
||||
import zako.zako.zako.ui.util.getSuSFS
|
||||
import zako.zako.zako.ui.util.getSuSFSFeatures
|
||||
import zako.zako.zako.ui.util.susfsSUS_SU_0
|
||||
import zako.zako.zako.ui.util.susfsSUS_SU_2
|
||||
import zako.zako.zako.ui.util.susfsSUS_SU_Mode
|
||||
import androidx.core.content.edit
|
||||
import zako.zako.zako.R
|
||||
|
||||
|
||||
fun saveCardConfig(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
with(prefs.edit()) {
|
||||
putFloat("card_alpha", CardConfig.cardAlpha)
|
||||
putBoolean("custom_background_enabled", CardConfig.cardElevation == 0.dp)
|
||||
putBoolean("is_custom_alpha_set", CardConfig.isCustomAlphaSet)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun MoreSettingsScreen(navigator: DestinationsNavigator) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val context = LocalContext.current
|
||||
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
|
||||
// 主题模式选择
|
||||
var themeMode by remember {
|
||||
mutableIntStateOf(
|
||||
when(ThemeConfig.forceDarkMode) {
|
||||
true -> 2 // 深色
|
||||
false -> 1 // 浅色
|
||||
null -> 0 // 跟随系统
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 动态颜色开关状态
|
||||
var useDynamicColor by remember {
|
||||
mutableStateOf(ThemeConfig.useDynamicColor)
|
||||
}
|
||||
|
||||
var showThemeModeDialog by remember { mutableStateOf(false) }
|
||||
// 主题模式选项
|
||||
val themeOptions = listOf(
|
||||
stringResource(R.string.theme_follow_system),
|
||||
stringResource(R.string.theme_light),
|
||||
stringResource(R.string.theme_dark)
|
||||
)
|
||||
|
||||
// 简洁模块开关状态
|
||||
var isSimpleMode by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_simple_mode", false))
|
||||
}
|
||||
|
||||
// 更新简洁模块开关状态
|
||||
val onSimpleModeChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_simple_mode", newValue) }
|
||||
isSimpleMode = newValue
|
||||
}
|
||||
|
||||
// 隐藏内核 KernelSU 版本号开关状态
|
||||
var isHideVersion by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_version", false))
|
||||
}
|
||||
|
||||
// 隐藏内核 KernelSU 版本号模块开关状态
|
||||
val onHideVersionChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_version", newValue) }
|
||||
isHideVersion = newValue
|
||||
}
|
||||
|
||||
// 隐藏模块数量等信息开关状态
|
||||
var isHideOtherInfo by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_other_info", false))
|
||||
}
|
||||
|
||||
// 隐藏模块数量等信息开关状态
|
||||
val onHideOtherInfoChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_other_info", newValue) }
|
||||
isHideOtherInfo = newValue
|
||||
}
|
||||
|
||||
// 隐藏 SuSFS 状态开关状态
|
||||
var isHideSusfsStatus by remember {
|
||||
mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false))
|
||||
}
|
||||
|
||||
// 隐藏 SuSFS 状态开关状态
|
||||
val onHideSusfsStatusChange = { newValue: Boolean ->
|
||||
prefs.edit { putBoolean("is_hide_susfs_status", newValue) }
|
||||
isHideSusfsStatus = newValue
|
||||
}
|
||||
|
||||
// SELinux 状态
|
||||
var selinuxEnabled by remember {
|
||||
mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing")
|
||||
}
|
||||
|
||||
// 卡片配置状态
|
||||
var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) }
|
||||
var showCardSettings by remember { mutableStateOf(false) }
|
||||
var isCustomBackgroundEnabled by rememberSaveable {
|
||||
mutableStateOf(ThemeConfig.customBackgroundUri != null)
|
||||
}
|
||||
|
||||
// 初始化卡片配置
|
||||
val systemIsDark = isSystemInDarkTheme()
|
||||
LaunchedEffect(Unit) {
|
||||
CardConfig.apply {
|
||||
cardAlpha = prefs.getFloat("card_alpha", 0.45f)
|
||||
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
|
||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
||||
|
||||
// 如果没有手动设置透明度,且是深色模式,则使用默认值
|
||||
if (!isCustomAlphaSet) {
|
||||
val isDarkMode = ThemeConfig.forceDarkMode ?: systemIsDark
|
||||
if (isDarkMode) {
|
||||
cardAlpha = 0.35f
|
||||
}
|
||||
}
|
||||
}
|
||||
themeMode = when (ThemeConfig.forceDarkMode) {
|
||||
true -> 2
|
||||
false -> 1
|
||||
null -> 0
|
||||
}
|
||||
}
|
||||
|
||||
// 主题色选项
|
||||
val themeColorOptions = listOf(
|
||||
stringResource(R.string.color_default) to ThemeColors.Default,
|
||||
stringResource(R.string.color_blue) to ThemeColors.Blue,
|
||||
stringResource(R.string.color_green) to ThemeColors.Green,
|
||||
stringResource(R.string.color_purple) to ThemeColors.Purple,
|
||||
stringResource(R.string.color_orange) to ThemeColors.Orange,
|
||||
stringResource(R.string.color_pink) to ThemeColors.Pink,
|
||||
stringResource(R.string.color_gray) to ThemeColors.Gray,
|
||||
stringResource(R.string.color_yellow) to ThemeColors.Yellow
|
||||
)
|
||||
|
||||
var showThemeColorDialog by remember { mutableStateOf(false) }
|
||||
|
||||
// 图片选择器
|
||||
val pickImageLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
) { uri: Uri? ->
|
||||
uri?.let {
|
||||
context.saveCustomBackground(it)
|
||||
isCustomBackgroundEnabled = true
|
||||
CardConfig.cardElevation = 0.dp
|
||||
saveCardConfig(context)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.more_settings)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navigator.popBackStack() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(top = 12.dp)
|
||||
) {
|
||||
// SELinux 开关
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Security,
|
||||
title = stringResource(R.string.selinux),
|
||||
summary = if (selinuxEnabled)
|
||||
stringResource(R.string.selinux_enabled) else
|
||||
stringResource(R.string.selinux_disabled),
|
||||
checked = selinuxEnabled
|
||||
) { enabled ->
|
||||
val command = if (enabled) "setenforce 1" else "setenforce 0"
|
||||
Shell.getShell().newJob().add(command).exec().let { result ->
|
||||
if (result.isSuccess) selinuxEnabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.AutoFixHigh, null) },
|
||||
headlineContent = { Text(stringResource(R.string.custom_settings)) },
|
||||
modifier = Modifier.clickable {
|
||||
isExpanded = !isExpanded
|
||||
}
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
||||
) {
|
||||
// 添加简洁模块开关
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Brush,
|
||||
title = stringResource(R.string.simple_mode),
|
||||
summary = stringResource(R.string.simple_mode_summary),
|
||||
checked = isSimpleMode
|
||||
) {
|
||||
onSimpleModeChange(it)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
||||
) {
|
||||
// 隐藏内核部分版本号
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_kernel_kernelsu_version),
|
||||
summary = stringResource(R.string.hide_kernel_kernelsu_version_summary),
|
||||
checked = isHideVersion
|
||||
) {
|
||||
onHideVersionChange(it)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
||||
) {
|
||||
// 模块数量等信息
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_other_info),
|
||||
summary = stringResource(R.string.hide_other_info_summary),
|
||||
checked = isHideOtherInfo
|
||||
) {
|
||||
onHideOtherInfoChange(it)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
||||
) {
|
||||
// SuSFS 状态信息
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(R.string.hide_susfs_status),
|
||||
summary = stringResource(R.string.hide_susfs_status_summary),
|
||||
checked = isHideSusfsStatus
|
||||
) {
|
||||
onHideSusfsStatusChange(it)
|
||||
}
|
||||
}
|
||||
|
||||
// region SUSFS 配置(仅在支持时显示)
|
||||
val suSFS = getSuSFS()
|
||||
val isSUS_SU = getSuSFSFeatures()
|
||||
if (suSFS == "Supported") {
|
||||
if (isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") {
|
||||
// 初始化时,默认启用
|
||||
var isEnabled by rememberSaveable {
|
||||
mutableStateOf(true) // 默认启用
|
||||
}
|
||||
|
||||
// 在启动时检查状态
|
||||
LaunchedEffect(Unit) {
|
||||
// 如果当前模式不是2就强制启用
|
||||
val currentMode = susfsSUS_SU_Mode()
|
||||
val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true)
|
||||
if (currentMode != "2" && wasManuallyDisabled) {
|
||||
susfsSUS_SU_2() // 强制切换到模式2
|
||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
||||
}
|
||||
isEnabled = currentMode == "2"
|
||||
}
|
||||
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.VisibilityOff,
|
||||
title = stringResource(id = R.string.settings_susfs_toggle),
|
||||
summary = stringResource(id = R.string.settings_susfs_toggle_summary),
|
||||
checked = isEnabled
|
||||
) {
|
||||
if (it) {
|
||||
// 手动启用
|
||||
susfsSUS_SU_2()
|
||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
||||
} else {
|
||||
// 手动关闭
|
||||
susfsSUS_SU_0()
|
||||
prefs.edit { putBoolean("enable_sus_su", false) }
|
||||
}
|
||||
isEnabled = it
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
// 动态颜色开关
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.ColorLens,
|
||||
title = stringResource(R.string.dynamic_color_title),
|
||||
summary = stringResource(R.string.dynamic_color_summary),
|
||||
checked = useDynamicColor
|
||||
) { enabled ->
|
||||
useDynamicColor = enabled
|
||||
context.saveDynamicColorState(enabled)
|
||||
}
|
||||
}
|
||||
// 只在未启用动态颜色时显示主题色选择
|
||||
AnimatedVisibility(
|
||||
visible = !useDynamicColor
|
||||
) {
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Default.Palette, null) },
|
||||
headlineContent = { Text(stringResource(R.string.theme_color)) },
|
||||
supportingContent = {
|
||||
val currentThemeName = when (ThemeConfig.currentTheme) {
|
||||
is ThemeColors.Default -> stringResource(R.string.color_default)
|
||||
is ThemeColors.Blue -> stringResource(R.string.color_blue)
|
||||
is ThemeColors.Green -> stringResource(R.string.color_green)
|
||||
is ThemeColors.Purple -> stringResource(R.string.color_purple)
|
||||
is ThemeColors.Orange -> stringResource(R.string.color_orange)
|
||||
is ThemeColors.Pink -> stringResource(R.string.color_pink)
|
||||
is ThemeColors.Gray -> stringResource(R.string.color_gray)
|
||||
is ThemeColors.Yellow -> stringResource(R.string.color_yellow)
|
||||
else -> stringResource(R.string.color_default)
|
||||
}
|
||||
Text(currentThemeName)
|
||||
},
|
||||
modifier = Modifier.clickable { showThemeColorDialog = true }
|
||||
)
|
||||
|
||||
if (showThemeColorDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showThemeColorDialog = false },
|
||||
title = { Text(stringResource(R.string.choose_theme_color)) },
|
||||
text = {
|
||||
Column {
|
||||
themeColorOptions.forEach { (name, theme) ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
context.saveThemeColors(when (theme) {
|
||||
ThemeColors.Default -> "default"
|
||||
ThemeColors.Blue -> "blue"
|
||||
ThemeColors.Green -> "green"
|
||||
ThemeColors.Purple -> "purple"
|
||||
ThemeColors.Orange -> "orange"
|
||||
ThemeColors.Pink -> "pink"
|
||||
ThemeColors.Gray -> "gray"
|
||||
ThemeColors.Yellow -> "yellow"
|
||||
else -> "default"
|
||||
})
|
||||
showThemeColorDialog = false
|
||||
}
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = ThemeConfig.currentTheme::class == theme::class,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.background(theme.Primary, shape = CircleShape)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
// 自定义背景开关
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.Wallpaper, null) },
|
||||
headlineContent = { Text(stringResource(id = R.string.settings_custom_background)) },
|
||||
supportingContent = { Text(stringResource(id = R.string.settings_custom_background_summary)) },
|
||||
modifier = Modifier.clickable {
|
||||
if (isCustomBackgroundEnabled) {
|
||||
showCardSettings = !showCardSettings
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = isCustomBackgroundEnabled,
|
||||
onCheckedChange = { isChecked ->
|
||||
if (isChecked) {
|
||||
pickImageLauncher.launch("image/*")
|
||||
} else {
|
||||
context.saveCustomBackground(null)
|
||||
isCustomBackgroundEnabled = false
|
||||
CardConfig.cardElevation = CardConfig.defaultElevation
|
||||
CardConfig.cardAlpha = 0.45f
|
||||
CardConfig.isCustomAlphaSet = false
|
||||
saveCardConfig(context)
|
||||
cardAlpha = 0.35f
|
||||
themeMode = 0
|
||||
context.saveThemeMode(null)
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
CardConfig.save(context)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
// 透明度 Slider
|
||||
AnimatedVisibility(
|
||||
visible = ThemeConfig.customBackgroundUri != null && showCardSettings,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
||||
) {
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.Opacity, null) },
|
||||
headlineContent = { Text(stringResource(R.string.settings_card_alpha)) },
|
||||
supportingContent = {
|
||||
Slider(
|
||||
value = cardAlpha,
|
||||
onValueChange = { newValue ->
|
||||
cardAlpha = newValue
|
||||
CardConfig.cardAlpha = newValue
|
||||
CardConfig.isCustomAlphaSet = true
|
||||
prefs.edit { putBoolean("is_custom_alpha_set", true) }
|
||||
prefs.edit { putFloat("card_alpha", newValue) }
|
||||
},
|
||||
onValueChangeFinished = {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
saveCardConfig(context)
|
||||
}
|
||||
},
|
||||
valueRange = 0f..1f,
|
||||
colors = getSliderColors(cardAlpha, useCustomColors = true),
|
||||
thumb = {
|
||||
SliderDefaults.Thumb(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
thumbSize = DpSize(0.dp, 0.dp)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = ThemeConfig.customBackgroundUri != null && showCardSettings,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
||||
){
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.DarkMode, null) },
|
||||
headlineContent = { Text(stringResource(R.string.theme_mode)) },
|
||||
supportingContent = { Text(themeOptions[themeMode]) },
|
||||
modifier = Modifier.clickable {
|
||||
showThemeModeDialog = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 主题模式选择对话框
|
||||
if (showThemeModeDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showThemeModeDialog = false },
|
||||
title = { Text(stringResource(R.string.theme_mode)) },
|
||||
text = {
|
||||
Column {
|
||||
themeOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
themeMode = index
|
||||
val newThemeMode = when(index) {
|
||||
0 -> null // 跟随系统
|
||||
1 -> false // 浅色
|
||||
2 -> true // 深色
|
||||
else -> null
|
||||
}
|
||||
context.saveThemeMode(newThemeMode)
|
||||
when (index) {
|
||||
2 -> {
|
||||
ThemeConfig.forceDarkMode = true
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
CardConfig.isUserDarkModeEnabled = true
|
||||
CardConfig.save(context)
|
||||
}
|
||||
1 -> {
|
||||
ThemeConfig.forceDarkMode = false
|
||||
CardConfig.isUserLightModeEnabled = true
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
CardConfig.save(context)
|
||||
}
|
||||
0 -> {
|
||||
ThemeConfig.forceDarkMode = null
|
||||
CardConfig.isUserLightModeEnabled = false
|
||||
CardConfig.isUserDarkModeEnabled = false
|
||||
CardConfig.save(context)
|
||||
}
|
||||
}
|
||||
showThemeModeDialog = false
|
||||
}
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = themeMode == index,
|
||||
onClick = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(option)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun getSliderColors(cardAlpha: Float, useCustomColors: Boolean = false): SliderColors {
|
||||
val theme = ThemeConfig.currentTheme
|
||||
val isDarkTheme = ThemeConfig.forceDarkMode ?: isSystemInDarkTheme()
|
||||
val useDynamicColor = ThemeConfig.useDynamicColor
|
||||
|
||||
return when {
|
||||
// 使用动态颜色时
|
||||
useDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
SliderDefaults.colors(
|
||||
activeTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),
|
||||
inactiveTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
|
||||
thumbColor = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
// 使用自定义主题色时
|
||||
useCustomColors -> {
|
||||
SliderDefaults.colors(
|
||||
activeTrackColor = theme.getCustomSliderActiveColor(),
|
||||
inactiveTrackColor = theme.getCustomSliderInactiveColor(),
|
||||
thumbColor = theme.Primary
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val activeColor = if (isDarkTheme) {
|
||||
theme.Primary.copy(alpha = cardAlpha)
|
||||
} else {
|
||||
theme.Primary.copy(alpha = cardAlpha)
|
||||
}
|
||||
val inactiveColor = if (isDarkTheme) {
|
||||
Color.DarkGray.copy(alpha = 0.3f)
|
||||
} else {
|
||||
Color.LightGray.copy(alpha = 0.3f)
|
||||
}
|
||||
SliderDefaults.colors(
|
||||
activeTrackColor = activeColor,
|
||||
inactiveTrackColor = inactiveColor,
|
||||
thumbColor = activeColor
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,547 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.safeDrawing
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
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.graphics.vector.ImageVector
|
||||
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.LineHeightStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.edit
|
||||
import com.maxkeppeker.sheets.core.models.base.IconSource
|
||||
import com.maxkeppeler.sheets.list.models.ListOption
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import zako.zako.zako.BuildConfig
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.R
|
||||
import zako.zako.zako.ui.component.AboutDialog
|
||||
import zako.zako.zako.ui.component.ConfirmResult
|
||||
import zako.zako.zako.ui.component.DialogHandle
|
||||
import zako.zako.zako.ui.component.SwitchItem
|
||||
import zako.zako.zako.ui.component.rememberConfirmDialog
|
||||
import zako.zako.zako.ui.component.rememberCustomDialog
|
||||
import zako.zako.zako.ui.component.rememberLoadingDialog
|
||||
import zako.zako.zako.ui.theme.CardConfig
|
||||
import zako.zako.zako.ui.theme.ThemeConfig
|
||||
import zako.zako.zako.ui.theme.getCardColors
|
||||
import zako.zako.zako.ui.theme.getCardElevation
|
||||
import zako.zako.zako.ui.util.LocalSnackbarHost
|
||||
import zako.zako.zako.ui.util.getBugreportFile
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
|
||||
/**
|
||||
* @author weishu
|
||||
* @date 2023/1/1.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||
// region 界面基础设置
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val snackBarHost = LocalSnackbarHost.current
|
||||
// endregion
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBar(
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||
) { paddingValues ->
|
||||
val aboutDialog = rememberCustomDialog {
|
||||
AboutDialog(it)
|
||||
}
|
||||
val loadingDialog = rememberLoadingDialog()
|
||||
// endregion
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// region 上下文与协程
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
// endregion
|
||||
|
||||
// region 日志导出功能
|
||||
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/gzip")
|
||||
) { uri: Uri? ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
scope.launch(Dispatchers.IO) {
|
||||
loadingDialog.show()
|
||||
context.contentResolver.openOutputStream(uri)?.use { output ->
|
||||
getBugreportFile(context).inputStream().use {
|
||||
it.copyTo(output)
|
||||
}
|
||||
}
|
||||
loadingDialog.hide()
|
||||
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
||||
}
|
||||
}
|
||||
// region 配置项列表
|
||||
// 配置文件模板入口
|
||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
||||
ListItem(
|
||||
leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) },
|
||||
headlineContent = { Text(profileTemplate) },
|
||||
supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) },
|
||||
modifier = Modifier.clickable {
|
||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||
}
|
||||
)
|
||||
// 卸载模块开关
|
||||
var umountChecked by rememberSaveable {
|
||||
mutableStateOf(Natives.isDefaultUmountModules())
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.FolderDelete,
|
||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
||||
checked = umountChecked
|
||||
) {
|
||||
if (Natives.setDefaultUmountModules(it)) {
|
||||
umountChecked = it
|
||||
}
|
||||
}
|
||||
// SU 禁用开关(仅在兼容版本显示)
|
||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
||||
var isSuDisabled by rememberSaveable {
|
||||
mutableStateOf(!Natives.isSuEnabled())
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.RemoveModerator,
|
||||
title = stringResource(id = R.string.settings_disable_su),
|
||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
||||
checked = isSuDisabled,
|
||||
) { checked ->
|
||||
val shouldEnable = !checked
|
||||
if (Natives.setSuEnabled(shouldEnable)) {
|
||||
isSuDisabled = !shouldEnable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
|
||||
// 更新检查开关
|
||||
var checkUpdate by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("check_update", true)
|
||||
)
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.Update,
|
||||
title = stringResource(id = R.string.settings_check_update),
|
||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
||||
checked = checkUpdate
|
||||
) {
|
||||
prefs.edit {putBoolean("check_update", it) }
|
||||
checkUpdate = it
|
||||
}
|
||||
|
||||
// Web调试开关
|
||||
var enableWebDebugging by rememberSaveable {
|
||||
mutableStateOf(
|
||||
prefs.getBoolean("enable_web_debugging", false)
|
||||
)
|
||||
}
|
||||
SwitchItem(
|
||||
icon = Icons.Filled.DeveloperMode,
|
||||
title = stringResource(id = R.string.enable_web_debugging),
|
||||
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
||||
checked = enableWebDebugging
|
||||
) {
|
||||
prefs.edit { putBoolean("enable_web_debugging", it) }
|
||||
enableWebDebugging = it
|
||||
}
|
||||
// 更多设置
|
||||
val newButtonTitle = stringResource(id = R.string.more_settings)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Settings,
|
||||
contentDescription = newButtonTitle
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(newButtonTitle) },
|
||||
supportingContent = { Text(stringResource(id = R.string.more_settings)) },
|
||||
modifier = Modifier.clickable {
|
||||
navigator.navigate(MoreSettingsScreenDestination)
|
||||
}
|
||||
)
|
||||
|
||||
var showBottomsheet by remember { mutableStateOf(false) }
|
||||
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.BugReport,
|
||||
stringResource(id = R.string.send_log)
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(stringResource(id = R.string.send_log)) },
|
||||
modifier = Modifier.clickable {
|
||||
showBottomsheet = true
|
||||
}
|
||||
)
|
||||
if (showBottomsheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { showBottomsheet = false },
|
||||
content = {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
|
||||
) {
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.clickable {
|
||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||
val current = LocalDateTime.now().format(formatter)
|
||||
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
||||
showBottomsheet = false
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Save,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.save_log),
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
textAlign = TextAlign.Center.also {
|
||||
LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
Box {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.clickable {
|
||||
scope.launch {
|
||||
val bugreport = loadingDialog.withLoading {
|
||||
withContext(Dispatchers.IO) {
|
||||
getBugreportFile(context)
|
||||
}
|
||||
}
|
||||
|
||||
val uri: Uri =
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||
bugreport
|
||||
)
|
||||
|
||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
setDataAndType(uri, "application/gzip")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
shareIntent,
|
||||
context.getString(R.string.send_log)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_log),
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
textAlign = TextAlign.Center.also {
|
||||
LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
||||
if (lkmMode) {
|
||||
UninstallItem(navigator) {
|
||||
loadingDialog.withLoading(it)
|
||||
}
|
||||
}
|
||||
|
||||
val about = stringResource(id = R.string.about)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.ContactPage,
|
||||
about
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(about) },
|
||||
modifier = Modifier.clickable {
|
||||
aboutDialog.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UninstallItem(
|
||||
navigator: DestinationsNavigator,
|
||||
withLoading: suspend (suspend () -> Unit) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val uninstallConfirmDialog = rememberConfirmDialog()
|
||||
val showTodo = {
|
||||
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
val uninstallDialog = rememberUninstallDialog { uninstallType ->
|
||||
scope.launch {
|
||||
val result = uninstallConfirmDialog.awaitConfirm(
|
||||
title = context.getString(uninstallType.title),
|
||||
content = context.getString(uninstallType.message)
|
||||
)
|
||||
if (result == ConfirmResult.Confirmed) {
|
||||
withLoading {
|
||||
when (uninstallType) {
|
||||
UninstallType.TEMPORARY -> showTodo()
|
||||
UninstallType.PERMANENT -> navigator.navigate(
|
||||
FlashScreenDestination(FlashIt.FlashUninstall)
|
||||
)
|
||||
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
|
||||
FlashScreenDestination(FlashIt.FlashRestore)
|
||||
)
|
||||
UninstallType.NONE -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val uninstall = stringResource(id = R.string.settings_uninstall)
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
uninstall
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(uninstall) },
|
||||
modifier = Modifier.clickable {
|
||||
uninstallDialog.show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) {
|
||||
TEMPORARY(
|
||||
R.string.settings_uninstall_temporary,
|
||||
R.string.settings_uninstall_temporary_message,
|
||||
Icons.Filled.Delete
|
||||
),
|
||||
PERMANENT(
|
||||
R.string.settings_uninstall_permanent,
|
||||
R.string.settings_uninstall_permanent_message,
|
||||
Icons.Filled.DeleteForever
|
||||
),
|
||||
RESTORE_STOCK_IMAGE(
|
||||
R.string.settings_restore_stock_image,
|
||||
R.string.settings_restore_stock_image_message,
|
||||
Icons.AutoMirrored.Filled.Undo
|
||||
),
|
||||
NONE(0, 0, Icons.Filled.Delete)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||
return rememberCustomDialog { dismiss ->
|
||||
val options = listOf(
|
||||
// UninstallType.TEMPORARY,
|
||||
UninstallType.PERMANENT,
|
||||
UninstallType.RESTORE_STOCK_IMAGE
|
||||
)
|
||||
val listOptions = options.map {
|
||||
ListOption(
|
||||
titleText = stringResource(it.title),
|
||||
subtitleText = if (it.message != 0) stringResource(it.message) else null,
|
||||
icon = IconSource(it.icon)
|
||||
)
|
||||
}
|
||||
|
||||
var selection = UninstallType.NONE
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(R.string.settings_uninstall))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
listOptions.forEachIndexed { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
selection = options[index]
|
||||
}
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = options[index].icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Column {
|
||||
Text(text = option.titleText)
|
||||
option.subtitleText?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
androidx.compose.material3.TextButton(
|
||||
onClick = {
|
||||
if (selection != UninstallType.NONE) {
|
||||
onSelected(selection)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
androidx.compose.material3.TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
tonalElevation = getCardElevation()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TopBar(
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||
) {
|
||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
val cardAlpha = CardConfig.cardAlpha
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.settings)) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||
),
|
||||
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SettingsPreview() {
|
||||
SettingScreen(EmptyDestinationsNavigator)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import zako.zako.zako.R
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.launch
|
||||
import zako.zako.zako.Natives
|
||||
import zako.zako.zako.ui.component.SearchAppBar
|
||||
import zako.zako.zako.ui.util.ModuleModify
|
||||
import zako.zako.zako.ui.viewmodel.SuperUserViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||
val viewModel = viewModel<SuperUserViewModel>()
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
val listState = rememberLazyListState()
|
||||
val context = LocalContext.current
|
||||
val snackBarHostState = remember { SnackbarHostState() }
|
||||
|
||||
// 添加备份和还原启动器
|
||||
val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState)
|
||||
val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState)
|
||||
|
||||
LaunchedEffect(key1 = navigator) {
|
||||
viewModel.search = ""
|
||||
if (viewModel.appList.isEmpty()) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(viewModel.search) {
|
||||
if (viewModel.search.isEmpty()) {
|
||||
listState.scrollToItem(0)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchAppBar(
|
||||
title = { Text(stringResource(R.string.superuser)) },
|
||||
searchText = viewModel.search,
|
||||
onSearchTextChange = { viewModel.search = it },
|
||||
onClearClick = { viewModel.search = "" },
|
||||
dropdownContent = {
|
||||
var showDropdown by remember { mutableStateOf(false) }
|
||||
|
||||
IconButton(
|
||||
onClick = { showDropdown = true },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(id = R.string.settings)
|
||||
)
|
||||
|
||||
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
||||
showDropdown = false
|
||||
}) {
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.refresh))
|
||||
}, onClick = {
|
||||
scope.launch {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
showDropdown = false
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Text(
|
||||
if (viewModel.showSystemApps) {
|
||||
stringResource(R.string.hide_system_apps)
|
||||
} else {
|
||||
stringResource(R.string.show_system_apps)
|
||||
}
|
||||
)
|
||||
}, onClick = {
|
||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
||||
showDropdown = false
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.backup_allowlist))
|
||||
}, onClick = {
|
||||
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
|
||||
showDropdown = false
|
||||
})
|
||||
DropdownMenuItem(text = {
|
||||
Text(stringResource(R.string.restore_allowlist))
|
||||
}, onClick = {
|
||||
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
|
||||
showDropdown = false
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHostState) },
|
||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
bottomBar = {
|
||||
// 批量操作按钮,直接放在底部栏
|
||||
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(true)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.batch_authorization))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
viewModel.updateBatchPermissions(false)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.batch_cancel_authorization))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
PullToRefreshBox(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
onRefresh = {
|
||||
scope.launch { viewModel.fetchAppList() }
|
||||
},
|
||||
isRefreshing = viewModel.isRefreshing
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
) {
|
||||
// 获取分组后的应用列表 - 修改分组逻辑,避免应用重复出现在多个分组中
|
||||
val rootApps = viewModel.appList.filter { it.allowSu }
|
||||
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
|
||||
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
|
||||
|
||||
// 显示ROOT权限应用组
|
||||
if (rootApps.isNotEmpty()) {
|
||||
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||
onSwitchChange = { allowSu ->
|
||||
scope.launch {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.showBatchActions) {
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
} else {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// 长按进入多选模式
|
||||
if (!viewModel.showBatchActions) {
|
||||
viewModel.toggleBatchMode()
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
}
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示自定义配置应用组
|
||||
if (customApps.isNotEmpty()) {
|
||||
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||
onSwitchChange = { allowSu ->
|
||||
scope.launch {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.showBatchActions) {
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
} else {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// 长按进入多选模式
|
||||
if (!viewModel.showBatchActions) {
|
||||
viewModel.toggleBatchMode()
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
}
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示其他应用组
|
||||
if (otherApps.isNotEmpty()) {
|
||||
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
||||
AppItem(
|
||||
app = app,
|
||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||
onSwitchChange = { allowSu ->
|
||||
scope.launch {
|
||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||
if (Natives.setAppProfile(updatedProfile)) {
|
||||
viewModel.fetchAppList()
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
if (viewModel.showBatchActions) {
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
} else {
|
||||
navigator.navigate(AppProfileScreenDestination(app))
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// 长按进入多选模式
|
||||
if (!viewModel.showBatchActions) {
|
||||
viewModel.toggleBatchMode()
|
||||
viewModel.toggleAppSelection(app.packageName)
|
||||
}
|
||||
},
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GroupHeader(title: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = TextStyle(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun AppItem(
|
||||
app: SuperUserViewModel.AppInfo,
|
||||
isSelected: Boolean,
|
||||
onToggleSelection: () -> Unit,
|
||||
onSwitchChange: (Boolean) -> Unit,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
viewModel: SuperUserViewModel
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = { onLongClick() },
|
||||
onTap = { onClick() }
|
||||
)
|
||||
},
|
||||
headlineContent = { Text(app.label) },
|
||||
supportingContent = {
|
||||
Column {
|
||||
Text(app.packageName)
|
||||
FlowRow {
|
||||
if (app.allowSu) {
|
||||
LabelText(label = "ROOT")
|
||||
} else {
|
||||
if (Natives.uidShouldUmount(app.uid)) {
|
||||
LabelText(label = "UMOUNT")
|
||||
}
|
||||
}
|
||||
if (app.hasCustomProfile) {
|
||||
LabelText(label = "CUSTOM")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
leadingContent = {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(app.packageInfo)
|
||||
.crossfade(true)
|
||||
.build(),
|
||||
contentDescription = app.label,
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.width(48.dp)
|
||||
.height(48.dp)
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
if (!viewModel.showBatchActions) {
|
||||
Switch(
|
||||
checked = app.allowSu,
|
||||
onCheckedChange = onSwitchChange
|
||||
)
|
||||
} else {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onToggleSelection() }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LabelText(label: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp, end = 4.dp)
|
||||
.background(
|
||||
Color.Black,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp),
|
||||
style = TextStyle(
|
||||
fontSize = 8.sp,
|
||||
color = Color.White,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,556 +0,0 @@
|
||||
package zako.zako.zako.ui.screen
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import zako.zako.zako.ui.component.ConfirmResult
|
||||
import zako.zako.zako.ui.component.SearchAppBar
|
||||
import zako.zako.zako.ui.component.rememberConfirmDialog
|
||||
import zako.zako.zako.ui.theme.getCardColors
|
||||
import zako.zako.zako.ui.theme.getCardElevation
|
||||
import zako.zako.zako.ui.viewmodel.KpmViewModel
|
||||
import zako.zako.zako.ui.util.loadKpmModule
|
||||
import zako.zako.zako.ui.util.unloadKpmModule
|
||||
import java.io.File
|
||||
import androidx.core.content.edit
|
||||
import zako.zako.zako.ui.theme.ThemeConfig
|
||||
import zako.zako.zako.ui.component.rememberCustomDialog
|
||||
import zako.zako.zako.ui.component.ConfirmDialogHandle
|
||||
import zako.zako.zako.R
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
|
||||
/**
|
||||
* KPM 管理界面
|
||||
* 以下内核模块功能由KernelPatch开发,经过修改后加入SukiSU Ultra的内核模块功能
|
||||
* 开发者:zako, Liaokong
|
||||
*/
|
||||
var globalModuleFileName: String = ""
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun KpmScreen(
|
||||
navigator: DestinationsNavigator,
|
||||
viewModel: KpmViewModel = viewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackBarHost = remember { SnackbarHostState() }
|
||||
val confirmDialog = rememberConfirmDialog()
|
||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||
ThemeConfig.currentTheme.ButtonContrast
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
|
||||
val kpmInstallSuccess = stringResource(R.string.kpm_install_success)
|
||||
val kpmInstallFailed = stringResource(R.string.kpm_install_failed)
|
||||
val cancel = stringResource(R.string.cancel)
|
||||
val uninstall = stringResource(R.string.uninstall)
|
||||
val failedToCheckModuleFile = stringResource(R.string.snackbar_failed_to_check_module_file)
|
||||
val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success)
|
||||
val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed)
|
||||
val kpmInstallMode = stringResource(R.string.kpm_install_mode)
|
||||
val kpmInstallModeLoad = stringResource(R.string.kpm_install_mode_load)
|
||||
val kpmInstallModeEmbed = stringResource(R.string.kpm_install_mode_embed)
|
||||
val kpmInstallModeDescription = stringResource(R.string.kpm_install_mode_description)
|
||||
val invalidFileTypeMessage = stringResource(R.string.invalid_file_type)
|
||||
val confirmTitle = stringResource(R.string.confirm_uninstall_title_with_filename)
|
||||
val confirmContent = stringResource(R.string.confirm_uninstall_content, globalModuleFileName)
|
||||
|
||||
var tempFileForInstall by remember { mutableStateOf<File?>(null) }
|
||||
val installModeDialog = rememberCustomDialog { dismiss ->
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
dismiss()
|
||||
tempFileForInstall?.delete()
|
||||
tempFileForInstall = null
|
||||
},
|
||||
title = { Text(kpmInstallMode) },
|
||||
text = { Text(kpmInstallModeDescription) },
|
||||
confirmButton = {
|
||||
Column {
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dismiss()
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
handleModuleInstall(
|
||||
tempFile = tempFile,
|
||||
isEmbed = false,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmInstallSuccess = kpmInstallSuccess,
|
||||
kpmInstallFailed = kpmInstallFailed
|
||||
)
|
||||
}
|
||||
tempFileForInstall = null
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(kpmInstallModeLoad)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
dismiss()
|
||||
tempFileForInstall?.let { tempFile ->
|
||||
handleModuleInstall(
|
||||
tempFile = tempFile,
|
||||
isEmbed = true,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmInstallSuccess = kpmInstallSuccess,
|
||||
kpmInstallFailed = kpmInstallFailed
|
||||
)
|
||||
}
|
||||
tempFileForInstall = null
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(kpmInstallModeEmbed)
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
dismiss()
|
||||
tempFileForInstall?.delete()
|
||||
tempFileForInstall = null
|
||||
}
|
||||
) {
|
||||
Text(cancel)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val selectPatchLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult
|
||||
|
||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||
|
||||
scope.launch {
|
||||
val fileName = uri.lastPathSegment ?: "unknown.kpm"
|
||||
val encodedFileName = URLEncoder.encode(fileName, "UTF-8")
|
||||
val tempFile = File(context.cacheDir, encodedFileName)
|
||||
|
||||
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
if (!tempFile.name.endsWith(".kpm")) {
|
||||
snackBarHost.showSnackbar(
|
||||
message = invalidFileTypeMessage,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
tempFile.delete()
|
||||
return@launch
|
||||
}
|
||||
|
||||
tempFileForInstall = tempFile
|
||||
installModeDialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
while(true) {
|
||||
viewModel.fetchModuleList()
|
||||
delay(5000)
|
||||
}
|
||||
}
|
||||
|
||||
val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
|
||||
var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SearchAppBar(
|
||||
title = { Text(stringResource(R.string.kpm_title)) },
|
||||
searchText = viewModel.search,
|
||||
onSearchTextChange = { viewModel.search = it },
|
||||
onClearClick = { viewModel.search = "" },
|
||||
scrollBehavior = scrollBehavior,
|
||||
dropdownContent = {
|
||||
IconButton(onClick = { viewModel.fetchModuleList() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Refresh,
|
||||
contentDescription = stringResource(R.string.refresh)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
ExtendedFloatingActionButton(
|
||||
onClick = {
|
||||
selectPatchLauncher.launch(
|
||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||
type = "*/*"
|
||||
}
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription = stringResource(R.string.kpm_install)
|
||||
)
|
||||
},
|
||||
text = { Text(stringResource(R.string.kpm_install)) },
|
||||
containerColor = cardColor.copy(alpha = 1f),
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
if (!isNoticeClosed) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.kernel_module_notice),
|
||||
modifier = Modifier.weight(1f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
IconButton(onClick = {
|
||||
isNoticeClosed = true
|
||||
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.close_notice)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (viewModel.moduleList.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.kpm_empty),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
items(viewModel.moduleList) { module ->
|
||||
KpmModuleItem(
|
||||
module = module,
|
||||
onUninstall = {
|
||||
scope.launch {
|
||||
handleModuleUninstall(
|
||||
module = module,
|
||||
viewModel = viewModel,
|
||||
snackBarHost = snackBarHost,
|
||||
kpmUninstallSuccess = kpmUninstallSuccess,
|
||||
kpmUninstallFailed = kpmUninstallFailed,
|
||||
failedToCheckModuleFile = failedToCheckModuleFile,
|
||||
uninstall = uninstall,
|
||||
cancel = cancel,
|
||||
confirmDialog = confirmDialog,
|
||||
confirmTitle = confirmTitle,
|
||||
confirmContent = confirmContent
|
||||
)
|
||||
}
|
||||
},
|
||||
onControl = {
|
||||
viewModel.loadModuleDetail(module.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleModuleInstall(
|
||||
tempFile: File,
|
||||
isEmbed: Boolean,
|
||||
viewModel: KpmViewModel,
|
||||
snackBarHost: SnackbarHostState,
|
||||
kpmInstallSuccess: String,
|
||||
kpmInstallFailed: String
|
||||
) {
|
||||
val moduleId = extractModuleId(tempFile.name)
|
||||
if (moduleId == null) {
|
||||
Log.e("KsuCli", "Failed to extract module ID from file: ${tempFile.name}")
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
tempFile.delete()
|
||||
return
|
||||
}
|
||||
|
||||
val targetPath = "/data/adb/kpm/$moduleId.kpm"
|
||||
|
||||
try {
|
||||
if (isEmbed) {
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p /data/adb/kpm")).waitFor()
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "cp ${tempFile.absolutePath} $targetPath")).waitFor()
|
||||
}
|
||||
|
||||
val loadResult = loadKpmModule(tempFile.absolutePath)
|
||||
if (loadResult.startsWith("Error")) {
|
||||
Log.e("KsuCli", "Failed to load KPM module: $loadResult")
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
} else {
|
||||
viewModel.fetchModuleList()
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallSuccess,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to load KPM module: ${e.message}", e)
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmInstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
tempFile.delete()
|
||||
}
|
||||
|
||||
private fun extractModuleId(fileName: String): String? {
|
||||
return try {
|
||||
val decodedFileName = URLDecoder.decode(fileName, "UTF-8")
|
||||
val pattern = "([^/]*?)\\.kpm$".toRegex()
|
||||
val matchResult = pattern.find(decodedFileName)
|
||||
matchResult?.groupValues?.get(1)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to extract module ID: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleModuleUninstall(
|
||||
module: KpmViewModel.ModuleInfo,
|
||||
viewModel: KpmViewModel,
|
||||
snackBarHost: SnackbarHostState,
|
||||
kpmUninstallSuccess: String,
|
||||
kpmUninstallFailed: String,
|
||||
failedToCheckModuleFile: String,
|
||||
uninstall: String,
|
||||
cancel: String,
|
||||
confirmTitle : String,
|
||||
confirmContent : String,
|
||||
confirmDialog: ConfirmDialogHandle
|
||||
) {
|
||||
val moduleFileName = "${module.id}.kpm"
|
||||
globalModuleFileName = moduleFileName
|
||||
val moduleFilePath = "/data/adb/kpm/$moduleFileName"
|
||||
|
||||
val fileExists = try {
|
||||
val result = Runtime.getRuntime().exec(arrayOf("su", "-c", "ls /data/adb/kpm/$moduleFileName")).waitFor() == 0
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e)
|
||||
snackBarHost.showSnackbar(
|
||||
message = failedToCheckModuleFile,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
false
|
||||
}
|
||||
val confirmResult = confirmDialog.awaitConfirm(
|
||||
title = confirmTitle,
|
||||
content = confirmContent,
|
||||
confirm = uninstall,
|
||||
dismiss = cancel
|
||||
)
|
||||
|
||||
if (confirmResult == ConfirmResult.Confirmed) {
|
||||
try {
|
||||
val unloadResult = unloadKpmModule(module.id)
|
||||
if (unloadResult.startsWith("Error")) {
|
||||
Log.e("KsuCli", "Failed to unload KPM module: $unloadResult")
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (fileExists) {
|
||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm $moduleFilePath")).waitFor()
|
||||
}
|
||||
|
||||
viewModel.fetchModuleList()
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallSuccess,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("KsuCli", "Failed to unload KPM module: ${e.message}", e)
|
||||
snackBarHost.showSnackbar(
|
||||
message = kpmUninstallFailed,
|
||||
duration = SnackbarDuration.Short
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KpmModuleItem(
|
||||
module: KpmViewModel.ModuleInfo,
|
||||
onUninstall: () -> Unit,
|
||||
onControl: () -> Unit
|
||||
) {
|
||||
val viewModel: KpmViewModel = viewModel()
|
||||
val scope = rememberCoroutineScope()
|
||||
val snackBarHost = remember { SnackbarHostState() }
|
||||
val successMessage = stringResource(R.string.kpm_control_success)
|
||||
val failureMessage = stringResource(R.string.kpm_control_failed)
|
||||
|
||||
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.hideInputDialog() },
|
||||
title = { Text(stringResource(R.string.kpm_control)) },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = viewModel.inputArgs,
|
||||
onValueChange = { viewModel.updateInputArgs(it) },
|
||||
label = { Text(stringResource(R.string.kpm_args)) },
|
||||
placeholder = { Text(module.args) }
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val result = viewModel.executeControl()
|
||||
val message = when (result) {
|
||||
0 -> successMessage
|
||||
else -> failureMessage
|
||||
}
|
||||
snackBarHost.showSnackbar(message)
|
||||
onControl()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = module.name,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Text(
|
||||
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = module.description,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = { viewModel.showInputDialog(module.id) },
|
||||
enabled = module.hasAction
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(stringResource(R.string.kpm_control))
|
||||
}
|
||||
|
||||
FilledTonalButton(
|
||||
onClick = onUninstall
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = null
|
||||
)
|
||||
Text(stringResource(R.string.kpm_uninstall))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package zako.zako.zako.ui.theme
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object CardConfig {
|
||||
val defaultElevation: Dp = 0.dp
|
||||
|
||||
var cardAlpha by mutableStateOf(0.45f)
|
||||
var cardElevation by mutableStateOf(defaultElevation)
|
||||
var isShadowEnabled by mutableStateOf(true)
|
||||
var isCustomAlphaSet by mutableStateOf(false)
|
||||
var isUserDarkModeEnabled by mutableStateOf(false)
|
||||
var isUserLightModeEnabled by mutableStateOf(false)
|
||||
var isCustomBackgroundEnabled by mutableStateOf(false)
|
||||
|
||||
fun save(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
prefs.edit().apply {
|
||||
putFloat("card_alpha", cardAlpha)
|
||||
putBoolean("custom_background_enabled", cardElevation == 0.dp)
|
||||
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
||||
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
||||
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
||||
putBoolean("is_custom_background_enabled", isCustomBackgroundEnabled)
|
||||
apply()
|
||||
}
|
||||
}
|
||||
|
||||
fun load(context: Context) {
|
||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
cardAlpha = prefs.getFloat("card_alpha", 0.45f)
|
||||
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
|
||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
||||
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
||||
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
||||
isCustomBackgroundEnabled = prefs.getBoolean("is_custom_background_enabled", false)
|
||||
}
|
||||
|
||||
fun updateShadowEnabled(enabled: Boolean) {
|
||||
isShadowEnabled = enabled
|
||||
cardElevation = if (enabled) defaultElevation else 0.dp
|
||||
}
|
||||
|
||||
fun setDarkModeDefaults() {
|
||||
if (!isCustomAlphaSet) {
|
||||
cardAlpha = 0.35f
|
||||
cardElevation = 0.dp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun getCardColors(originalColor: Color) = CardDefaults.elevatedCardColors(
|
||||
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
|
||||
contentColor = when {
|
||||
CardConfig.isUserLightModeEnabled -> {
|
||||
Color.Black
|
||||
}
|
||||
CardConfig.isUserDarkModeEnabled -> {
|
||||
Color.White
|
||||
}
|
||||
!isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled -> {
|
||||
Color.Black
|
||||
}
|
||||
!isSystemInDarkTheme() && !CardConfig.isCustomBackgroundEnabled && !CardConfig.isUserDarkModeEnabled && originalColor.luminance() > 0.3 -> {
|
||||
Color.Black
|
||||
}
|
||||
isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled && !CardConfig.isUserLightModeEnabled-> {
|
||||
Color.White
|
||||
}
|
||||
else -> {
|
||||
Color.White
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
fun getCardElevation() = CardConfig.cardElevation
|
||||
@@ -1,170 +0,0 @@
|
||||
package zako.zako.zako.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
sealed class ThemeColors {
|
||||
abstract val Primary: Color
|
||||
abstract val Secondary: Color
|
||||
abstract val Tertiary: Color
|
||||
abstract val OnPrimary: Color
|
||||
abstract val OnSecondary: Color
|
||||
abstract val OnTertiary: Color
|
||||
abstract val PrimaryContainer: Color
|
||||
abstract val SecondaryContainer: Color
|
||||
abstract val TertiaryContainer: Color
|
||||
abstract val OnPrimaryContainer: Color
|
||||
abstract val OnSecondaryContainer: Color
|
||||
abstract val OnTertiaryContainer: Color
|
||||
abstract val ButtonContrast: Color
|
||||
|
||||
open fun getCustomSliderActiveColor(): Color = Primary
|
||||
open fun getCustomSliderInactiveColor(): Color = PrimaryContainer
|
||||
|
||||
// Default Theme (white)
|
||||
object Default : ThemeColors() {
|
||||
override val Primary = Color(0xFFFFFFFF)
|
||||
override val Secondary = Color(0xFFF5F5F5)
|
||||
override val Tertiary = Color(0xFFE0E0E0)
|
||||
override val OnPrimary = Color(0xFF616161)
|
||||
override val OnSecondary = Color(0xFF616161)
|
||||
override val OnTertiary = Color(0xFF616161)
|
||||
override val PrimaryContainer = Color(0xFFF5F5F5)
|
||||
override val SecondaryContainer = Color(0xFFEEEEEE)
|
||||
override val TertiaryContainer = Color(0xFFE0E0E0)
|
||||
override val OnPrimaryContainer = Color(0xFF000000)
|
||||
override val OnSecondaryContainer = Color(0xFF000000)
|
||||
override val OnTertiaryContainer = Color(0xFF000000)
|
||||
override val ButtonContrast = Color(0xFF00BFFF)
|
||||
}
|
||||
|
||||
// Blue Theme
|
||||
object Blue : ThemeColors() {
|
||||
override val Primary = Color(0xFF2196F3)
|
||||
override val Secondary = Color(0xFF1E88E5)
|
||||
override val Tertiary = Color(0xFF0D47A1)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFCBE6FC)
|
||||
override val SecondaryContainer = Color(0xFFBBDEFB)
|
||||
override val TertiaryContainer = Color(0xFF90CAF9)
|
||||
override val OnPrimaryContainer = Color(0xFF0A1A2E)
|
||||
override val OnSecondaryContainer = Color(0xFF0A192D)
|
||||
override val OnTertiaryContainer = Color(0xFF071B3D)
|
||||
override val ButtonContrast = Color(0xFF00BFFF)
|
||||
}
|
||||
|
||||
// Green Theme
|
||||
object Green : ThemeColors() {
|
||||
override val Primary = Color(0xFF4CAF50)
|
||||
override val Secondary = Color(0xFF43A047)
|
||||
override val Tertiary = Color(0xFF1B5E20)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFC8E6C9)
|
||||
override val SecondaryContainer = Color(0xFFA5D6A7)
|
||||
override val TertiaryContainer = Color(0xFF81C784)
|
||||
override val OnPrimaryContainer = Color(0xFF0A1F0B)
|
||||
override val OnSecondaryContainer = Color(0xFF0A1D0B)
|
||||
override val OnTertiaryContainer = Color(0xFF071F09)
|
||||
override val ButtonContrast = Color(0xFF32CD32)
|
||||
}
|
||||
|
||||
// Purple Theme
|
||||
object Purple : ThemeColors() {
|
||||
override val Primary = Color(0xFF9C27B0)
|
||||
override val Secondary = Color(0xFF8E24AA)
|
||||
override val Tertiary = Color(0xFF4A148C)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFE1BEE7)
|
||||
override val SecondaryContainer = Color(0xFFCE93D8)
|
||||
override val TertiaryContainer = Color(0xFFB39DDB)
|
||||
override val OnPrimaryContainer = Color(0xFF1F0A23)
|
||||
override val OnSecondaryContainer = Color(0xFF1C0A21)
|
||||
override val OnTertiaryContainer = Color(0xFF12071C)
|
||||
override val ButtonContrast = Color(0xFFDA70D6)
|
||||
}
|
||||
|
||||
// Orange Theme
|
||||
object Orange : ThemeColors() {
|
||||
override val Primary = Color(0xFFFF9800)
|
||||
override val Secondary = Color(0xFFFB8C00)
|
||||
override val Tertiary = Color(0xFFE65100)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFFE0B2)
|
||||
override val SecondaryContainer = Color(0xFFFFCC80)
|
||||
override val TertiaryContainer = Color(0xFFFFB74D)
|
||||
override val OnPrimaryContainer = Color(0xFF1A1100)
|
||||
override val OnSecondaryContainer = Color(0xFF1A1000)
|
||||
override val OnTertiaryContainer = Color(0xFF1A0B00)
|
||||
override val ButtonContrast = Color(0xFFFF6347)
|
||||
}
|
||||
|
||||
// Pink Theme
|
||||
object Pink : ThemeColors() {
|
||||
override val Primary = Color(0xFFE91E63)
|
||||
override val Secondary = Color(0xFFD81B60)
|
||||
override val Tertiary = Color(0xFF880E4F)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFF8BBD0)
|
||||
override val SecondaryContainer = Color(0xFFF48FB1)
|
||||
override val TertiaryContainer = Color(0xFFE91E63)
|
||||
override val OnPrimaryContainer = Color(0xFF2E0A14)
|
||||
override val OnSecondaryContainer = Color(0xFF2B0A13)
|
||||
override val OnTertiaryContainer = Color(0xFF1C0311)
|
||||
override val ButtonContrast = Color(0xFFFF1493)
|
||||
}
|
||||
|
||||
// Gray Theme
|
||||
object Gray : ThemeColors() {
|
||||
override val Primary = Color(0xFF9E9E9E)
|
||||
override val Secondary = Color(0xFF757575)
|
||||
override val Tertiary = Color(0xFF616161)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFEEEEEE)
|
||||
override val SecondaryContainer = Color(0xFFE0E0E0)
|
||||
override val TertiaryContainer = Color(0xFFBDBDBD)
|
||||
override val OnPrimaryContainer = Color(0xFF1A1A1A)
|
||||
override val OnSecondaryContainer = Color(0xFF171717)
|
||||
override val OnTertiaryContainer = Color(0xFF141414)
|
||||
override val ButtonContrast = Color(0xFF696969)
|
||||
}
|
||||
|
||||
object Yellow : ThemeColors() {
|
||||
override val Primary = Color(0xFFFFD700)
|
||||
override val Secondary = Color(0xFFFFBC52)
|
||||
override val Tertiary = Color(0xFF795548)
|
||||
override val OnPrimary = Color(0xFFFFFFFF)
|
||||
override val OnSecondary = Color(0xFFFFFFFF)
|
||||
override val OnTertiary = Color(0xFFFFFFFF)
|
||||
override val PrimaryContainer = Color(0xFFFFF7D6)
|
||||
override val SecondaryContainer = Color(0xFFFFE6B3)
|
||||
override val TertiaryContainer = Color(0xFFD7CCC8)
|
||||
override val OnPrimaryContainer = Color(0xFF1A1600)
|
||||
override val OnSecondaryContainer = Color(0xFF1A1100)
|
||||
override val OnTertiaryContainer = Color(0xFF1A1717)
|
||||
override val ButtonContrast = Color(0xFFFFD700)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
|
||||
"blue" -> Blue
|
||||
"green" -> Green
|
||||
"purple" -> Purple
|
||||
"orange" -> Orange
|
||||
"pink" -> Pink
|
||||
"gray" -> Gray
|
||||
"white" -> Yellow
|
||||
else -> Default
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
package zako.zako.zako.ui.theme
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.paint
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.zIndex
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
|
||||
object ThemeConfig {
|
||||
var customBackgroundUri by mutableStateOf<Uri?>(null)
|
||||
var forceDarkMode by mutableStateOf<Boolean?>(null)
|
||||
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
|
||||
var useDynamicColor by mutableStateOf(false)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getDarkColorScheme() = darkColorScheme(
|
||||
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
|
||||
onPrimary = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
|
||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
|
||||
onPrimaryContainer = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
|
||||
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
|
||||
onSecondary = mixColors(ThemeConfig.currentTheme.Secondary, Color.White, 0.2f),
|
||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
|
||||
onSecondaryContainer = mixColors(ThemeConfig.currentTheme.Secondary, Color.White, 0.2f),
|
||||
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
|
||||
onTertiary = mixColors(ThemeConfig.currentTheme.Tertiary, Color.White, 0.2f),
|
||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
|
||||
onTertiaryContainer = mixColors(ThemeConfig.currentTheme.Tertiary, Color.White, 0.2f),
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.1f),
|
||||
onSurface = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.1f),
|
||||
surfaceVariant = Color(0xFF2F2F2F),
|
||||
onSurfaceVariant = mixColors(ThemeConfig.currentTheme.Primary, Color.White, 0.2f),
|
||||
outline = Color.White.copy(alpha = 0.12f),
|
||||
outlineVariant = Color.White.copy(alpha = 0.12f)
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun getLightColorScheme() = lightColorScheme(
|
||||
primary = ThemeConfig.currentTheme.Primary,
|
||||
onPrimary = ThemeConfig.currentTheme.OnPrimary,
|
||||
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
|
||||
onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer,
|
||||
secondary = ThemeConfig.currentTheme.Secondary,
|
||||
onSecondary = ThemeConfig.currentTheme.OnSecondary,
|
||||
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer,
|
||||
onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer,
|
||||
tertiary = ThemeConfig.currentTheme.Tertiary,
|
||||
onTertiary = ThemeConfig.currentTheme.OnTertiary,
|
||||
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer,
|
||||
onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer,
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.Black.copy(alpha = 0.87f),
|
||||
onSurface = Color.Black.copy(alpha = 0.87f),
|
||||
surfaceVariant = Color(0xFFF5F5F5),
|
||||
onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
|
||||
outline = Color.Black.copy(alpha = 0.12f),
|
||||
outlineVariant = Color.Black.copy(alpha = 0.12f)
|
||||
)
|
||||
|
||||
// 复制图片到应用内部存储
|
||||
fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
|
||||
try {
|
||||
val contentResolver: ContentResolver = contentResolver
|
||||
val inputStream: InputStream = contentResolver.openInputStream(uri)!!
|
||||
val fileName = "custom_background.jpg"
|
||||
val file = File(filesDir, fileName)
|
||||
val outputStream = FileOutputStream(file)
|
||||
val buffer = ByteArray(4 * 1024)
|
||||
var read: Int
|
||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||
outputStream.write(buffer, 0, read)
|
||||
}
|
||||
outputStream.flush()
|
||||
outputStream.close()
|
||||
inputStream.close()
|
||||
return Uri.fromFile(file)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ImageCopy", "Failed to copy image: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KernelSUTheme(
|
||||
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
|
||||
true -> true
|
||||
false -> false
|
||||
null -> isSystemInDarkTheme()
|
||||
},
|
||||
dynamicColor: Boolean = ThemeConfig.useDynamicColor,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
context.loadCustomBackground()
|
||||
context.loadThemeColors()
|
||||
context.loadDynamicColorState()
|
||||
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
if (darkTheme) {
|
||||
val originalScheme = dynamicDarkColorScheme(context)
|
||||
originalScheme.copy(
|
||||
// 调整按钮相关颜色
|
||||
primary = adjustColor(originalScheme.primary),
|
||||
onPrimary = adjustColor(originalScheme.onPrimary),
|
||||
primaryContainer = adjustColor(originalScheme.primaryContainer),
|
||||
onPrimaryContainer = adjustColor(originalScheme.onPrimaryContainer),
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent,
|
||||
onBackground = Color.White,
|
||||
onSurface = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onSecondaryContainer = Color.White,
|
||||
onTertiaryContainer = Color.White
|
||||
)
|
||||
} else {
|
||||
val originalScheme = dynamicLightColorScheme(context)
|
||||
originalScheme.copy(
|
||||
primary = adjustColor(originalScheme.primary),
|
||||
onPrimary = adjustColor(originalScheme.onPrimary),
|
||||
primaryContainer = adjustColor(originalScheme.primaryContainer),
|
||||
onPrimaryContainer = adjustColor(originalScheme.onPrimaryContainer),
|
||||
background = Color.Transparent,
|
||||
surface = Color.Transparent
|
||||
)
|
||||
}
|
||||
}
|
||||
darkTheme -> getDarkColorScheme()
|
||||
else -> getLightColorScheme()
|
||||
}
|
||||
|
||||
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
|
||||
|
||||
if (darkTheme && !dynamicColor) {
|
||||
CardConfig.setDarkModeDefaults()
|
||||
}
|
||||
|
||||
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
// 背景图层
|
||||
ThemeConfig.customBackgroundUri?.let { uri ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(-1f)
|
||||
) {
|
||||
// 背景图片
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.paint(
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = uri,
|
||||
onError = {
|
||||
ThemeConfig.customBackgroundUri = null
|
||||
context.saveCustomBackground(null)
|
||||
}
|
||||
),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
)
|
||||
|
||||
// 亮度调节层
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
if (darkTheme) {
|
||||
Color.Black.copy(alpha = 0.4f)
|
||||
} else {
|
||||
Color.White.copy(alpha = 0.1f)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
// 边缘渐变遮罩层
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
if (darkTheme) {
|
||||
Color.Black.copy(alpha = 0.5f)
|
||||
} else {
|
||||
Color.Black.copy(alpha = 0.2f)
|
||||
}
|
||||
),
|
||||
radius = 1200f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
// 内容图层
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zIndex(1f)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.saveCustomBackground(uri: Uri?) {
|
||||
val newUri = uri?.let { copyImageToInternalStorage(it) }
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("custom_background", newUri?.toString())
|
||||
}
|
||||
ThemeConfig.customBackgroundUri = newUri
|
||||
}
|
||||
|
||||
fun Context.loadCustomBackground() {
|
||||
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("custom_background", null)
|
||||
ThemeConfig.customBackgroundUri = uriString?.toUri()
|
||||
}
|
||||
|
||||
fun Context.saveThemeMode(forceDark: Boolean?) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString(
|
||||
"theme_mode", when (forceDark) {
|
||||
true -> "dark"
|
||||
false -> "light"
|
||||
null -> "system"
|
||||
}
|
||||
)
|
||||
}
|
||||
ThemeConfig.forceDarkMode = forceDark
|
||||
}
|
||||
|
||||
fun Context.loadThemeMode() {
|
||||
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("theme_mode", "system")
|
||||
ThemeConfig.forceDarkMode = when(mode) {
|
||||
"dark" -> true
|
||||
"light" -> false
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.saveThemeColors(themeName: String) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putString("theme_colors", themeName)
|
||||
}
|
||||
|
||||
ThemeConfig.currentTheme = when(themeName) {
|
||||
"blue" -> ThemeColors.Blue
|
||||
"green" -> ThemeColors.Green
|
||||
"purple" -> ThemeColors.Purple
|
||||
"orange" -> ThemeColors.Orange
|
||||
"pink" -> ThemeColors.Pink
|
||||
"gray" -> ThemeColors.Gray
|
||||
"yellow" -> ThemeColors.Yellow
|
||||
else -> ThemeColors.Default
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.loadThemeColors() {
|
||||
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getString("theme_colors", "default")
|
||||
|
||||
ThemeConfig.currentTheme = when(themeName) {
|
||||
"blue" -> ThemeColors.Blue
|
||||
"green" -> ThemeColors.Green
|
||||
"purple" -> ThemeColors.Purple
|
||||
"orange" -> ThemeColors.Orange
|
||||
"pink" -> ThemeColors.Pink
|
||||
"gray" -> ThemeColors.Gray
|
||||
"yellow" -> ThemeColors.Yellow
|
||||
else -> ThemeColors.Default
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.saveDynamicColorState(enabled: Boolean) {
|
||||
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.edit {
|
||||
putBoolean("use_dynamic_color", enabled)
|
||||
}
|
||||
ThemeConfig.useDynamicColor = enabled
|
||||
}
|
||||
|
||||
fun Context.loadDynamicColorState() {
|
||||
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||
.getBoolean("use_dynamic_color", true)
|
||||
ThemeConfig.useDynamicColor = enabled
|
||||
}
|
||||
|
||||
private fun adjustColor(color: Color): Color {
|
||||
val minLuminance = 0.75f
|
||||
val maxLuminance = 1f
|
||||
var luminance = color.luminance()
|
||||
if (luminance < minLuminance) {
|
||||
luminance = minLuminance
|
||||
} else if (luminance > maxLuminance) {
|
||||
luminance = maxLuminance
|
||||
}
|
||||
return color.copy(luminance)
|
||||
}
|
||||
|
||||
private fun mixColors(color1: Color, color2: Color, ratio: Float): Color {
|
||||
val r = (color1.red * ratio + color2.red * (1 - ratio))
|
||||
val g = (color1.green * ratio + color2.green * (1 - ratio))
|
||||
val b = (color1.blue * ratio + color2.blue * (1 - ratio))
|
||||
val a = (color1.alpha * ratio + color2.alpha * (1 - ratio))
|
||||
return Color(r, g, b, a)
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package zako.zako.zako.ui.theme
|
||||
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = androidx.compose.material3.Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
2
manager/app/src/main/jniLibs/.gitignore
vendored
2
manager/app/src/main/jniLibs/.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
libzakozako.so
|
||||
libzakozakozako.so
|
||||
libkpmmgr.so
|
||||
libzako.so
|
||||
libandroidx.graphics.path.so
|
||||
@@ -50,7 +50,6 @@
|
||||
<string name="home_click_to_learn_kernelsu">یاد بگیرید چگونه از کرنل اس یو و ماژول ها استفاده کنید</string>
|
||||
<string name="home_support_title">از ما حمایت کنید</string>
|
||||
<string name="home_support_content">KernelSU رایگان است و همیشه خواهد بود و منبع باز است. با این حال، می توانید با اهدای کمک مالی به ما نشان دهید که برایتان مهم است.</string>
|
||||
<string name="profile">پروفایل برنامه</string>
|
||||
<string name="profile_default">پیشفرض</string>
|
||||
<string name="profile_template">قالب</string>
|
||||
<string name="profile_custom">شخصی سازی شده</string>
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
<string name="home_click_to_learn_kernelsu">Pelajari cara instal KernelSU dan menggunakan modul</string>
|
||||
<string name="home_support_title">Dukung Kami</string>
|
||||
<string name="home_support_content">KernelSU akan selalu menjadi aplikasi gratis dan terbuka. Anda dapat memberikan donasi sebagai bentuk dukungan.</string>
|
||||
<string name="profile">Profil Apl</string>
|
||||
<string name="profile_default">Bawaan</string>
|
||||
<string name="profile_template">Templat</string>
|
||||
<string name="profile_custom">Khusus</string>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user