diff --git a/.idea/misc.xml b/.idea/misc.xml index 8419a1a..58dbf54 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -10,11 +10,15 @@ + + + + diff --git a/app/src/main/java/com/fankes/miui/notify/bean/IconDataBean.kt b/app/src/main/java/com/fankes/miui/notify/bean/IconDataBean.kt index 9a5a6e3..8cbf125 100644 --- a/app/src/main/java/com/fankes/miui/notify/bean/IconDataBean.kt +++ b/app/src/main/java/com/fankes/miui/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/miui/notify/hook/HookConst.kt b/app/src/main/java/com/fankes/miui/notify/hook/HookConst.kt index eb770e4..cb01b44 100644 --- a/app/src/main/java/com/fankes/miui/notify/hook/HookConst.kt +++ b/app/src/main/java/com/fankes/miui/notify/hook/HookConst.kt @@ -34,5 +34,12 @@ object HookConst { const val ENABLE_NOTIFY_ICON_FIX = "_notify_icon_fix" 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/miui/notify/params/IconPackParams.kt b/app/src/main/java/com/fankes/miui/notify/params/IconPackParams.kt index 6a58b69..8e3f948 100644 --- a/app/src/main/java/com/fankes/miui/notify/params/IconPackParams.kt +++ b/app/src/main/java/com/fankes/miui/notify/params/IconPackParams.kt @@ -88,6 +88,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/miui/notify/ui/ConfigureActivity.kt b/app/src/main/java/com/fankes/miui/notify/ui/ConfigureActivity.kt index b2e9e00..394fe81 100644 --- a/app/src/main/java/com/fankes/miui/notify/ui/ConfigureActivity.kt +++ b/app/src/main/java/com/fankes/miui/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.miui.notify.R import com.fankes.miui.notify.bean.IconDataBean +import com.fankes.miui.notify.hook.HookConst.SOURCE_SYNC_WAY +import com.fankes.miui.notify.hook.HookConst.SOURCE_SYNC_WAY_CUSTOM_URL +import com.fankes.miui.notify.hook.HookConst.TYPE_SOURCE_SYNC_WAY_1 +import com.fankes.miui.notify.hook.HookConst.TYPE_SOURCE_SYNC_WAY_2 +import com.fankes.miui.notify.hook.HookConst.TYPE_SOURCE_SYNC_WAY_3 import com.fankes.miui.notify.hook.factory.isAppNotifyHookAllOf import com.fankes.miui.notify.hook.factory.isAppNotifyHookOf import com.fankes.miui.notify.hook.factory.putAppNotifyHookAllOf @@ -44,14 +50,13 @@ import com.fankes.miui.notify.params.IconPackParams import com.fankes.miui.notify.ui.base.BaseActivity import com.fankes.miui.notify.utils.* import com.fankes.miui.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/MIUI/NotifyIconsSupportConfig.json" + url = "$url/OS/MIUI/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/miui/notify/utils/ClientRequestTool.kt b/app/src/main/java/com/fankes/miui/notify/utils/ClientRequestTool.kt index d8a7fb4..18adf9f 100644 --- a/app/src/main/java/com/fankes/miui/notify/utils/ClientRequestTool.kt +++ b/app/src/main/java/com/fankes/miui/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/miui/notify/utils/Utils.kt b/app/src/main/java/com/fankes/miui/notify/utils/Utils.kt index 551a93b..66e54dc 100644 --- a/app/src/main/java/com/fankes/miui/notify/utils/Utils.kt +++ b/app/src/main/java/com/fankes/miui/notify/utils/Utils.kt @@ -26,6 +26,8 @@ package com.fankes.miui.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 @@ -48,6 +50,7 @@ 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 /** * 系统深色模式是否开启 @@ -195,6 +198,17 @@ val Number.dp get() = (toFloat() * appContext.resources.displayMetrics.density). */ fun Number.dp(context: Context) = toFloat() * context.resources.displayMetrics.density +/** + * 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] @@ -298,6 +312,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 a168931..b5f55d9 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