├── app
├── .gitignore
├── src
│ └── main
│ │ ├── assets
│ │ └── xposed_init
│ │ ├── res
│ │ ├── drawable
│ │ │ ├── arrow.png
│ │ │ ├── arrow_icon.xml
│ │ │ └── ic_launcher_background.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── values
│ │ │ ├── themes.xml
│ │ │ ├── dimens.xml
│ │ │ ├── colors.xml
│ │ │ └── strings.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── xml
│ │ │ ├── backup_rules.xml
│ │ │ └── data_extraction_rules.xml
│ │ ├── values-zh-rCN
│ │ │ └── strings.xml
│ │ └── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ └── moe
│ │ │ └── lyniko
│ │ │ └── replacecursor
│ │ │ ├── ui
│ │ │ ├── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Type.kt
│ │ │ │ └── Theme.kt
│ │ │ ├── SettingsView.kt
│ │ │ ├── AboutView.kt
│ │ │ ├── AppNavHost.kt
│ │ │ └── HomeView.kt
│ │ │ ├── MyApplication.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── MainHook.kt
│ │ │ └── utils
│ │ │ └── PreferenceUtils.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── magisk
├── META-INF
│ └── com
│ │ └── google
│ │ └── android
│ │ ├── updater-script
│ │ └── update-binary
├── module.prop
├── system
│ └── vendor
│ │ └── overlay
│ │ └── allusive_rro_sign.apk
├── customize.sh
└── README.md
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── settings.gradle
├── .gitignore
├── LICENSE
├── .github
└── workflows
│ └── build.yml
├── gradle.properties
├── gradlew.bat
├── README.md
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/magisk/META-INF/com/google/android/updater-script:
--------------------------------------------------------------------------------
1 | #MAGISK
2 |
--------------------------------------------------------------------------------
/app/src/main/assets/xposed_init:
--------------------------------------------------------------------------------
1 | moe.lyniko.replacecursor.MainHook
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/app/src/main/res/drawable/arrow.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/magisk/module.prop:
--------------------------------------------------------------------------------
1 | id=replace_cursor
2 | name=Replace Cursor
3 | version=v3
4 | versionCode=3
5 | author=LY
6 | description=Current cursor theme: Mahiro
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/magisk/system/vendor/overlay/allusive_rro_sign.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/magisk/system/vendor/overlay/allusive_rro_sign.apk
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Young-Lord/replaceCursor/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/magisk/customize.sh:
--------------------------------------------------------------------------------
1 | set_perm_recursive "$MODPATH/system/vendor/overlay" 0 0 0755 0644 u:object_r:vendor_overlay_file:s0
2 | chown 0:2000 "$MODPATH/system/vendor/overlay"
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/arrow_icon.xml:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Dec 16 19:53:24 GMT+08:00 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/magisk/README.md:
--------------------------------------------------------------------------------
1 | Magisk module for customizing cursors.
2 |
3 | Original file from . Modified by LY at
4 |
5 | Current cursor theme from: [【别当欧尼酱了】真寻自制像素鼠标指针](https://www.bilibili.com/video/BV1FT41127t8)
6 |
--------------------------------------------------------------------------------
/app/src/main/java/moe/lyniko/replacecursor/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package moe.lyniko.replacecursor.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven { url "https://api.xposed.info/" }
14 | }
15 | }
16 | rootProject.name = "Hide Recent Task"
17 | include ':app'
18 |
--------------------------------------------------------------------------------
/app/src/main/java/moe/lyniko/replacecursor/MyApplication.kt:
--------------------------------------------------------------------------------
1 | package moe.lyniko.replacecursor
2 |
3 | import android.app.Application
4 | import android.content.res.Resources
5 |
6 | // https://stackoverflow.com/a/54686443/22911792
7 | class MyApplication : Application() {
8 | companion object {
9 | lateinit var instance: Application
10 | lateinit var resourcesPublic: Resources
11 | }
12 |
13 | override fun onCreate() {
14 | super.onCreate()
15 | instance = this
16 | resourcesPublic = resources
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 更改指针图标
4 | 自定义包括鼠标指针、触控点在内的各种图片资源
5 | 主页
6 | 设置
7 | 关于
8 | 请先在 LSPosed 中激活并强制停止本模块。
9 | 选择图片
10 | Resource ID
11 | 添加
12 |
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle files
2 | .gradle/
3 | build/
4 |
5 | # Local configuration file (sdk path, etc)
6 | local.properties
7 |
8 | # Log/OS Files
9 | *.log
10 |
11 | # Android Studio generated files and folders
12 | captures/
13 | .externalNativeBuild/
14 | .cxx/
15 | *.apk
16 | output.json
17 |
18 | # IntelliJ
19 | *.iml
20 | .idea/
21 | misc.xml
22 | deploymentTargetDropDown.xml
23 | render.experimental.xml
24 |
25 | # Keystore files
26 | *.jks
27 | *.keystore
28 |
29 | # Google Services (e.g. APIs or Firebase)
30 | google-services.json
31 |
32 | # Android Profiling
33 | *.hprof
34 |
35 | !/magisk/system/vendor/overlay/*.apk
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/magisk/META-INF/com/google/android/update-binary:
--------------------------------------------------------------------------------
1 | #!/sbin/sh
2 |
3 | #################
4 | # Initialization
5 | #################
6 |
7 | umask 022
8 |
9 | # echo before loading util_functions
10 | ui_print() { echo "$1"; }
11 |
12 | require_new_magisk() {
13 | ui_print "*******************************"
14 | ui_print " Please install Magisk v20.4+! "
15 | ui_print "*******************************"
16 | exit 1
17 | }
18 |
19 | #########################
20 | # Load util_functions.sh
21 | #########################
22 |
23 | OUTFD=$2
24 | ZIPFILE=$3
25 |
26 | mount /data 2>/dev/null
27 |
28 | [ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
29 | . /data/adb/magisk/util_functions.sh
30 | [ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
31 |
32 | install_module
33 | exit 0
34 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | https://github.com/Young-Lord/replaceCursor
3 | Replace Cursor
4 | Replace mouse cursor / touch indicator with a custom one
5 |
6 |
7 | - android
8 |
9 | Please activate this module in LSPosed Manager, then force stop.
10 | Home
11 | Settings
12 | About
13 | Select image
14 | Resource ID
15 | Add
16 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 | -keep class moe.lyniko.replacecursor.MainHook
23 | # -keep class rikka.shizuku.SystemServiceHelper
24 | # -keep class android.** { *; }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 LY
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/app/src/main/java/moe/lyniko/replacecursor/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package moe.lyniko.replacecursor.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | paths-ignore:
7 | - '.github/ISSUE_TEMPLATE/**'
8 | - '.github/workflows/issue.yml'
9 | - 'magisk/**'
10 | - '**.md'
11 | workflow_dispatch:
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout the code
18 | uses: actions/checkout@v3
19 |
20 | - name: Set up JDK
21 | uses: actions/setup-java@v3
22 | with:
23 | java-version: '17'
24 | distribution: 'temurin'
25 |
26 | - name: Cache gradle
27 | uses: actions/cache@v3
28 | with:
29 | path: |
30 | ~/.gradle/caches
31 | ~/.gradle/wrapper
32 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
33 | restore-keys: ${{ runner.os }}-gradle-
34 |
35 | - name: Build the app
36 | run: |
37 | if [ ! -f "gradlew" ]; then gradle wrapper; fi
38 | chmod +x gradlew
39 | ./gradlew assemble --stacktrace
40 |
41 | - name: Upload APK
42 | uses: actions/upload-artifact@v4
43 | with:
44 | name: my-build-apk
45 | path: app/build/outputs/apk
46 |
47 | - name: Upload mapping
48 | uses: actions/upload-artifact@v4
49 | with:
50 | name: mapping
51 | path: app/build/outputs/mapping
52 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/src/main/java/moe/lyniko/replacecursor/ui/SettingsView.kt:
--------------------------------------------------------------------------------
1 | package moe.lyniko.replacecursor.ui
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.platform.LocalContext
9 | import me.zhanghai.compose.preference.ProvidePreferenceLocals
10 | import me.zhanghai.compose.preference.getPreferenceFlow
11 | import me.zhanghai.compose.preference.switchPreference
12 | import moe.lyniko.replacecursor.R
13 | import moe.lyniko.replacecursor.utils.PreferenceUtils
14 | import moe.lyniko.replacecursor.utils.PreferenceUtils.Companion.ConfigKeys
15 |
16 |
17 | @Composable
18 | fun SettingsView() {
19 | val context = LocalContext.current
20 | val managerPref = PreferenceUtils.getInstance(context).managerPref
21 | ProvidePreferenceLocals(
22 | flow = managerPref.getPreferenceFlow()
23 | ) {
24 | Text(text = "Currently empty.")
25 | // LazyColumn(modifier = Modifier.fillMaxSize()) {
26 | // switchPreference(
27 | // key=ConfigKeys.HideNoActivityPackages.key,
28 | // defaultValue = ConfigKeys.HideNoActivityPackages.default,
29 | // title = { Text(context.getString(R.string.hide_no_activity_packages)) },
30 | // summary = { Text(context.getString(R.string.hide_no_activity_packages_summary)) },
31 | // )
32 | // }
33 | }
34 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
7 |
8 |
19 |
22 |
25 |
28 |
31 |
34 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'moe.lyniko.replacecursor'
8 | compileSdk 34
9 |
10 | defaultConfig {
11 | applicationId "moe.lyniko.replacecursor"
12 | minSdk 29
13 | targetSdk 34
14 | versionCode 204
15 | versionName "2.0.4"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | zipAlignEnabled true
21 | minifyEnabled true
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | signingConfig signingConfigs.debug
24 | }
25 | debug {
26 | applicationIdSuffix '.debug'
27 | versionNameSuffix '-debug'
28 | }
29 | }
30 | compileOptions {
31 | sourceCompatibility JavaVersion.VERSION_1_8
32 | targetCompatibility JavaVersion.VERSION_1_8
33 | }
34 | kotlinOptions {
35 | jvmTarget = '1.8'
36 | }
37 | buildFeatures {
38 | buildConfig true
39 | compose true
40 | }
41 | composeOptions {
42 | kotlinCompilerExtensionVersion '1.4.3'
43 | }
44 | packagingOptions {
45 | resources {
46 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
47 | }
48 | }
49 | }
50 |
51 | dependencies {
52 | // module stuff
53 | compileOnly 'de.robv.android.xposed:api:82'
54 | implementation 'com.google.code.gson:gson:2.8.9'
55 | implementation 'me.zhanghai.compose.preference:library:1.0.0'
56 |
57 | // ui stuff
58 | implementation "com.google.accompanist:accompanist-drawablepainter:0.28.0"
59 | implementation "androidx.navigation:navigation-compose:2.7.6"
60 | implementation 'androidx.core:core-ktx:1.12.0'
61 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
62 | implementation 'androidx.activity:activity-compose:1.8.2'
63 | implementation platform('androidx.compose:compose-bom:2022.10.00')
64 | implementation 'androidx.compose.ui:ui'
65 | implementation 'androidx.compose.ui:ui-graphics'
66 | implementation 'androidx.compose.ui:ui-tooling-preview'
67 | implementation 'androidx.compose.material3:material3'
68 |
69 |
70 | debugImplementation 'androidx.compose.ui:ui-tooling'
71 | debugImplementation 'androidx.compose.ui:ui-test-manifest'
72 | }
73 |
--------------------------------------------------------------------------------
/app/src/main/java/moe/lyniko/replacecursor/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package moe.lyniko.replacecursor
2 |
3 | // https://stackoverflow.com/a/63877349
4 | // https://stackoverflow.com/a/1109108
5 | import android.os.Bundle
6 | import android.widget.Toast
7 | import androidx.activity.ComponentActivity
8 | import androidx.activity.compose.setContent
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material3.Scaffold
11 | import androidx.compose.material3.SnackbarHost
12 | import androidx.compose.material3.SnackbarHostState
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.runtime.rememberCoroutineScope
15 | import androidx.compose.ui.Modifier
16 | import androidx.navigation.compose.rememberNavController
17 | import moe.lyniko.replacecursor.ui.AppNavHost
18 | import moe.lyniko.replacecursor.ui.BottomNavigation
19 | import moe.lyniko.replacecursor.ui.theme.MyApplicationTheme
20 | import moe.lyniko.replacecursor.utils.PreferenceUtils
21 |
22 |
23 | class MainActivity : ComponentActivity() {
24 | private var snackbarHostState = SnackbarHostState()
25 |
26 | override fun onCreate(savedInstanceState: Bundle?) {
27 | super.onCreate(savedInstanceState)
28 | try {
29 | PreferenceUtils.getInstance(this)
30 | } catch (e: SecurityException) {
31 | e.printStackTrace()
32 | Toast.makeText(this, getString(R.string.not_activated), Toast.LENGTH_LONG).show()
33 | finish()
34 | return
35 | }
36 | setContent {
37 | MyApplicationTheme {
38 |
39 | val scope = rememberCoroutineScope()
40 | val snackbarHostStateRemember = remember { snackbarHostState }
41 | val navController = rememberNavController()
42 |
43 | Scaffold(
44 | snackbarHost = {
45 | SnackbarHost(hostState = snackbarHostStateRemember)
46 | },
47 | bottomBar = {
48 | BottomNavigation(navController = navController)
49 | }
50 | ) { innerPadding ->
51 | AppNavHost(
52 | navController = navController,
53 | modifier = Modifier.padding(innerPadding)
54 | )
55 | }
56 | }
57 | }
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/moe/lyniko/replacecursor/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package moe.lyniko.replacecursor.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 |
18 | private val DarkColorScheme = darkColorScheme(
19 | primary = Purple80,
20 | secondary = PurpleGrey80,
21 | tertiary = Pink80
22 | )
23 |
24 | private val LightColorScheme = lightColorScheme(
25 | primary = Purple40,
26 | secondary = PurpleGrey40,
27 | tertiary = Pink40
28 |
29 | /* Other default colors to override
30 | background = Color(0xFFFFFBFE),
31 | surface = Color(0xFFFFFBFE),
32 | onPrimary = Color.White,
33 | onSecondary = Color.White,
34 | onTertiary = Color.White,
35 | onBackground = Color(0xFF1C1B1F),
36 | onSurface = Color(0xFF1C1B1F),
37 | */
38 | )
39 |
40 | @Composable
41 | fun MyApplicationTheme(
42 | darkTheme: Boolean = isSystemInDarkTheme(),
43 | // Dynamic color is available on Android 12+
44 | dynamicColor: Boolean = true,
45 | content: @Composable () -> Unit
46 | ) {
47 | val colorScheme = when {
48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
49 | val context = LocalContext.current
50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
51 | }
52 |
53 | darkTheme -> DarkColorScheme
54 | else -> LightColorScheme
55 | }
56 | val view = LocalView.current
57 | if (!view.isInEditMode) {
58 | SideEffect {
59 | val window = (view.context as Activity).window
60 | window.statusBarColor = colorScheme.primary.toArgb()
61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
62 | }
63 | }
64 |
65 | MaterialTheme(
66 | colorScheme = colorScheme,
67 | typography = Typography,
68 | content = content
69 | )
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/moe/lyniko/replacecursor/ui/AboutView.kt:
--------------------------------------------------------------------------------
1 | package moe.lyniko.replacecursor.ui
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.lazy.LazyColumn
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Surface
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.platform.LocalContext
17 | import androidx.compose.ui.text.style.TextAlign
18 | import androidx.compose.ui.unit.dp
19 | import moe.lyniko.replacecursor.R
20 |
21 |
22 | @Composable
23 | fun AboutView() {
24 | // center a clickable to open project URL
25 | LazyColumn(modifier = Modifier.fillMaxSize()) {
26 | item {
27 | // card
28 | val context = LocalContext.current
29 | Surface(
30 | shape = MaterialTheme.shapes.medium, // 使用 MaterialTheme 自带的形状
31 | shadowElevation = 5.dp,
32 | modifier = Modifier
33 | .padding(all = 8.dp)
34 | .fillMaxWidth(),
35 | onClick = {
36 | // open project URL
37 | val browserIntent =
38 | Intent(
39 | Intent.ACTION_VIEW,
40 | Uri.parse(context.getString(R.string.project_url))
41 | )
42 | context.startActivity(browserIntent)
43 | }
44 | ) {
45 | Column {
46 | Row(
47 | modifier = Modifier.padding(all = 8.dp)
48 | ) {
49 | // url notice
50 | Text(
51 | text = "URL",
52 | textAlign = TextAlign.Center
53 | )
54 | }
55 | Row(
56 | modifier = Modifier.padding(all = 8.dp)
57 | ) {
58 | // url
59 | Text(
60 | text = LocalContext.current.getString(R.string.project_url),
61 | textAlign = TextAlign.Center
62 | )
63 | }
64 | }
65 | }
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/java/moe/lyniko/replacecursor/ui/AppNavHost.kt:
--------------------------------------------------------------------------------
1 | package moe.lyniko.replacecursor.ui
2 |
3 | import androidx.compose.material.icons.Icons
4 | import androidx.compose.material.icons.filled.Home
5 | import androidx.compose.material.icons.filled.Info
6 | import androidx.compose.material.icons.filled.Settings
7 | import androidx.compose.material3.Icon
8 | import androidx.compose.material3.NavigationBar
9 | import androidx.compose.material3.NavigationBarItem
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.vector.ImageVector
15 | import androidx.compose.ui.platform.LocalContext
16 | import androidx.navigation.NavDestination.Companion.hierarchy
17 | import androidx.navigation.NavGraph.Companion.findStartDestination
18 | import androidx.navigation.NavHostController
19 | import androidx.navigation.compose.NavHost
20 | import androidx.navigation.compose.composable
21 | import androidx.navigation.compose.currentBackStackEntryAsState
22 | import moe.lyniko.replacecursor.R
23 |
24 | sealed class NavigationItem(val route: String, val icon: ImageVector, val title: Int) {
25 | object Home : NavigationItem("Home", Icons.Filled.Home, title = R.string.title_home)
26 | object Settings :
27 | NavigationItem("Settings", Icons.Filled.Settings, title = R.string.title_settings)
28 |
29 | object About : NavigationItem("About", Icons.Filled.Info, title = R.string.title_about)
30 | }
31 |
32 | @Composable
33 | fun BottomNavigation(
34 | navController: NavHostController
35 | ) {
36 | val items = listOf(
37 | NavigationItem.Home,
38 | // NavigationItem.Settings,
39 | NavigationItem.About,
40 | )
41 |
42 | NavigationBar {
43 | val navBackStackEntry by navController.currentBackStackEntryAsState()
44 | val currentDestination = navBackStackEntry?.destination
45 | items.forEach { item ->
46 | // Log.w("BottomNavigation", "item: $item; currentDestination: $currentDestination")
47 | NavigationBarItem(
48 | // Text that shows bellow the icon
49 | label = {
50 | Text(text = LocalContext.current.getString(item.title))
51 | },
52 | // The icon resource
53 | icon = {
54 | Icon(
55 | item.icon,
56 | contentDescription = null
57 | )
58 | },
59 | selected = currentDestination?.hierarchy?.any { it.route == item.route } == true,
60 | onClick = {
61 | navController.navigate(item.route) {
62 | // https://medium.com/@KaushalVasava/navigation-in-jetpack-compose-full-guide-beginner-to-advanced-950c1133740
63 | // Pop up to the start destination of the graph to
64 | // avoid building up a large stack of destinations
65 | // on the back stack as users select items
66 | popUpTo(navController.graph.findStartDestination().id) {
67 | saveState = true
68 | }
69 | // Avoid multiple copies of the same destination when
70 | // re-selecting the same item
71 | launchSingleTop = true
72 | // Restore state when re-selecting a previously selected item
73 | restoreState = true
74 | }
75 | },
76 | )
77 | }
78 | }
79 | }
80 |
81 | @Composable
82 | fun AppNavHost(
83 | modifier: Modifier = Modifier,
84 | navController: NavHostController,
85 | startDestination: String = NavigationItem.Home.route,
86 | ) {
87 | NavHost(
88 | modifier = modifier,
89 | navController = navController,
90 | startDestination = startDestination
91 | ) {
92 | composable(NavigationItem.Home.route) {
93 | HomeView()
94 | }
95 | composable(NavigationItem.Settings.route) {
96 | SettingsView()
97 | }
98 | composable(NavigationItem.About.route) {
99 | AboutView()
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/main/java/moe/lyniko/replacecursor/MainHook.kt:
--------------------------------------------------------------------------------
1 | package moe.lyniko.replacecursor
2 |
3 | import android.content.res.XResources
4 | import android.graphics.drawable.Drawable
5 | import de.robv.android.xposed.IXposedHookZygoteInit
6 | import de.robv.android.xposed.IXposedHookZygoteInit.StartupParam
7 | import de.robv.android.xposed.XSharedPreferences
8 | import de.robv.android.xposed.XposedBridge
9 | import moe.lyniko.replacecursor.utils.PreferenceUtils
10 | import moe.lyniko.replacecursor.utils.ResourceHookEntry
11 |
12 |
13 | class MainHook : IXposedHookZygoteInit {
14 | private var hooks: List
15 | private var xsp: XSharedPreferences =
16 | XSharedPreferences(BuildConfig.APPLICATION_ID, PreferenceUtils.functionalConfigName)
17 |
18 | override fun initZygote(param: StartupParam) {
19 | hooks.forEach { hook ->
20 | if(!hook.enabled) return@forEach
21 | try {
22 | XposedBridge.log("ReplaceCursor - Setting ${hook.imageFile} for ${hook.resourceId}")
23 | val imageBinary = PreferenceUtils.getImageBinaryFrom(xsp, hook.imageFile)
24 | val drawable = Drawable.createFromStream(imageBinary.inputStream(), null)!!
25 | XResources.setSystemWideReplacement(
26 | "android",
27 | "drawable",
28 | hook.resourceId,
29 | // get drawable from filePath
30 | object : XResources.DrawableLoader() {
31 | override fun newDrawable(xModuleResources: XResources, s: Int): Drawable {
32 | /*val fileNameLastPart = File(hook.imageFile).name
33 | val resolver: ContentResolver =
34 | AndroidAppHelper.currentApplication().contentResolver
35 | val uri = Uri.parse("content://moe.lyniko.replacecursor.ImageProvider/${fileNameLastPart}")
36 | val cursor = resolver.query(uri, null, null, null, null)!!
37 | cursor.moveToFirst()
38 | val imageBinary = cursor.getBlob(0)
39 | cursor.close()
40 | val drawable = Drawable.createFromStream(imageBinary.inputStream(), null)*/
41 | // XposedBridge.log("ReplaceCursor - drawable: $drawable")
42 | return drawable
43 | }
44 | }
45 | )
46 | /*
47 | Change cursor hotspot, not working.
48 | val xml = """
49 |
53 | """.trimIndent()
54 | val drawableIcon = xmlStringToDrawable(xml)
55 | XResources.setSystemWideReplacement(
56 | "android",
57 | "drawable",
58 | hook.resourceId+"_icon",
59 | object : XResources.DrawableLoader() {
60 | override fun newDrawable(xModuleResources: XResources, s: Int): Drawable {
61 | XposedBridge.log("ReplaceCursor - drawableIcon for ${hook.resourceId}: $drawableIcon")
62 | return drawableIcon
63 | }
64 | }
65 | )
66 | */
67 | }
68 | catch (e: Exception) {
69 | e.printStackTrace()
70 | }
71 | }
72 | /* Static Way:
73 | val MODULE_PATH = param.modulePath;
74 | val modRes = XModuleResources.createInstance(MODULE_PATH, null)
75 |
76 | XResources.setSystemWideReplacement("android", "drawable", "pointer_arrow",
77 | modRes.fwd(R.drawable.arrow)
78 | )
79 | */
80 | }
81 |
82 | /*
83 | private fun xmlStringToDrawable(yourString: String): Drawable{
84 | val parser = Xml.newPullParser()
85 | parser.setInput(StringReader(yourString))
86 | return Drawable.createFromXml(MyApplication.resourcesPublic, parser)
87 | }
88 | */
89 |
90 | init {
91 | xsp.makeWorldReadable()
92 | hooks = PreferenceUtils.getResourceHooksFrom(xsp)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Replace Cursor
2 |
3 | Replace mouse cursor with a custom one.
4 |
5 | 自定义包括鼠标指针、触控点在内的各种图片资源。
6 |
7 | Note: You can use Magisk + [RRO](https://source.android.com/docs/core/runtime/rros) for better experience.
8 | See [`magisk` folder](https://github.com/Young-Lord/replaceCursor/tree/master/magisk) or releases for more information.
9 |
10 | 
11 |
12 | ## How to use / 用法
13 |
14 | > Tested on: Android 10 (AOSP), Android 12 (MIUI 13), Android 13 (MIUI 14)
15 |
16 | 1. Select `System framework` (package name may be `android` or `system` or empty, [see this](https://github.com/LSPosed/LSPosed/releases/tag/v1.9.1)) in module scope and activate the module
17 | 2. Force stop module
18 | 3. Add resources to change. Please make sure that image sizes are bigger than hotspot (cursor left-top corner / touch point), otherwise nothing will show.
19 | 4. Reboot (you MUST reboot when you modify anything, or changes will not be applied until next reboot)
20 | 5. Reverse engineer `/system/framework/framework-res.apk` to find out the resource ID of the cursor you want to replace.
21 |
22 | For MiPad users, install [MaxMiPad](https://github.com/Xposed-Modules-Repo/com.yifeplayte.maxmipadinput/releases/latest) and enable `No Magic Pointer`.
23 |
24 | If you have Windows-style `.ani` cursors, you can use [ani2png](https://github.com/Mastermindzh/Scripts/blob/master/c%2B%2B/ani2png.c) to convert them.
25 | For example, use `ls -a *.ani | xargs -L1 ani2png` to convert all `.ani` files in current directory to `.png` files.
26 |
27 | Looking for equivalent on **Linux**? Check [Image2Cursors](https://github.com/Young-Lord/Image2Cursors)!
28 |
29 | ## Common resources / 常用资源
30 |
31 | From MIUI 13, Android 12.
32 |
33 | | Resource ID | Description | HotSpot |
34 | |--------------------|---------------------------------------------------------------------------------|----------|
35 | | pointer_spot_touch | Touch point | (22, 22) |
36 | | pointer_arrow | Mouse Pointer (Arrow) | (5, 5) |
37 | | pointer_hand | Mouse Pointer (Hand, for example when hover on sth. clickable) | (9, 4) |
38 | | pointer_text | Mouse Pointer (Looks like |, for editing vertical text input) | (12, 12) |
39 |
40 | Please note that the images may be scaled. See [Android 加载 drawable 中图片后自动缩放的原理](https://juejin.cn/post/6844903914022633480) and [Android drawable微技巧,你所不知道的drawable的那些细节](https://blog.csdn.net/guolin_blog/article/details/50727753#:~:text=%E5%8F%AF%E4%BB%A5%E7%9C%8B%E5%88%B0%EF%BC%8C%E6%AF%8F%E4%B8%80%E7%A7%8D%E5%AF%86%E5%BA%A6%E7%9A%84dpi%E8%8C%83%E5%9B%B4%E9%83%BD%E6%9C%89%E4%B8%80%E4%B8%AA%E6%9C%80%E5%A4%A7%E5%80%BC%EF%BC%8C%E8%BF%99%E4%B8%AA%E6%9C%80%E5%A4%A7%E5%80%BC%E4%B9%8B%E9%97%B4%E7%9A%84%E6%AF%94%E4%BE%8B%E5%B0%B1%E6%98%AF%E5%9B%BE%E7%89%87%E4%BC%9A%E8%A2%AB%E7%B3%BB%E7%BB%9F%E8%87%AA%E5%8A%A8%E6%94%BE%E5%A4%A7%E7%9A%84%E6%AF%94%E4%BE%8B%E3%80%82) and [getResources().getDisplayMetrics().density 的理解](https://blog.csdn.net/lgzaaron/article/details/52517941) for more informaton. In my case, `360x360` on a `xhdpi` device becomes `135x135` (`360 * (240 / 320) / 2 = 135`, not sure). You may need to manually adjust size & padding (border) to fit size & hotspot.
41 |
42 | Mouse-related resource-id may have a `_large` suffix, used when `Accessibility` -> `Large mouse pointer`(`大号鼠标指针`) is enabled.
43 |
44 | ## Module Scope
45 |
46 | - android
47 |
48 | ## Project URL
49 |
50 | Home URL:
51 |
52 | Xposed Modules Repo URL:
53 |
54 | ## License
55 |
56 | Apache-2.0 License or MIT License are all OK.
57 |
58 | ## Thanks
59 |
60 | - (doesn't work for me)
61 | - (per-app configuration)
62 | - (convert `.cur` and `.ani` to Xcursor format, for Linux)
63 | - (convert Xcursor format to `.png`, see pull requests for a better version)
64 | - [将Windows上的鼠标指针主题移植到Linux上](https://blog.csdn.net/qq_41172785/article/details/89063164) (convert, edit, install Windows cursors to Linux)
65 |
66 | ## TODO
67 |
68 | Currently, this module use SharedPreferences to store images, which is really shitty. Anyone is free to improve this module.
69 |
70 | Also, I am too low to modify hotspot using Xposed. If you know how to do this, please tell me.
71 |
72 | Also: Origin image resolution, disable scaling.
73 |
--------------------------------------------------------------------------------
/app/src/main/java/moe/lyniko/replacecursor/utils/PreferenceUtils.kt:
--------------------------------------------------------------------------------
1 | package moe.lyniko.replacecursor.utils
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.content.SharedPreferences
6 | import android.util.Base64
7 | import android.util.Log
8 |
9 | class ResourceHookEntry(resource_id: String, image_file: String, var enabled: Boolean,
10 | var version: Int
11 | ) {
12 | var resourceId: String = resource_id
13 | var imageFile: String = image_file
14 |
15 | fun toPerfString(): String {
16 | return "$resourceId$delimiter$imageFile$delimiter$enabled$delimiter$version"
17 | }
18 | companion object {
19 | const val delimiter = ":"
20 | fun fromPerfString(perfString: String): ResourceHookEntry {
21 | val split = perfString.split(delimiter, limit = 4)
22 | return ResourceHookEntry(split[0], split[1], split[2].toBoolean(), split[3].toInt())
23 | }
24 | }
25 | }
26 |
27 | @SuppressLint("WorldReadableFiles")
28 | class PreferenceUtils( // init context on constructor
29 | context: Context
30 | ) {
31 | // ------ 1. get several SharedPreferences (funcPref is the only accessible during Xposed inject) ------
32 | private var funcPref: SharedPreferences = try {
33 | @Suppress("DEPRECATION")
34 | context.getSharedPreferences(functionalConfigName, Context.MODE_WORLD_READABLE)
35 | } catch (e: SecurityException) {
36 | throw e
37 | // Log.w("PreferenceUtil", "Fallback to Private SharedPref for error!!!: ${e.message}")
38 | // context.getSharedPreferences(functionalConfigName, Context.MODE_PRIVATE)
39 | }
40 |
41 | var managerPref: SharedPreferences =
42 | context.getSharedPreferences(managerConfigName, Context.MODE_PRIVATE)
43 | var resourceHooks: List
44 |
45 | companion object {
46 | @Volatile
47 | private var instance: PreferenceUtils? = null
48 |
49 | fun getInstance(context: Context) =
50 | instance ?: synchronized(this) {
51 | instance ?: PreferenceUtils(context).also { instance = it }
52 | }
53 |
54 | const val functionalConfigName = "functional_config"
55 | const val managerConfigName = "manager_config"
56 | const val resourceHookConfigName = "resource_hook_config"
57 | const val imageBinaryPrefix = "image_binary_"
58 |
59 | enum class ConfigKeys(val key: String, val default: Boolean) {
60 | // HideNoActivityPackages("hide_no_activity_packages", true)
61 | }
62 |
63 | fun getResourceHooksFrom(preferences: SharedPreferences): List {
64 | val resourceHooks =
65 | preferences.getStringSet(resourceHookConfigName, null) ?: return listOf()
66 | return resourceHooks.map { ResourceHookEntry.fromPerfString(it) }
67 | }
68 |
69 | fun getImageBinaryFrom(preferences: SharedPreferences, filename: String): ByteArray {
70 | val base64 = preferences.getString(imageBinaryPrefix + filename, null)!!
71 | return Base64.decode(base64, Base64.NO_WRAP)
72 | }
73 | }
74 |
75 | // ------ 2. get/set several SharedPreferences ------
76 | private fun initResourceHooks(): List {
77 | return getResourceHooksFrom(funcPref)
78 | }
79 |
80 | init {
81 | resourceHooks = initResourceHooks()
82 | }
83 |
84 | private fun saveResourceHooks() {
85 | // Log.e("PreferenceUtils", "saveResourceHooks: $resourceHooks")
86 | val resourceHooksString = resourceHooks.map { it.toPerfString() }.toSet()
87 | funcPref.edit().putStringSet(resourceHookConfigName, resourceHooksString).apply()
88 | }
89 |
90 | fun addResourceHook(resourceHook: ResourceHookEntry) {
91 | // Log.e("PreferenceUtils", "addResourceHook: $resourceHook")
92 | resourceHooks = resourceHooks + resourceHook
93 | saveResourceHooks()
94 | }
95 |
96 | fun removeResourceHook(resourceHook: ResourceHookEntry, keepImage: Boolean = false) {
97 | // Log.e("PreferenceUtils", "removeResourceHook: $resourceHook")
98 | if (!keepImage) removeImageBinary(resourceHook.imageFile)
99 | resourceHooks = resourceHooks - resourceHook
100 | saveResourceHooks()
101 | }
102 |
103 | private fun getResourceHook(resourceId: String): ResourceHookEntry? {
104 | return resourceHooks.find { it.resourceId == resourceId }
105 | }
106 |
107 | fun updateResourceHook(resourceHook: ResourceHookEntry) {
108 | // Log.e("PreferenceUtils", "updateResourceHook: $resourceHook")
109 | val oldResourceHook = getResourceHook(resourceHook.resourceId)
110 | if (oldResourceHook != null) {
111 | removeResourceHook(oldResourceHook, keepImage = true)
112 | }
113 | addResourceHook(resourceHook)
114 | }
115 | @SuppressLint("ApplySharedPref")
116 | fun setImageBinary(filename: String, data: ByteArray) {
117 | // Log.e("PreferenceUtils", "setImageBinary: $filename")
118 | funcPref.edit().putString(imageBinaryPrefix+filename, Base64.encodeToString(data, Base64.NO_WRAP)).apply()
119 | }
120 | fun getImageBinary(filename: String): ByteArray {
121 | // Log.e("PreferenceUtils", "getImageBinary: $filename")
122 | return getImageBinaryFrom(funcPref, filename)
123 | }
124 | fun removeImageBinary(filename: String) {
125 | // Log.e("PreferenceUtils", "removeImageBinary: $filename")
126 | funcPref.edit().remove(imageBinaryPrefix+filename).apply()
127 | }
128 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/moe/lyniko/replacecursor/ui/HomeView.kt:
--------------------------------------------------------------------------------
1 | package moe.lyniko.replacecursor.ui
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import android.content.Context
6 | import android.content.ContextWrapper
7 | import android.content.Intent
8 | import android.graphics.drawable.Drawable
9 | import android.util.Log
10 | import androidx.activity.ComponentActivity
11 | import androidx.activity.compose.rememberLauncherForActivityResult
12 | import androidx.activity.result.ActivityResult
13 | import androidx.activity.result.contract.ActivityResultContracts
14 | import androidx.compose.foundation.Image
15 | import androidx.compose.foundation.layout.Arrangement
16 | import androidx.compose.foundation.layout.Column
17 | import androidx.compose.foundation.layout.Row
18 | import androidx.compose.foundation.layout.Spacer
19 | import androidx.compose.foundation.layout.fillMaxWidth
20 | import androidx.compose.foundation.layout.padding
21 | import androidx.compose.foundation.layout.size
22 | import androidx.compose.material.icons.Icons
23 | import androidx.compose.material.icons.filled.Clear
24 | import androidx.compose.material3.Button
25 | import androidx.compose.material3.Icon
26 | import androidx.compose.material3.IconButton
27 | import androidx.compose.material3.MaterialTheme
28 | import androidx.compose.material3.Scaffold
29 | import androidx.compose.material3.SnackbarHost
30 | import androidx.compose.material3.SnackbarHostState
31 | import androidx.compose.material3.Switch
32 | import androidx.compose.material3.Text
33 | import androidx.compose.material3.TextField
34 | import androidx.compose.runtime.Composable
35 | import androidx.compose.runtime.MutableState
36 | import androidx.compose.runtime.getValue
37 | import androidx.compose.runtime.key
38 | import androidx.compose.runtime.mutableIntStateOf
39 | import androidx.compose.runtime.mutableStateOf
40 | import androidx.compose.runtime.remember
41 | import androidx.compose.runtime.setValue
42 | import androidx.compose.ui.Alignment
43 | import androidx.compose.ui.Modifier
44 | import androidx.compose.ui.platform.LocalContext
45 | import androidx.compose.ui.unit.dp
46 | import com.google.accompanist.drawablepainter.rememberDrawablePainter
47 | import moe.lyniko.replacecursor.BuildConfig
48 | import moe.lyniko.replacecursor.R
49 | import moe.lyniko.replacecursor.ui.theme.MyApplicationTheme
50 | import moe.lyniko.replacecursor.utils.PreferenceUtils
51 | import moe.lyniko.replacecursor.utils.ResourceHookEntry
52 | import java.lang.NullPointerException
53 |
54 | fun Context.getActivity(): ComponentActivity? = when (this) {
55 | is ComponentActivity -> this
56 | is ContextWrapper -> baseContext.getActivity()
57 | else -> null
58 | }
59 |
60 | fun isResourceIdValid(resourceId: String): Boolean {
61 | return resourceId.matches(Regex("^[a-z0-9_]+$"))
62 | }
63 |
64 | private lateinit var preferenceUtils: PreferenceUtils
65 | private var snackbarHostState = SnackbarHostState()
66 |
67 | @Composable
68 | fun HomeView() {
69 | val context = LocalContext.current
70 | val recomposeEntries: MutableState = remember { mutableIntStateOf(0) }
71 | preferenceUtils = PreferenceUtils.getInstance(context)
72 | MyApplicationTheme {
73 | val snackbarHostStateRemember = remember { snackbarHostState }
74 |
75 | Scaffold(
76 | snackbarHost = {
77 | SnackbarHost(hostState = snackbarHostStateRemember)
78 | },
79 | ) { innerPadding ->
80 | Column(modifier = Modifier.padding(innerPadding)) {
81 | key(recomposeEntries.value){
82 | MainEntries(recomposeEntries)
83 | }
84 | MainSettings(recomposeEntries)
85 | }
86 | }
87 | }
88 | }
89 |
90 | @SuppressLint("SetWorldReadable", "SdCardPath")
91 | @Composable
92 | private fun MainSettings(recomposeEntries: MutableState) {
93 | val context = LocalContext.current
94 | val activity = context.getActivity()!!
95 | var resultFile by remember { mutableStateOf("") }
96 | var resourceId by remember { mutableStateOf("") }
97 | @Suppress("BlockingMethodInNonBlockingContext") val startForResult =
98 | rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
99 | if (result.resultCode == Activity.RESULT_OK) {
100 | val uri = result.data?.data
101 | if (uri != null) {
102 | val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION
103 | activity.contentResolver.takePersistableUriPermission(
104 | uri,
105 | takeFlags
106 | )
107 | // copy to data dir
108 | val inputStream = activity.contentResolver.openInputStream(uri)!!
109 | if(resultFile.isNotEmpty()) preferenceUtils.removeImageBinary(resultFile)
110 | resultFile = "${resourceId}_${System.currentTimeMillis()}"
111 | preferenceUtils.setImageBinary(resultFile, inputStream.readBytes())
112 | /*
113 | // random hardcoded for xpsoed
114 | val resultFileObject = File("/data/data/${BuildConfig.APPLICATION_ID}/files/").resolve(resultFile)
115 | val outputStream = resultFileObject.outputStream()
116 | inputStream!!.copyTo(outputStream)
117 | outputStream.close()
118 | resultFileObject.setReadable(true, false)
119 | resultFile = resultFileObject.absolutePath
120 | */
121 | inputStream.close()
122 | // Log.e("HomeView", "resultFilename: $resultFile")
123 | }
124 | }
125 | }
126 | // a card with resource id input and image file input
127 |
128 | //1. display a card
129 | Column(
130 | modifier = Modifier
131 | .fillMaxWidth()
132 | .padding(8.dp)
133 | ) {
134 | //2. display text field for resource id
135 | TextField(
136 | value = resourceId,
137 | onValueChange = { resourceId = it },
138 | label = { Text(text = LocalContext.current.getString(R.string.resource_id)) },
139 | modifier = Modifier.fillMaxWidth()
140 | )
141 | //3. display Button for image file, use SAF to select
142 | Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
143 | Button(
144 | enabled = isResourceIdValid(resourceId),
145 | onClick = {
146 | // open saf to get image
147 | val intent = activity.let {
148 | Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
149 | addCategory(Intent.CATEGORY_OPENABLE)
150 | type = "image/*"
151 | }
152 | }
153 | startForResult.launch(intent)
154 | }) {
155 | Text(text = LocalContext.current.getString(R.string.select_image))
156 | }
157 | //4. display Button for add
158 | Button(
159 | enabled = isResourceIdValid(resourceId) && resultFile.isNotEmpty(),
160 | onClick = {
161 | // add to preferenceUtils
162 | preferenceUtils.addResourceHook(
163 | ResourceHookEntry(
164 | resourceId,
165 | resultFile,
166 | true,
167 | 0
168 | )
169 | )
170 | // refresh
171 | resourceId = ""
172 | resultFile = ""
173 | recomposeEntries.value += 1
174 | }) {
175 | Text(text = LocalContext.current.getString(R.string.add))
176 | }
177 | }
178 | }
179 | }
180 |
181 | @Composable
182 | private fun MainEntries(recomposeEntries: MutableState) {
183 | val hooks by remember { mutableStateOf(preferenceUtils.resourceHooks) }
184 | hooks.forEach {
185 | SingleEntry(it, recomposeEntries)
186 | }
187 | }
188 |
189 | @Composable
190 | private fun SingleEntry(res: ResourceHookEntry, recomposeEntries: MutableState) {
191 | // val activity = LocalContext.current.getActivity()!!
192 |
193 | // get resource_name, image from preferenceUtils, display them along with a switch
194 | // when switch is toggled, update preferenceUtils
195 |
196 | val imageBinary: ByteArray
197 | try{
198 | imageBinary = preferenceUtils.getImageBinary(res.imageFile)
199 | // Log.e("HomeView", "imageBinary: ${imageBinary.size}")
200 | }
201 | catch (e: NullPointerException) {
202 | e.printStackTrace()
203 | Log.w(BuildConfig.APPLICATION_ID, "image ${res.imageFile} not found, removed")
204 | preferenceUtils.removeResourceHook(res)
205 | return
206 | }
207 |
208 | //1. display a card
209 | Row(modifier=Modifier.fillMaxWidth()){
210 | //2. display image
211 | // Log.e("HomeView", "res.imageFile: ${res.imageFile}")
212 | Image(
213 | painter = rememberDrawablePainter(
214 | drawable = Drawable.createFromStream(
215 | imageBinary.inputStream(),
216 | null
217 | )
218 | ),
219 | contentDescription = null,
220 | modifier = Modifier.size(48.dp)
221 | )
222 | //3. display resource id
223 | Column(
224 | modifier = Modifier.align(Alignment.CenterVertically),
225 | ) {
226 | Text(text = res.resourceId, style = MaterialTheme.typography.titleLarge)
227 | }
228 | Spacer(Modifier.weight(1f))
229 | //4. display switch
230 | var checked by remember { mutableStateOf(res.enabled) }
231 | Switch(checked = checked, onCheckedChange = {
232 | res.enabled = it
233 | preferenceUtils.updateResourceHook(res)
234 | checked = it
235 | })
236 | //5. delete button
237 | IconButton(onClick = {
238 | preferenceUtils.removeResourceHook(res)
239 | recomposeEntries.value += 1
240 | }) {
241 | Icon(
242 | imageVector = Icons.Default.Clear,
243 | contentDescription = null
244 | )
245 | }
246 | /*
247 | Test Content Provider
248 | val resolver = activity.contentResolver
249 | val uri = Uri.parse("content://moe.lyniko.replacecursor.ImageProvider/${File(res.imageFile).name}")
250 | val cursor = resolver.query(uri, null, null, null, null)
251 | if (cursor != null) {
252 | cursor.moveToFirst()
253 | val imageBinary = cursor.getBlob(0)
254 | cursor.close()
255 | Log.e("HomeView", "imageBinary: ${imageBinary.size}")
256 | Text(text = "imageBinary: ${imageBinary.size}")
257 | }
258 | else {
259 | Log.e("HomeView", "cursor is null")
260 | Text(text = "cursor is null")
261 | }
262 | */
263 | }
264 | }
--------------------------------------------------------------------------------