diff --git a/.idea/misc.xml b/.idea/misc.xml
index 98f4325..40b3180 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -8,6 +8,8 @@
+
+
diff --git a/app/build.gradle b/app/build.gradle
index 82947bd..50bbb82 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -60,6 +60,7 @@ tasks.whenTaskAdded {
}
dependencies {
+ implementation 'com.github.tiann:FreeReflection:3.1.0'
implementation "com.github.topjohnwu.libsu:core:3.1.2"
implementation 'androidx.annotation:annotation:1.3.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.1'
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 786522e..123d549 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -40,6 +40,8 @@
-keep class android.support**
-keep class androidx**
+-keep class me.weishu**{*;}
+
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
diff --git a/app/src/main/java/com/fankes/coloros/notify/application/CNNApplication.kt b/app/src/main/java/com/fankes/coloros/notify/application/CNNApplication.kt
index 20ea087..cdcba15 100644
--- a/app/src/main/java/com/fankes/coloros/notify/application/CNNApplication.kt
+++ b/app/src/main/java/com/fankes/coloros/notify/application/CNNApplication.kt
@@ -25,7 +25,9 @@
package com.fankes.coloros.notify.application
import android.app.Application
+import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
+import me.weishu.reflection.Reflection
class CNNApplication : Application() {
@@ -38,6 +40,12 @@ class CNNApplication : Application() {
val appContext get() = context ?: error("App is death")
}
+ override fun attachBaseContext(base: Context?) {
+ super.attachBaseContext(base)
+ /** 解锁隐藏 API */
+ Reflection.unseal(base)
+ }
+
override fun onCreate() {
super.onCreate()
/** 设置静态实例 */
diff --git a/app/src/main/java/com/fankes/coloros/notify/bean/IconDataBean.kt b/app/src/main/java/com/fankes/coloros/notify/bean/IconDataBean.kt
index 04da6f0..819a8f8 100644
--- a/app/src/main/java/com/fankes/coloros/notify/bean/IconDataBean.kt
+++ b/app/src/main/java/com/fankes/coloros/notify/bean/IconDataBean.kt
@@ -47,4 +47,13 @@ data class IconDataBean(
) : Serializable {
fun toEnabledName() = ("$appName$packageName").base64 + "_enable"
fun toEnabledAllName() = ("$appName$packageName").base64 + "_enable_all"
+ override fun toString() = "{\n" +
+ " \"appName\": \"$appName\",\n" +
+ " \"packageName\": \"$packageName\",\n" +
+ " \"iconBitmap\": \"${iconBitmap.base64}\",\n" +
+ " \"iconColor\": \"#${Integer.toHexString(iconColor)}\",\n" +
+ " \"contributorName\": \"$contributorName\",\n" +
+ " \"isEnabled\": $isEnabled,\n" +
+ " \"isEnabledAll\": $isEnabledAll\n" +
+ " }"
}
\ No newline at end of file
diff --git a/app/src/main/java/com/fankes/coloros/notify/hook/HookConst.kt b/app/src/main/java/com/fankes/coloros/notify/hook/HookConst.kt
index fe3cb50..cc4c98a 100644
--- a/app/src/main/java/com/fankes/coloros/notify/hook/HookConst.kt
+++ b/app/src/main/java/com/fankes/coloros/notify/hook/HookConst.kt
@@ -33,5 +33,12 @@ object HookConst {
const val REMOVE_CHANGECP_NOTIFY = "_remove_charge_complete_notify"
const val NOTIFY_ICON_DATAS = "_notify_icon_datas"
+ const val SOURCE_SYNC_WAY = "_source_sync_way"
+ const val SOURCE_SYNC_WAY_CUSTOM_URL = "_source_sync_way_custom_url"
+
+ const val TYPE_SOURCE_SYNC_WAY_1 = 1000
+ const val TYPE_SOURCE_SYNC_WAY_2 = 2000
+ const val TYPE_SOURCE_SYNC_WAY_3 = 3000
+
const val SYSTEMUI_PACKAGE_NAME = "com.android.systemui"
}
\ No newline at end of file
diff --git a/app/src/main/java/com/fankes/coloros/notify/param/IconPackParams.kt b/app/src/main/java/com/fankes/coloros/notify/param/IconPackParams.kt
index 8cacf1c..d9e1a44 100644
--- a/app/src/main/java/com/fankes/coloros/notify/param/IconPackParams.kt
+++ b/app/src/main/java/com/fankes/coloros/notify/param/IconPackParams.kt
@@ -142,6 +142,20 @@ class IconPackParams(private val context: Context? = null, private val param: Pa
dataJson1.replace(oldValue = "]", newValue = "") + "," + dataJson2.replace(oldValue = "[", newValue = "")
}
+ /**
+ * 是否不为合法 JSON
+ * @param json 数据
+ * @return [Boolean]
+ */
+ fun isNotVaildJson(json: String) = json.trim().let { !it.startsWith("[") || !it.endsWith("]") }
+
+ /**
+ * 是否为异常地址
+ * @param json 数据
+ * @return [Boolean]
+ */
+ fun isHackString(json: String) = json.contains(other = "Checking your browser before accessing")
+
/**
* 比较图标数据不相等
* @param dataJson 图标数据 JSON
diff --git a/app/src/main/java/com/fankes/coloros/notify/ui/ConfigureActivity.kt b/app/src/main/java/com/fankes/coloros/notify/ui/ConfigureActivity.kt
index 3575dcf..f342c23 100644
--- a/app/src/main/java/com/fankes/coloros/notify/ui/ConfigureActivity.kt
+++ b/app/src/main/java/com/fankes/coloros/notify/ui/ConfigureActivity.kt
@@ -34,8 +34,14 @@ import android.widget.ListView
import android.widget.TextView
import androidx.constraintlayout.utils.widget.ImageFilterView
import androidx.core.view.isVisible
+import androidx.core.widget.doOnTextChanged
import com.fankes.coloros.notify.R
import com.fankes.coloros.notify.bean.IconDataBean
+import com.fankes.coloros.notify.hook.HookConst.SOURCE_SYNC_WAY
+import com.fankes.coloros.notify.hook.HookConst.SOURCE_SYNC_WAY_CUSTOM_URL
+import com.fankes.coloros.notify.hook.HookConst.TYPE_SOURCE_SYNC_WAY_1
+import com.fankes.coloros.notify.hook.HookConst.TYPE_SOURCE_SYNC_WAY_2
+import com.fankes.coloros.notify.hook.HookConst.TYPE_SOURCE_SYNC_WAY_3
import com.fankes.coloros.notify.hook.factory.isAppNotifyHookAllOf
import com.fankes.coloros.notify.hook.factory.isAppNotifyHookOf
import com.fankes.coloros.notify.hook.factory.putAppNotifyHookAllOf
@@ -44,14 +50,13 @@ import com.fankes.coloros.notify.param.IconPackParams
import com.fankes.coloros.notify.ui.base.BaseActivity
import com.fankes.coloros.notify.utils.*
import com.fankes.coloros.notify.view.MaterialSwitch
+import com.google.android.material.radiobutton.MaterialRadioButton
import com.google.android.material.textfield.TextInputEditText
+import com.highcapable.yukihookapi.hook.factory.modulePrefs
import com.highcapable.yukihookapi.hook.xposed.YukiHookModuleStatus
class ConfigureActivity : BaseActivity() {
- /** 访问请求链接 */
- private var rawGithubUrl = "https://raw.fastgit.org/fankes/AndroidNotifyIconAdapt/main"
-
/** 当前筛选条件 */
private var filterText = ""
@@ -188,7 +193,18 @@ class ConfigureActivity : BaseActivity() {
lateinit var switchOpen: MaterialSwitch
lateinit var switchAll: MaterialSwitch
}
- }.apply { onChanged = { notifyDataSetChanged() } }
+ }.apply {
+ setOnItemLongClickListener { _, _, p, _ ->
+ showDialog {
+ title = "复制“${iconDatas[p].appName}”的规则"
+ msg = "是否复制单条规则到剪贴板?"
+ confirmButton { copyToClipboard(iconDatas[p].toString()) }
+ cancelButton()
+ }
+ true
+ }
+ onChanged = { notifyDataSetChanged() }
+ }
onScrollEvent = { post { setSelection(if (it) iconDatas.lastIndex else 0) } }
}
/** 设置点击事件 */
@@ -210,16 +226,99 @@ class ConfigureActivity : BaseActivity() {
/** 首次进入或更新数据 */
private fun onStartRefresh() =
showDialog {
- title = if (iconAllDatas.isNotEmpty()) "同步列表" else "初始化"
- msg = (if (iconAllDatas.isNotEmpty()) "建议定期从云端拉取数据以获得最新的通知图标优化名单适配数据。\n\n"
- else "首次装载需要从云端下载最新适配数据,后续可继续前往这里检查更新。\n\n") +
- "通过从 Github 同步最新数据,无法连接可能需要魔法上网。"
- confirmButton(text = "开始同步") { onRefreshing() }
+ title = "同步列表"
+ var sourceType = modulePrefs.getInt(SOURCE_SYNC_WAY, TYPE_SOURCE_SYNC_WAY_1)
+ var customUrl = modulePrefs.getString(SOURCE_SYNC_WAY_CUSTOM_URL)
+ addView(R.layout.dia_source_from).apply {
+ val radio1 = findViewById(R.id.dia_sf_rd1)
+ val radio2 = findViewById(R.id.dia_sf_rd2)
+ val radio3 = findViewById(R.id.dia_sf_rd3)
+ val edLin = findViewById(R.id.dia_sf_text_lin)
+ findViewById(R.id.dia_sf_text).apply {
+ if (customUrl.isNotBlank()) {
+ setText(customUrl)
+ setSelection(customUrl.length)
+ }
+ doOnTextChanged { text, _, _, _ ->
+ customUrl = text.toString()
+ modulePrefs.putString(SOURCE_SYNC_WAY_CUSTOM_URL, text.toString())
+ }
+ }
+ edLin.isVisible = sourceType == TYPE_SOURCE_SYNC_WAY_3
+ radio1.isChecked = sourceType == TYPE_SOURCE_SYNC_WAY_1
+ radio2.isChecked = sourceType == TYPE_SOURCE_SYNC_WAY_2
+ radio3.isChecked = sourceType == TYPE_SOURCE_SYNC_WAY_3
+ radio1.setOnClickListener {
+ radio2.isChecked = false
+ radio3.isChecked = false
+ edLin.isVisible = false
+ sourceType = TYPE_SOURCE_SYNC_WAY_1
+ modulePrefs.putInt(SOURCE_SYNC_WAY, TYPE_SOURCE_SYNC_WAY_1)
+ }
+ radio2.setOnClickListener {
+ radio1.isChecked = false
+ radio3.isChecked = false
+ edLin.isVisible = false
+ sourceType = TYPE_SOURCE_SYNC_WAY_2
+ modulePrefs.putInt(SOURCE_SYNC_WAY, TYPE_SOURCE_SYNC_WAY_2)
+ }
+ radio3.setOnClickListener {
+ radio1.isChecked = false
+ radio2.isChecked = false
+ edLin.isVisible = true
+ sourceType = TYPE_SOURCE_SYNC_WAY_3
+ modulePrefs.putInt(SOURCE_SYNC_WAY, TYPE_SOURCE_SYNC_WAY_3)
+ }
+ }
+ confirmButton {
+ when (sourceType) {
+ TYPE_SOURCE_SYNC_WAY_1 -> onRefreshing(url = "https://raw.fastgit.org/fankes/AndroidNotifyIconAdapt/main")
+ TYPE_SOURCE_SYNC_WAY_2 -> onRefreshing(url = "https://raw.githubusercontent.com/fankes/AndroidNotifyIconAdapt/main")
+ TYPE_SOURCE_SYNC_WAY_3 ->
+ if (customUrl.isNotBlank())
+ if (customUrl.startsWith("http://") || customUrl.startsWith("https://"))
+ onRefreshingCustom(customUrl)
+ else snake(msg = "同步地址不是一个合法的 URL")
+ else snake(msg = "同步地址不能为空")
+ else -> snake(msg = "同步类型错误")
+ }
+ }
cancelButton()
+ neutralButton(text = "自定义规则") {
+ showDialog {
+ title = "自定义规则"
+ var editText: TextInputEditText
+ addView(R.layout.dia_source_from_string).apply {
+ editText = findViewById(R.id.dia_sfs_input_edit).apply {
+ requestFocus()
+ invalidate()
+ }
+ }
+ confirmButton {
+ IconPackParams(context = this@ConfigureActivity).also { params ->
+ when {
+ editText.text.toString().isNotBlank() && params.isNotVaildJson(editText.text.toString()) ->
+ snake(msg = "不是有效的 JSON 数据")
+ editText.text.toString().isNotBlank() -> {
+ params.save(editText.text.toString())
+ filterText = ""
+ mockLocalData()
+ SystemUITool.showNeedUpdateApplySnake(context = this@ConfigureActivity)
+ }
+ else -> snake(msg = "规则数组内容为空")
+ }
+ }
+ }
+ cancelButton()
+ }
+ }
}
- /** 开始更新数据 */
- private fun onRefreshing() = ClientRequestTool.checkingInternetConnect(context = this) {
+ /**
+ * 开始更新数据
+ * @param url
+ */
+ private fun onRefreshing(url: String) = ClientRequestTool.checkingInternetConnect(context = this) {
ProgressDialog(this).apply {
setDefaultStyle(context = this@ConfigureActivity)
setCancelable(false)
@@ -229,22 +328,27 @@ class ConfigureActivity : BaseActivity() {
}.also {
ClientRequestTool.wait(
context = this,
- url = "$rawGithubUrl/OS/ColorOS/NotifyIconsSupportConfig.json"
+ url = "$url/OS/ColorOS/NotifyIconsSupportConfig.json"
) { isDone1, ctOS ->
it.setMessage("正在同步 APP 数据")
ClientRequestTool.wait(
context = this,
- url = "$rawGithubUrl/APP/NotifyIconsSupportConfig.json"
+ url = "$url/APP/NotifyIconsSupportConfig.json"
) { isDone2, ctAPP ->
it.cancel()
IconPackParams(context = this).also { params ->
if (isDone1 && isDone2) params.splicingJsonArray(ctOS, ctAPP).also {
- if (params.isCompareDifferent(it)) {
- params.save(it)
- filterText = ""
- mockLocalData()
- SystemUITool.showNeedUpdateApplySnake(context = this)
- } else snake(msg = "列表数据已是最新")
+ when {
+ params.isHackString(it) -> snake(msg = "请求需要验证,请尝试魔法上网或关闭魔法")
+ params.isNotVaildJson(it) -> snake(msg = "在线规则发生问题,请稍后重试")
+ params.isCompareDifferent(it) -> {
+ params.save(it)
+ filterText = ""
+ mockLocalData()
+ SystemUITool.showNeedUpdateApplySnake(context = this)
+ }
+ else -> snake(msg = "列表数据已是最新")
+ }
} else showDialog {
title = "连接失败"
msg = "连接失败,错误如下:\n${if (!isDone1) ctOS else ctAPP}"
@@ -259,6 +363,46 @@ class ConfigureActivity : BaseActivity() {
}
}
+ /**
+ * 开始更新数据
+ * @param url
+ */
+ private fun onRefreshingCustom(url: String) = ClientRequestTool.checkingInternetConnect(context = this) {
+ ProgressDialog(this).apply {
+ setDefaultStyle(context = this@ConfigureActivity)
+ setCancelable(false)
+ setTitle("同步中")
+ setMessage("正在通过自定义地址同步数据")
+ show()
+ }.also {
+ ClientRequestTool.wait(
+ context = this,
+ url = url
+ ) { isDone, content ->
+ it.cancel()
+ IconPackParams(context = this).also { params ->
+ if (isDone)
+ when {
+ params.isHackString(content) -> snake(msg = "请求需要验证,请尝试魔法上网或关闭魔法")
+ params.isNotVaildJson(content) -> snake(msg = "目标地址不是有效的 JSON 数据")
+ params.isCompareDifferent(content) -> {
+ params.save(content)
+ filterText = ""
+ mockLocalData()
+ SystemUITool.showNeedUpdateApplySnake(context = this)
+ }
+ else -> snake(msg = "列表数据已是最新")
+ }
+ else showDialog {
+ title = "连接失败"
+ msg = "连接失败,错误如下:\n$content"
+ confirmButton(text = "我知道了")
+ }
+ }
+ }
+ }
+ }
+
/** 刷新适配器结果相关 */
private fun refreshAdapterResult() {
onChanged?.invoke()
diff --git a/app/src/main/java/com/fankes/coloros/notify/utils/ClientRequestTool.kt b/app/src/main/java/com/fankes/coloros/notify/utils/ClientRequestTool.kt
index ae090b9..5e3e384 100644
--- a/app/src/main/java/com/fankes/coloros/notify/utils/ClientRequestTool.kt
+++ b/app/src/main/java/com/fankes/coloros/notify/utils/ClientRequestTool.kt
@@ -79,7 +79,7 @@ object ClientRequestTool {
* @param url 请求地址
* @param it 回调 - ([Boolean] 是否成功,[String] 成功的内容或失败消息)
*/
- fun wait(context: Activity, url: String, it: (Boolean, String) -> Unit) {
+ fun wait(context: Activity, url: String, it: (Boolean, String) -> Unit) = runCatching {
OkHttpClient().newBuilder().apply {
SSLSocketClient.sSLSocketFactory?.let { sslSocketFactory(it, SSLSocketClient.trustManager) }
hostnameVerifier(SSLSocketClient.hostnameVerifier)
@@ -98,7 +98,7 @@ object ClientRequestTool {
context.runOnUiThread { it(true, bodyString) }
}
})
- }
+ }.onFailure { it(false, "URL 无效") }
/**
* 自动信任 SSL 证书
diff --git a/app/src/main/java/com/fankes/coloros/notify/utils/Utils.kt b/app/src/main/java/com/fankes/coloros/notify/utils/Utils.kt
index 20bc524..f9d1fbd 100644
--- a/app/src/main/java/com/fankes/coloros/notify/utils/Utils.kt
+++ b/app/src/main/java/com/fankes/coloros/notify/utils/Utils.kt
@@ -26,6 +26,8 @@ package com.fankes.coloros.notify.utils
import android.app.Activity
import android.app.AlertDialog
+import android.content.ClipData
+import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
@@ -42,11 +44,13 @@ import android.widget.Toast
import com.fankes.coloros.notify.application.CNNApplication.Companion.appContext
import com.google.android.material.snackbar.Snackbar
import com.highcapable.yukihookapi.hook.factory.classOf
+import com.highcapable.yukihookapi.hook.factory.field
import com.highcapable.yukihookapi.hook.factory.hasClass
import com.highcapable.yukihookapi.hook.factory.method
import com.highcapable.yukihookapi.hook.log.loggerE
import com.highcapable.yukihookapi.hook.type.java.StringType
import com.topjohnwu.superuser.Shell
+import java.io.ByteArrayOutputStream
/**
* 系统深色模式是否开启
@@ -103,7 +107,10 @@ inline val isNotColorOS get() = !isColorOS
*/
val colorOSVersion
get() = safeOf(default = "无法获取") {
- findPropString(key = "ro.system.build.fingerprint", default = "无法获取")
+ classOf(name = "com.oplus.os.OplusBuild").let {
+ it.field { name = "VERSIONS" }.ignoredError().of>()
+ ?.get((it.method { name = "getOplusOSVERSION" }.ignoredError().get().invoke() ?: 23) - 1)
+ } ?: findPropString(key = "ro.system.build.fingerprint", default = "无法获取")
.split("ssi:")[1]
.split("/")[0].trim()
}
@@ -152,6 +159,17 @@ val Number.dp get() = (toFloat() * appContext.resources.displayMetrics.density).
*/
fun Number.dp(context: Context) = (toFloat() * context.resources.displayMetrics.density).toInt()
+/**
+ * Base64 加密
+ * @return [String]
+ */
+val Bitmap.base64
+ get() = safeOfNothing {
+ val baos = ByteArrayOutputStream()
+ compress(Bitmap.CompressFormat.PNG, 100, baos)
+ Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT)
+ }
+
/**
* Base64 加密
* @return [String]
@@ -255,6 +273,19 @@ fun Context.openBrowser(url: String, packageName: String = "") =
else snake(msg = "启动系统浏览器失败")
}
+/**
+ * 复制到剪贴板
+ * @param content 要复制的文本
+ */
+fun Context.copyToClipboard(content: String) = runCatching {
+ (getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).apply {
+ setPrimaryClip(ClipData.newPlainText(null, content))
+ (primaryClip?.getItemAt(0)?.text ?: "").also {
+ if (it != content) snake(msg = "复制失败") else snake(msg = "已复制")
+ }
+ }
+}
+
/**
* 忽略异常返回值
* @param it 回调 - 如果异常为空
diff --git a/app/src/main/res/layout/activity_config.xml b/app/src/main/res/layout/activity_config.xml
index 3bbd983..78167c1 100644
--- a/app/src/main/res/layout/activity_config.xml
+++ b/app/src/main/res/layout/activity_config.xml
@@ -149,7 +149,7 @@
android:divider="@color/trans"
android:dividerHeight="15dp"
android:fadingEdgeLength="10dp"
- android:listSelector="@null"
+ android:listSelector="@color/trans"
android:padding="15dp"
android:requiresFadingEdge="vertical"
android:scrollbars="none" />
diff --git a/app/src/main/res/layout/adapter_config.xml b/app/src/main/res/layout/adapter_config.xml
index 02353d9..5e6a564 100644
--- a/app/src/main/res/layout/adapter_config.xml
+++ b/app/src/main/res/layout/adapter_config.xml
@@ -5,6 +5,7 @@
android:layout_height="wrap_content"
android:background="@drawable/bg_permotion_round"
android:baselineAligned="false"
+ android:descendantFocusability="blocksDescendants"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="15dp"
diff --git a/app/src/main/res/layout/dia_source_from.xml b/app/src/main/res/layout/dia_source_from.xml
new file mode 100644
index 0000000..0d88d51
--- /dev/null
+++ b/app/src/main/res/layout/dia_source_from.xml
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dia_source_from_string.xml b/app/src/main/res/layout/dia_source_from_string.xml
new file mode 100644
index 0000000..ed4712f
--- /dev/null
+++ b/app/src/main/res/layout/dia_source_from_string.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
\ No newline at end of file