├── library
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ └── cn
│ │ └── lalaki
│ │ └── iconpackmanager
│ │ └── IconPackManager.kt
└── build.gradle.kts
├── app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── anim
│ │ │ └── shake.xml
│ │ ├── layout
│ │ │ └── item.xml
│ │ └── values
│ │ │ └── styles.xml
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ └── cn
│ │ └── lalaki
│ │ └── tinydesk
│ │ └── MainActivity.kt
├── proguard-rules.pro
└── build.gradle.kts
├── video
└── demo.gif
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle.kts
├── gradle.properties
├── gradlew.bat
├── README_zh_cn.md
├── README.md
├── gradlew
└── LICENSE
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /release
--------------------------------------------------------------------------------
/video/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lalakii/IconPackManager/HEAD/video/demo.gif
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lalakii/IconPackManager/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Jan 27 08:36:29 CST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=http\://mirrors.cloud.tencent.com/gradle/gradle-8.8-all.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 | .idea
17 | /.idea
18 | aar-publish
19 | /aar-publish
--------------------------------------------------------------------------------
/app/src/main/res/anim/shake.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | mavenCentral()
5 | google()
6 | maven(url = "https://mirrors.cloud.tencent.com/nexus/repository/maven-public/")
7 | }
8 | }
9 | @Suppress("UnstableApiUsage")
10 | dependencyResolutionManagement {
11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
12 | repositories {
13 | mavenCentral()
14 | google()
15 | maven(url = "https://mirrors.cloud.tencent.com/nexus/repository/maven-public/")
16 | }
17 | }
18 | rootProject.name = "IconPackManager"
19 | include(":app")
20 | include(":library")
21 |
--------------------------------------------------------------------------------
/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=-Xmx1536m -Dfile.encoding=UTF-8
10 | org.gradle.warning.mode=all
11 | android.useAndroidX=true
12 | kotlin.code.style=official
13 | android.nonTransitiveRClass=true
14 | android.suppressUnsupportedCompileSdk=35
15 | android.injected.testOnly=false
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -verbose
2 | -ignorewarnings
3 | -optimizationpasses 7
4 | -dontusemixedcaseclassnames
5 | -overloadaggressively
6 | -flattenpackagehierarchy "unused"
7 | -adaptresourcefilenames
8 | -assumenosideeffects class android.util.Log {
9 | public static boolean isLoggable(java.lang.String, int);
10 | public static *** isLoggable(java.lang.String, ...);
11 | public static int v(...);
12 | public static int i(...);
13 | public static int w(...);
14 | public static int d(...);
15 | public static int e(...);
16 | public static java.lang.String getStackTraceString(java.lang.Throwable);}
17 | -assumenosideeffects class java.io.PrintStream { *;}
18 | #-assumenosideeffects class java.io.PrintWriter { *;}
19 | -assumenosideeffects class java.util.logging.Logger {
20 | public *** log(...);
21 | public *** logp(...);}
22 | -assumenosideeffects class androidx.profileinstaller.**{ *;}
23 | -assumenosideeffects class androidx.view.menu.**{ *;}
24 | -assumenosideeffects class androidx.emoji2.**{ *;}
25 | -assumenosideeffects class androidx.startup.**{ *;}
26 | -assumenosideeffects class androidx.versionedparcelable.**{ *;}
27 | -assumenosideeffects class androidx.activity.OnBackPressedDispatcher{ *;}
28 | -assumenosideeffects class androidx.activity.OnBackPressedDispatcher$LifecycleOnBackPressedCancellable{ *;}
29 | -assumenosideeffects class androidx.activity.ImmLeaksCleaner{ *;}
30 | -dontwarn java.lang.*
--------------------------------------------------------------------------------
/library/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | id("org.jetbrains.kotlin.android")
4 | signing
5 | id("cn.lalaki.publisher") version "1.0.5"
6 | }
7 | android {
8 | namespace = "cn.lalaki.iconpackmanager"
9 | compileSdk = 35
10 | version = 7.0
11 | buildTypes {
12 | named("release") {
13 | isMinifyEnabled = false
14 | isDefault = true
15 | }
16 | }
17 | compileOptions {
18 | sourceCompatibility = JavaVersion.VERSION_17
19 | targetCompatibility = JavaVersion.VERSION_17
20 | }
21 | kotlinOptions.jvmTarget = "17"
22 | buildToolsVersion = "35.0.0"
23 | }
24 | tasks.configureEach {
25 | if (name.contains("AarMetadata", ignoreCase = true)) {
26 | enabled = false
27 | }
28 | }
29 | signing {
30 | useGpgCmd()
31 | }
32 | centralPortal {
33 | name = rootProject.name
34 | group = "cn.lalaki"
35 | username = System.getenv("TEMP_USER")
36 | password = System.getenv("TEMP_PASS")
37 | publishingType = cn.lalaki.pub.internal.BaseCentralPortalExtension.PublishingType.USER_MANAGED
38 | pom {
39 | url = "https://github.com/lalakii/IconPackManager"
40 | description = "Library for loading icon pack resources."
41 | licenses {
42 | license {
43 | name = "The Apache License, Version 2.0"
44 | url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
45 | }
46 | }
47 | developers {
48 | developer {
49 | name = "lalakii"
50 | email = "dazen@189.cn"
51 | organization = "lalaki.cn"
52 | organizationUrl = "https://github.com/lalakii"
53 | }
54 | }
55 | scm {
56 | connection = "scm:git:https://github.com/lalakii/IconPackManager"
57 | developerConnection = "scm:git:https://github.com/lalakii/IconPackManager"
58 | url = "https://github.com/lalakii/IconPackManager"
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/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/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.time.ZonedDateTime
2 | import java.time.format.DateTimeFormatter
3 |
4 | plugins {
5 | id("com.android.application")
6 | id("org.jetbrains.kotlin.android")
7 | id("cn.lalaki.repack") version "3.0.0-LTS"
8 | id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
9 | }
10 | android {
11 | namespace = "cn.lalaki.tinydesk"
12 | compileSdk = 35
13 | defaultConfig {
14 | applicationId = namespace
15 | minSdk = 14
16 | //noinspection ExpiredTargetSdkVersion
17 | targetSdk = 29
18 | versionCode = 5
19 | versionName =
20 | "$versionCode.${
21 | ZonedDateTime.now().toLocalDate().format(DateTimeFormatter.ofPattern("MMdd"))
22 | }"
23 | resourceConfigurations.add("en")
24 | }
25 | signingConfigs {
26 | register("release") {
27 | storeFile = file("D:\\imoe.jks")
28 | storePassword = System.getenv("mystorepass")
29 | keyAlias = "dazen@189.cn"
30 | keyPassword = System.getenv("mystorepass2")
31 | }
32 | }
33 | buildTypes {
34 | release {
35 | isDefault = true
36 | isMinifyEnabled = true
37 | isShrinkResources = true
38 | isDebuggable = false
39 | isJniDebuggable = false
40 | renderscriptOptimLevel = 3
41 | proguardFiles(
42 | getDefaultProguardFile("proguard-android-optimize.txt"),
43 | "proguard-rules.pro",
44 | )
45 | signingConfig = signingConfigs["release"]
46 | }
47 | }
48 | dependenciesInfo {
49 | includeInApk = false
50 | includeInBundle = false
51 | }
52 | compileOptions {
53 | sourceCompatibility = JavaVersion.VERSION_22
54 | targetCompatibility = JavaVersion.VERSION_22
55 | }
56 | kotlinOptions.jvmTarget = "22"
57 | buildToolsVersion = "35.0.0"
58 | }
59 | repackConfig {
60 | resign = true // 对重新打包的apk签名
61 | addV2Sign = false // v2签名,android9以下需要
62 | addV1Sign = true
63 | disableV3V4 = true
64 | blacklist =
65 | arrayOf(
66 | "META-INF",
67 | "kotlin-tooling-metadata.json",
68 | "kotlin",
69 | "unused",
70 | ) // 重新打包时,可以排除某些无用的文件或文件夹,可以为null
71 | quiet = false // 控制台输出日志
72 | }
73 | dependencies {
74 | implementation(project(":library"))
75 | implementation("cn.lalaki:pinyin4j-chinese-simplified:1.0.7")
76 | implementation("androidx.recyclerview:recyclerview:1.4.0-alpha01")
77 | }
78 | tasks.configureEach {
79 | if (arrayOf("aarmetadata", "artprofile", "jni", "native").any {
80 | name.contains(it, ignoreCase = true)
81 | }
82 | ) {
83 | enabled = false
84 | }
85 | }
86 | configurations.all {
87 | exclude("androidx.profileinstaller", "profileinstaller")
88 | exclude("androidx.versionedparcelable", "versionedparcelable")
89 | exclude("androidx.emoji2", "emoji2")
90 | exclude("androidx.appcompat", "appcompat-resources")
91 | }
92 |
--------------------------------------------------------------------------------
/README_zh_cn.md:
--------------------------------------------------------------------------------
1 | # IconPackManager
2 | [](https://central.sonatype.com/artifact/cn.lalaki/IconPackManager/)  
3 |
4 | [ [中文说明](#) | [English](README.md) ]
5 |
6 | **快速加载图标包**
7 |
8 | *用于读取图标包资源的库,支持自定义关键词查找图标,支持图标裁剪、缩放以及色彩饱和度修改。演示app,是一个简易的安卓桌面,可以更加直观的了解这个库的用处。*
9 |
10 | ## 必要条件
11 | + SDK 版本 大于 14
12 | + kotlin
13 | + 测试图标包 [点此搜索](https://www.baidu.com/s?wd=%E5%9B%BE%E6%A0%87%E5%8C%85%20apk%20%E4%B8%8B%E8%BD%BD)
14 |
15 | ## 快速开始
16 |
17 | 1. 导入
18 |
19 | 使用Gradle,或者直接下载AAR [IconPackManager](https://github.com/lalakii/IconPackManager/releases)
20 |
21 | ```kotlin
22 | dependencies {
23 | implementation("cn.lalaki:IconPackManager:7.0")
24 | }
25 | ```
26 |
27 | 2. 代码示例
28 |
29 | ```kotlin
30 | import cn.lalaki.iconpackmanager.IconPackManager
31 |
32 | // IconPackManager
33 | val ipm = IconPackManager(packageManager)
34 |
35 | val iconPacks = ipm.isSupportedIconPacks()
36 |
37 | ipm.isSupportedIconPacks(true)// 强制刷新缓存,如果需要
38 |
39 | iconPacks.forEach {
40 | //……遍历所有图标包
41 | }
42 |
43 | // 为某个图标包添加自定义规则,图标包没有相应图标时,按照规则查找图标包内的图标
44 | val rules = HashMap> //key是包名,可模糊匹配;value是需要匹配的应用图标包名,可模糊匹配,支持多个参数
45 | rules["com.android.chrome"]=arrayOf("browser","...")
46 | rules["com.android.email"]=arrayOf("mail","message")
47 | rules["video"]=arrayOf("tencent","youtube")
48 | iconPackItem.setRules(rules)
49 |
50 | //清除当前图标包的自定义规则
51 | iconPackItem.setRules(null)
52 | ```
53 | ```xml
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | ...
63 | ...
64 | ...
65 |
66 | ```
67 | *ipm.isSupportedIconPacks()是一个list,遍历可获取图标包名称以及相应的工具类*
68 | ```kotlin
69 | // load icon pack
70 | ipm.isSupportedIconPacks().forEach {
71 | /** 根据图标包名称,筛选图标包 **/
72 | // if (it.name != "your icon pack")
73 |
74 | //get icon pack name
75 | val iconPackName = it.name
76 |
77 | //get icon from icon pack;
78 | val launchIntent = getLaunchIntentForPackage(packageName)
79 | val icon = it.loadIcon(launchIntent) //可选Intent or ComponentName or ApplicationInfo
80 |
81 | //如果未找到图标,可选 裁剪原始图标
82 | if(icon == null)
83 | {
84 | //裁剪圆形
85 | it.transformIcon(applicationInfo.loadIcon(packageManager),0.5f,scaleF,saturation)
86 |
87 | //裁剪圆角 需要自己定义一些参数 radius是半径,scaleF是图标缩放,saturation是色彩饱和度。Float可变类型参数,默认值为1f
88 | it.transformIcon(applicationInfo.loadIcon(packageManager),radius,scaleF,saturation)
89 | }
90 | }
91 | ```
92 |
93 | ## 演示
94 |
95 |
96 |
97 | ## 关于
98 |
99 | 为爱发电。
100 |
101 | + feedback:dazen@189.cn
102 |
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IconPackManager
2 | [](https://central.sonatype.com/artifact/cn.lalaki/IconPackManager/)  
3 |
4 | [ [中文说明](README_zh_cn.md) | [English](#) ]
5 |
6 | **Quick load icon pack.**
7 |
8 | *Library for reading icon pack resources, support custom keywords to find icons, support icon cropping, scaling and color saturation modification. Demo app, a simple Android desktop, can be more intuitive to understand the usefulness of this library.*
9 |
10 | ## Prerequisites
11 | + SDK Version >= 14
12 | + kotlin
13 | + install the custom icon pack [Pure Icon Pack](https://apkpure.net/cn/pure-circle-icon-pack/me.morirain.dev.iconpack.pure) or Others you like
14 |
15 | ## Quick Start
16 |
17 | 1. Import AAR
18 |
19 | Gradle or Download [IconPackManager](https://github.com/lalakii/IconPackManager/releases)
20 |
21 | ```kotlin
22 | dependencies {
23 | implementation("cn.lalaki:IconPackManager:7.0")
24 | }
25 | ```
26 |
27 | 2. Code Sample
28 |
29 | ```kotlin
30 | import cn.lalaki.iconpackmanager.IconPackManager
31 |
32 | // IconPackManager
33 | val ipm = IconPackManager(packageManager)
34 |
35 | val iconPacks = ipm.isSupportedIconPacks()
36 |
37 | ipm.isSupportedIconPacks(true) //Force Flush IconPack cache
38 |
39 | iconPacks.forEach {
40 | //……
41 | }
42 |
43 | // Add rules for a custom icon
44 | val rules = HashMap>
45 | rules["com.android.chrome"]=arrayOf("browser","...")
46 | rules["com.android.email"]=arrayOf("mail","message")
47 | rules["video"]=arrayOf("tencent","youtube")
48 | iconPackItem.setRules(rules)
49 | /** Use this method to add rules when you need to specify icons for an application,
50 | parameter 1: package name, parameter 2: keyword (icon resource name) Fuzzy Matching
51 | How to get keywords? see icon pack.apk assets/appfilter.xml
52 | */
53 |
54 | // clear all rules, if need
55 | iconPackItem.setRules(null)
56 | ```
57 | ```xml
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | ...
67 | ...
68 | ...
69 |
70 | ```
71 | *ipm.isSupportedIconPacks() is a list that can be traversed to get all the icon packs.*
72 | ```kotlin
73 | // load icon pack
74 | ipm.isSupportedIconPacks().forEach {
75 | /** If you have more than one icon pack theme installed, you need to exclude it here
76 | filter other icon pack**/
77 | // if (it.name != "your icon pack")
78 |
79 | //get icon pack name
80 | val iconPackName = it.name
81 |
82 | //get icon from icon pack
83 | val launchIntent = getLaunchIntentForPackage(packageName)
84 | val icon = it.loadIcon(launchIntent) //Intent or ComponentName or ApplicationInfo
85 |
86 | //if not found icon, modify the original icon
87 | if(icon == null)
88 | {
89 | //Cutting circles
90 | it.transformIcon(applicationInfo.loadIcon(packageManager),0.5f,scaleF,saturation)
91 |
92 | //Cropping rounded corners requires a number of parameters to be defined,
93 | // in order of radius, scale, and color saturation,
94 | // and a variable type parameter, Float, which defaults to 1f.
95 | it.transformIcon(applicationInfo.loadIcon(packageManager),radius,scaleF,saturation)
96 | }
97 | }
98 | ```
99 |
100 | ## Demo
101 |
102 |
103 |
104 | ## About
105 |
106 | Generating electricity for love.
107 |
108 | + feedback:dazen@189.cn
109 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/library/src/main/kotlin/cn/lalaki/iconpackmanager/IconPackManager.kt:
--------------------------------------------------------------------------------
1 | package cn.lalaki.iconpackmanager
2 |
3 | import android.content.ComponentName
4 | import android.content.Intent
5 | import android.content.pm.ApplicationInfo
6 | import android.content.pm.PackageManager
7 | import android.content.res.Resources
8 | import android.content.res.XmlResourceParser
9 | import android.graphics.Bitmap
10 | import android.graphics.BitmapFactory
11 | import android.graphics.Canvas
12 | import android.graphics.ColorMatrix
13 | import android.graphics.ColorMatrixColorFilter
14 | import android.graphics.Paint
15 | import android.graphics.Path
16 | import android.graphics.RectF
17 | import android.graphics.drawable.BitmapDrawable
18 | import android.graphics.drawable.Drawable
19 | import android.os.Build
20 |
21 | @Suppress("unused", "deprecation")
22 | open class IconPackManager(
23 | val pm: PackageManager,
24 | ) {
25 | private val iconPacks by lazy { mutableListOf() }
26 | private val paint by lazy { Paint() }
27 | private val rect by lazy { RectF() }
28 | private val path by lazy { Path() }
29 | private val type = "drawable"
30 |
31 | open fun isSupportedIconPacks() = isSupportedIconPacks(false)
32 |
33 | open fun isSupportedIconPacks(reload: Boolean): MutableList {
34 | if (iconPacks.isEmpty() || reload) {
35 | iconPacks.clear()
36 | for (info in pm.queryIntentActivities(
37 | Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER),
38 | PackageManager.GET_META_DATA,
39 | )) {
40 | if (info.activityInfo.flags and (ApplicationInfo.FLAG_SYSTEM) == 0) {
41 | try {
42 | val res = pm.getResourcesForApplication(info.activityInfo.packageName)
43 | val id =
44 | getIdentifier(res, "appfilter", "xml", info.activityInfo.packageName)
45 | if (id > 0) {
46 | iconPacks +=
47 | IconPack(
48 | res.getXml(id),
49 | res,
50 | info.activityInfo.packageName,
51 | info.loadLabel(pm),
52 | )
53 | }
54 | } catch (_: Throwable) {
55 | }
56 | }
57 | }
58 | }
59 | return iconPacks
60 | }
61 |
62 | private fun getIdentifier(
63 | res: Resources,
64 | name: String,
65 | type: String,
66 | packageName: String,
67 | ) = res.getIdentifier(name, type, packageName)
68 |
69 | open inner class IconPack(
70 | xml: XmlResourceParser,
71 | private val res: Resources,
72 | val packageName: String,
73 | val name: CharSequence,
74 | ) {
75 | private val caches by lazy { hashMapOf() }
76 | private var rules: HashMap>? = null
77 | private val icons = hashMapOf()
78 | private var saturation = 1f
79 | private val colorFilter by lazy {
80 | ColorMatrixColorFilter(ColorMatrix().apply { setSaturation(saturation) })
81 | }
82 |
83 | init {
84 | xml.run {
85 | use {
86 | while (eventType != XmlResourceParser.END_DOCUMENT) {
87 | if (eventType == XmlResourceParser.START_TAG && name == "item") {
88 | val value = getAttributeValue(null, type)
89 | if (!value.isNullOrEmpty()) {
90 | val key = getAttributeValue(null, "component")
91 | if (!key.isNullOrEmpty()) {
92 | icons[key] = value
93 | }
94 | }
95 | }
96 | next()
97 | }
98 | }
99 | }
100 | }
101 |
102 | @Deprecated("There may be serious performance loss.")
103 | open fun getAllIconResources(): HashMap {
104 | val drawables = hashMapOf()
105 | val distinctIcon =
106 | icons.entries.distinctBy { it.value }.associate { it.key to it.value }
107 | for (it in distinctIcon) {
108 | val icon = getDrawable(it.value)
109 | if (icon != null) {
110 | drawables[it.key] = icon
111 | }
112 | }
113 | return drawables
114 | }
115 |
116 | private fun getDrawable(value: String): BitmapDrawable? {
117 | val id = getIdentifier(res, value, type, packageName)
118 | if (id > 0) {
119 | val bmp = BitmapFactory.decodeResource(res, id)
120 | return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
121 | BitmapDrawable(bmp)
122 | } else {
123 | BitmapDrawable(res, bmp)
124 | }
125 | }
126 | return null
127 | }
128 |
129 | open fun loadIcon(info: ApplicationInfo): BitmapDrawable? {
130 | if (caches.containsKey(info.packageName)) {
131 | val comp = caches[info.packageName]
132 | if (comp != null) {
133 | val icon = loadIcon(comp)
134 | if (icon != null) return icon
135 | }
136 | }
137 | val activities =
138 | pm.getPackageArchiveInfo(info.sourceDir, PackageManager.GET_ACTIVITIES)?.activities
139 | if (activities != null) {
140 | for (it in activities) {
141 | val icon = loadIcon(ComponentName(info.packageName, it.name))
142 | if (icon != null) {
143 | return icon
144 | }
145 | }
146 | }
147 | return null
148 | }
149 |
150 | open fun loadIcon(launchIntent: Intent): BitmapDrawable? {
151 | val comp = launchIntent.component ?: return null
152 | return loadIcon(comp) ?: loadIcon(
153 | pm.getApplicationInfo(
154 | comp.packageName,
155 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
156 | PackageManager.MATCH_UNINSTALLED_PACKAGES
157 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) {
158 | PackageManager.GET_UNINSTALLED_PACKAGES
159 | } else {
160 | 0
161 | },
162 | ),
163 | )
164 | }
165 |
166 | open fun loadIcon(comp: ComponentName): BitmapDrawable? {
167 | var drawableVal = icons[comp.toString()]
168 | if (drawableVal.isNullOrEmpty()) {
169 | val rules = this.rules ?: return null
170 | var words: Array? = null
171 | for (it in rules) {
172 | if (comp.packageName.contains(it.key, ignoreCase = true)) {
173 | words = rules[it.key]
174 | break
175 | }
176 | }
177 | if (words == null) return null
178 | for (icon in icons.keys) {
179 | if (words.any { icon.contains(it, ignoreCase = true) }) {
180 | drawableVal = icons[icon]
181 | break
182 | }
183 | }
184 | }
185 | if (!drawableVal.isNullOrEmpty()) {
186 | caches[comp.packageName] = comp
187 | return getDrawable(drawableVal)
188 | }
189 | return null
190 | }
191 |
192 | open fun setRules(r: HashMap>) {
193 | this.rules = r
194 | }
195 |
196 | open fun transformIcon(
197 | drawable: Drawable,
198 | vararg params: Float,
199 | ): BitmapDrawable {
200 | var radius: Float? = null
201 | var scale: Float? = null
202 | for (p in params) {
203 | if (radius == null) {
204 | radius = p
205 | } else if (scale == null) {
206 | scale = p
207 | } else {
208 | this.saturation = p
209 | }
210 | }
211 | val icon =
212 | Bitmap.createBitmap(
213 | drawable.intrinsicWidth,
214 | drawable.intrinsicHeight,
215 | Bitmap.Config.ARGB_8888,
216 | )
217 | val canvas = Canvas(icon)
218 | val side = canvas.width.toFloat()
219 | if (scale != null) {
220 | val cwh = side / 2
221 | canvas.scale(scale, scale, cwh, cwh)
222 | }
223 | drawable.setBounds(0, 0, side.toInt(), side.toInt())
224 | rect.set(0f, 0f, side, side)
225 | var roundedWidth = side
226 | if (radius != null) {
227 | roundedWidth *= radius
228 | }
229 | path.reset()
230 | path.addRoundRect(rect, roundedWidth, roundedWidth, Path.Direction.CW)
231 | canvas.clipPath(path)
232 | drawable.draw(canvas)
233 | if (this.saturation != 1f) {
234 | paint.reset()
235 | paint.colorFilter = colorFilter
236 | Canvas(icon).drawBitmap(icon, 0f, 0f, paint)
237 | }
238 | return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.CUPCAKE) {
239 | BitmapDrawable(null, icon)
240 | } else {
241 | BitmapDrawable(icon)
242 | }
243 | }
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/cn/lalaki/tinydesk/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package cn.lalaki.tinydesk
2 |
3 | import android.app.Activity
4 | import android.content.BroadcastReceiver
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.content.IntentFilter
8 | import android.content.pm.PackageManager
9 | import android.graphics.Color
10 | import android.graphics.drawable.Drawable
11 | import android.net.Uri
12 | import android.os.Bundle
13 | import android.provider.Settings
14 | import android.util.DisplayMetrics
15 | import android.view.KeyEvent
16 | import android.view.LayoutInflater
17 | import android.view.View
18 | import android.view.View.OnLongClickListener
19 | import android.view.ViewGroup
20 | import android.view.animation.AnimationUtils
21 | import android.widget.LinearLayout
22 | import android.widget.SearchView
23 | import android.widget.SearchView.OnQueryTextListener
24 | import android.widget.TextView
25 | import androidx.recyclerview.widget.GridLayoutManager
26 | import androidx.recyclerview.widget.ItemTouchHelper
27 | import androidx.recyclerview.widget.RecyclerView
28 | import androidx.recyclerview.widget.RecyclerView.ViewHolder
29 | import cn.lalaki.iconpackmanager.IconPackManager
30 | import net.sourceforge.pinyin4j.BasePinyinHelper
31 | import java.io.File
32 | import java.io.FileReader
33 | import java.io.PrintWriter
34 | import java.util.Collections
35 |
36 | @Suppress("QueryPermissionsNeeded", "InternalInsetResource", "DiscouragedApi")
37 | class MainActivity :
38 | Activity(),
39 | OnQueryTextListener,
40 | OnLongClickListener {
41 | private val appMap = linkedMapOf()
42 | private val recycler by lazy {
43 | RecyclerView(this).apply {
44 | clipToPadding = false
45 | }
46 | }
47 | private val sortConfig by lazy {
48 | File(innerDir.absoluteFile, "sortConfig.txt")
49 | }
50 | private val colConfig by lazy {
51 | File(innerDir.absoluteFile, "colConfig.txt")
52 | }
53 | private val innerDir by lazy { filesDir }
54 | private val search by lazy {
55 | SearchView(this).apply {
56 | setOnQueryTextListener(this@MainActivity)
57 | setOnLongClickListener(this@MainActivity)
58 | isIconifiedByDefault = false
59 | queryHint = "搜索本机应用"
60 | setBackgroundColor(Color.argb(50, 255, 255, 255))
61 | }
62 | }
63 |
64 | @Suppress("ResourceType")
65 | override fun onCreate(savedInstanceState: Bundle?) {
66 | super.onCreate(savedInstanceState)
67 | var statusBarHeight = 20
68 | val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
69 | if (resourceId > 0) {
70 | statusBarHeight = resources.getDimensionPixelSize(resourceId)
71 | }
72 | setContentView(
73 | LinearLayout(this).apply {
74 | orientation = LinearLayout.VERTICAL
75 | isFocusableInTouchMode = true
76 | setBackgroundColor(Color.argb(35, 255, 255, 255))
77 | addView(
78 | search,
79 | LinearLayout
80 | .LayoutParams(
81 | LinearLayout.LayoutParams.MATCH_PARENT,
82 | LinearLayout.LayoutParams.WRAP_CONTENT,
83 | ).apply {
84 | leftMargin = 20
85 | rightMargin = 20
86 | topMargin = statusBarHeight + 20
87 | },
88 | )
89 | addView(
90 | recycler,
91 | LinearLayout.LayoutParams(
92 | LinearLayout.LayoutParams.MATCH_PARENT,
93 | 0,
94 | 1f,
95 | ),
96 | )
97 | },
98 | )
99 | var column = 3
100 | val colTxt = readConfig(colConfig)
101 | if (colTxt.isNotEmpty()) {
102 | val col = colTxt.toIntOrNull()
103 | if (col != null && col > 0) column = col
104 | }
105 | recycler.layoutManager = GridLayoutManager(this, column)
106 | val side =
107 | resources.displayMetrics.widthPixels / (recycler.layoutManager as GridLayoutManager).spanCount
108 | val density = resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
109 | val mTextSize = side / density * 0.135f
110 | val paddingSide = side / 5
111 | val halfSide = side / 2
112 | recycler.setPadding(0, paddingSide, 0, paddingSide)
113 | val topVal = side / 8
114 | val pm = packageManager
115 | val iconPack = IconPackManager(pm).isSupportedIconPacks().firstOrNull()
116 | for (app in pm.queryIntentActivities(
117 | Intent(Intent.ACTION_MAIN).addCategory(
118 | Intent.CATEGORY_LAUNCHER,
119 | ),
120 | PackageManager.GET_ACTIVITIES,
121 | )) {
122 | val appName = app.loadLabel(pm).toString()
123 | if (appName.contains("输入法") ||
124 | appName.contains(
125 | "gboard",
126 | ignoreCase = true,
127 | ) ||
128 | packageName == app.activityInfo.packageName ||
129 | app.activityInfo.packageName == iconPack?.packageName
130 | ) {
131 | continue
132 | }
133 | val launchIntent =
134 | Intent()
135 | .setClassName(app.activityInfo.packageName, app.activityInfo.name)
136 | .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
137 | var icon = app.loadIcon(pm)
138 | if (iconPack != null) {
139 | val customIcon = iconPack.loadIcon(launchIntent)
140 | val iconPackName = iconPack.name.toString().lowercase()
141 | if (customIcon == null) {
142 | icon =
143 | if (iconPackName.contains("aura")) { // 这些图标默认是圆角
144 | iconPack.transformIcon(
145 | icon,
146 | 0.3f,
147 | 0.92f,
148 | )
149 | } else {
150 | if (iconPackName.contains("delta")) {
151 | iconPack.transformIcon(
152 | icon,
153 | 0.5f,
154 | 0.89f,
155 | 0.72f,
156 | )
157 | } else {
158 | iconPack.transformIcon(
159 | icon,
160 | 0.5f,
161 | 0.89f,
162 | )
163 | }
164 | }
165 | } else {
166 | icon = customIcon
167 | }
168 | }
169 | val app0 =
170 | AppModel(
171 | appName,
172 | icon,
173 | mTextSize,
174 | side,
175 | halfSide,
176 | launchIntent,
177 | Uri.parse("package:${app.activityInfo.packageName}"),
178 | topVal,
179 | app.activityInfo.packageName,
180 | )
181 | appMap[app.activityInfo.packageName] = app0
182 | if (app0.pinyin.isEmpty()) {
183 | for (c in appName) {
184 | try {
185 | app0.pinyin +=
186 | BasePinyinHelper
187 | .toHanyuPinyinStringArray(this, c)
188 | .joinToString(transform = { it.replace(Regex("\\d+"), "") })
189 | } catch (_: Throwable) {
190 | }
191 | }
192 | }
193 | }
194 | val sortsTxt = readConfig(sortConfig)
195 | if (sortsTxt.isNotEmpty()) {
196 | val sortArr = sortsTxt.split(";")
197 | val sortedList =
198 | appMap.map { it.value }.sortedBy {
199 | val index = sortArr.indexOf(it.backupKey)
200 | if (index == -1) 999 else index
201 | }
202 | appMap.clear()
203 | sortedList.forEach {
204 | appMap[it.backupKey] = it
205 | }
206 | }
207 | registerReceiver(
208 | object : BroadcastReceiver() {
209 | override fun onReceive(
210 | c: Context,
211 | i: Intent,
212 | ) {
213 | val packageName =
214 | i.dataString
215 | .toString()
216 | .split(':')
217 | .last()
218 | val adapter = recycler.adapter as AppListAdapter
219 | when (i.action) {
220 | Intent.ACTION_PACKAGE_ADDED -> {
221 | if (!adapter.map.containsKey(packageName)) recreate()
222 | }
223 |
224 | Intent.ACTION_PACKAGE_REMOVED -> {
225 | try {
226 | pm.getApplicationEnabledSetting(packageName)
227 | } catch (_: Throwable) {
228 | var delIndex = -1
229 | adapter.map.onEachIndexed { index, entry ->
230 | if (entry.key == packageName) delIndex = index
231 | }
232 | if (delIndex != -1) {
233 | adapter.map.remove(packageName)
234 | adapter.notifyItemRemoved(delIndex)
235 | }
236 | if (appMap.containsKey(packageName)) {
237 | appMap.remove(packageName)
238 | }
239 | }
240 | }
241 | }
242 | }
243 | },
244 | IntentFilter().apply {
245 | addAction(Intent.ACTION_PACKAGE_ADDED)
246 | addAction(Intent.ACTION_PACKAGE_REMOVED)
247 | addDataScheme("package")
248 | },
249 | )
250 | recycler.adapter = AppListAdapter(appMap)
251 | }
252 |
253 | fun writeConfig(
254 | config: File,
255 | configTxt: String,
256 | ) {
257 | val out = PrintWriter(config)
258 | out.print(configTxt)
259 | out.close()
260 | }
261 |
262 | private fun readConfig(config: File): String {
263 | var configTxt = ""
264 | if (config.exists()) {
265 | val reader = FileReader(config)
266 | configTxt = reader.readText()
267 | reader.close()
268 | }
269 | return configTxt
270 | }
271 |
272 | override fun onResume() {
273 | super.onResume()
274 | clearSearchText()
275 | }
276 |
277 | private fun clearSearchText() {
278 | if (search.isFocusable || search.query.toString().isNotEmpty()) {
279 | search.setQuery("", false)
280 | }
281 | search.clearFocus()
282 | }
283 |
284 | override fun onKeyDown(
285 | keyCode: Int,
286 | event: KeyEvent?,
287 | ): Boolean {
288 | if (keyCode == KeyEvent.KEYCODE_BACK) {
289 | clearSearchText()
290 | val adapter = recycler.adapter
291 | if (adapter is AppListAdapter) {
292 | if (adapter.canDrag) {
293 | adapter.disableDrag()
294 | }
295 | }
296 | return true
297 | }
298 | return super.onKeyDown(keyCode, event)
299 | }
300 |
301 | override fun onQueryTextSubmit(query: String?): Boolean = false
302 |
303 | @Suppress("NotifyDataSetChanged", "UNCHECKED_CAST")
304 | override fun onQueryTextChange(newText: String?): Boolean {
305 | val appAdapter = recycler.adapter
306 | if (appAdapter is AppListAdapter) {
307 | appAdapter.map = appMap.clone() as LinkedHashMap
308 | if (newText != null) {
309 | if (1 == newText.toSet().size && newText.length > 8) {
310 | val num = newText[0].digitToIntOrNull()
311 | if (num != null && num > 0) {
312 | if (num == 3) {
313 | colConfig.delete()
314 | } else {
315 | writeConfig(colConfig, num.toString())
316 | }
317 | recreate()
318 | }
319 | }
320 | val text = newText.replace(Regex("\\s+"), "")
321 | if (text.isNotEmpty()) {
322 | if (text == "设置壁纸") {
323 | try {
324 | startActivity(
325 | Intent.createChooser(
326 | Intent(Intent.ACTION_SET_WALLPAPER),
327 | "选择壁纸",
328 | ),
329 | )
330 | } catch (_: Throwable) {
331 | }
332 | return true
333 | }
334 | val removeList = arrayListOf()
335 | for (it in appAdapter.map) {
336 | if (!it.value.appName.contains(
337 | text,
338 | ignoreCase = true,
339 | ) &&
340 | !it.value.pinyin.contains(
341 | text,
342 | ignoreCase = true,
343 | ) &&
344 | !it.key.contains(text, ignoreCase = true)
345 | ) {
346 | removeList.add(it.key)
347 | }
348 | }
349 | removeList.forEach { appAdapter.map.remove(it) }
350 | }
351 | }
352 | appAdapter.notifyDataSetChanged()
353 | }
354 | return false
355 | }
356 |
357 | override fun onLongClick(p0: View?): Boolean {
358 | val adapter = recycler.adapter
359 | if (adapter is AppListAdapter) {
360 | if (adapter.canDrag) {
361 | adapter.disableDrag()
362 | } else {
363 | adapter.enableDrag()
364 | }
365 | }
366 | return true
367 | }
368 |
369 | class AppModel(
370 | val appName: String,
371 | val icon: Drawable,
372 | val mTextSize: Float,
373 | val side: Int,
374 | val halfSide: Int,
375 | val launchIntent: Intent,
376 | val detail: Uri?,
377 | val topVal: Int,
378 | var backupKey: String,
379 | ) {
380 | var pinyin: String = ""
381 | }
382 |
383 | inner class AppViewHolder(
384 | private val adapter: AppListAdapter,
385 | itemView: View,
386 | ) : ViewHolder(itemView) {
387 | private var view = itemView.findViewById(R.id.tv)
388 | private val animaCache by lazy {
389 | AnimationUtils.loadAnimation(itemView.context, R.anim.shake)
390 | }
391 |
392 | fun startAnimation() {
393 | if (itemView.animation != animaCache) {
394 | itemView.animation = animaCache
395 | }
396 | animaCache?.startNow()
397 | }
398 |
399 | fun stopAnimation() {
400 | itemView.animation?.cancel()
401 | itemView.animation = null
402 | }
403 |
404 | fun bind(m: AppModel) {
405 | if (adapter.canDrag) {
406 | startAnimation()
407 | } else {
408 | stopAnimation()
409 | }
410 | if (!view.compoundDrawables.contains(m.icon)) {
411 | view.text = m.appName
412 | view.textSize = m.mTextSize
413 | val params = view.layoutParams
414 | if (params is RecyclerView.LayoutParams) {
415 | params.width = m.side
416 | params.height = m.side
417 | params.topMargin = m.topVal
418 | }
419 | m.icon.setBounds(0, 0, m.halfSide, m.halfSide)
420 | view.setCompoundDrawables(null, m.icon, null, null)
421 | view.setOnClickListener {
422 | startActivity(m.launchIntent)
423 | }
424 | view.setOnLongClickListener {
425 | val adapter = recycler.adapter
426 | if (adapter is AppListAdapter) {
427 | if (!adapter.canDrag) {
428 | startActivity(
429 | Intent(
430 | Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
431 | m.detail,
432 | ),
433 | )
434 | }
435 | }
436 | false
437 | }
438 | }
439 | }
440 | }
441 |
442 | inner class AppListAdapter(
443 | var map: LinkedHashMap,
444 | ) : RecyclerView.Adapter() {
445 | private val helper = HolderTouchHelper()
446 | var canDrag = false
447 |
448 | override fun onCreateViewHolder(
449 | parent: ViewGroup,
450 | viewType: Int,
451 | ): AppViewHolder =
452 | AppViewHolder(
453 | this,
454 | LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false),
455 | )
456 |
457 | fun swap(
458 | source: Int,
459 | target: Int,
460 | ) = run {
461 | val tempArray = map.map { it.value }
462 | Collections.swap(tempArray, source, target)
463 | appMap.clear()
464 | val sb = StringBuilder()
465 | tempArray.forEach {
466 | appMap[it.backupKey] = it
467 | sb.append("${it.backupKey};")
468 | }
469 | writeConfig(sortConfig, sb.toString())
470 | map = appMap
471 | }
472 |
473 | override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
474 | helper.attachToView(recyclerView)
475 | super.onAttachedToRecyclerView(recyclerView)
476 | }
477 |
478 | fun disableDrag() {
479 | canDrag = false
480 | helper.disableDrag()
481 | animationRefresh()
482 | }
483 |
484 | fun enableDrag() {
485 | canDrag = true
486 | helper.enableDrag()
487 | animationRefresh()
488 | }
489 |
490 | private fun animationRefresh() {
491 | map.keys.forEachIndexed { index, _ ->
492 | notifyItemChanged(index)
493 | }
494 | }
495 |
496 | override fun getItemCount() = map.size
497 |
498 | override fun onBindViewHolder(
499 | holder: AppViewHolder,
500 | position: Int,
501 | ) {
502 | holder.bind(map.values.elementAt(position))
503 | }
504 | }
505 |
506 | class HolderTouchHelper : ItemTouchHelper.SimpleCallback(0, 0) {
507 | private val helper = ItemTouchHelper(this)
508 | private var viewHolder: AppViewHolder? = null
509 |
510 | fun attachToView(view: RecyclerView?) = helper.attachToRecyclerView(view)
511 |
512 | fun disableDrag() {
513 | setDefaultDragDirs(0)
514 | }
515 |
516 | fun enableDrag() {
517 | setDefaultDragDirs(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT or ItemTouchHelper.UP or ItemTouchHelper.DOWN)
518 | }
519 |
520 | override fun onSwiped(
521 | viewHolder: ViewHolder,
522 | direction: Int,
523 | ) = Unit
524 |
525 | override fun onSelectedChanged(
526 | viewHolder: ViewHolder?,
527 | actionState: Int,
528 | ) {
529 | super.onSelectedChanged(viewHolder, actionState)
530 | if (this.viewHolder == null && viewHolder == null) return
531 | when (actionState) {
532 | ItemTouchHelper.ACTION_STATE_DRAG -> {
533 | if (viewHolder is AppViewHolder) {
534 | this.viewHolder = viewHolder
535 | viewHolder.stopAnimation()
536 | }
537 | }
538 |
539 | ItemTouchHelper.ACTION_STATE_IDLE -> {
540 | val holder = this.viewHolder
541 | if (holder is AppViewHolder) {
542 | holder.startAnimation()
543 | this.viewHolder = null
544 | }
545 | }
546 | }
547 | }
548 |
549 | override fun onMove(
550 | recyclerView: RecyclerView,
551 | viewHolder: ViewHolder,
552 | target: ViewHolder,
553 | ): Boolean {
554 | if (target is AppViewHolder) {
555 | val sourcePos = viewHolder.layoutPosition
556 | val targetPos = target.layoutPosition
557 | val adapter = recyclerView.adapter
558 | if (adapter is AppListAdapter) {
559 | if (sourcePos < targetPos) {
560 | for (i in sourcePos until targetPos) {
561 | adapter.swap(i, i + 1)
562 | }
563 | } else {
564 | for (i in sourcePos downTo targetPos + 1) {
565 | adapter.swap(i, i - 1)
566 | }
567 | }
568 | adapter.notifyItemMoved(sourcePos, targetPos)
569 | }
570 | }
571 | return true
572 | }
573 | }
574 | }
575 |
--------------------------------------------------------------------------------