From d117a9a6e969ac04d71785aa2ef347b0aa9074e5 Mon Sep 17 00:00:00 2001 From: Fankesyooni Date: Tue, 15 Feb 2022 21:52:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=80=9A=E7=9F=A5=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=9B=BE=E6=A0=87=E8=BF=87=E6=BB=A4=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=88=B0=201.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 2 + README.md | 6 +- app/build.gradle | 8 +- app/src/main/AndroidManifest.xml | 15 ++-- .../miui/notify/data/LoginDataSource.kt | 24 ++++++ .../miui/notify/data/LoginRepository.kt | 46 +++++++++++ .../com/fankes/miui/notify/data/Result.kt | 18 +++++ .../miui/notify/data/model/LoggedInUser.kt | 9 +++ .../miui/notify/ui/ConfigureActivity.kt | 73 ++++++++++++++++-- .../fankes/miui/notify/utils/DialogBuilder.kt | 23 +++++- app/src/main/res/layout/activity_config.xml | 50 ++++++++---- app/src/main/res/layout/activity_main.xml | 6 +- app/src/main/res/layout/dia_icon_filter.xml | 24 ++++++ .../main/res/mipmap-xxhdpi/icon_search.png | Bin 5416 -> 0 bytes build.gradle | 4 +- 15 files changed, 265 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/com/fankes/miui/notify/data/LoginDataSource.kt create mode 100644 app/src/main/java/com/fankes/miui/notify/data/LoginRepository.kt create mode 100644 app/src/main/java/com/fankes/miui/notify/data/Result.kt create mode 100644 app/src/main/java/com/fankes/miui/notify/data/model/LoggedInUser.kt create mode 100644 app/src/main/res/layout/dia_icon_filter.xml delete mode 100644 app/src/main/res/mipmap-xxhdpi/icon_search.png diff --git a/.idea/misc.xml b/.idea/misc.xml index 0682c95..31799fa 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -9,8 +9,10 @@ + + diff --git a/README.md b/README.md index 3521350..35e1e24 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # MIUI 原生通知图标 -![Eclipse Marketplace](https://img.shields.io/badge/build-pending-dbab09) +![Eclipse Marketplace](https://img.shields.io/badge/build-passing-brightgreen) ![Eclipse Marketplace](https://img.shields.io/badge/license-AGPL3.0-blue) -![Eclipse Marketplace](https://img.shields.io/badge/version-v1.36-green) +![Eclipse Marketplace](https://img.shields.io/badge/version-v1.5-green)


@@ -12,7 +12,7 @@ Fix the native notification bar icon function abandoned by the MIUI development # 开始使用 点击下载最新版本 -![Eclipse Marketplace](https://img.shields.io/badge/download-v1.36-green) +![Eclipse Marketplace](https://img.shields.io/badge/download-v1.5-green)

⚠️ 适配说明
diff --git a/app/build.gradle b/app/build.gradle index 3519878..b98b7e9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,7 +20,7 @@ android { defaultConfig { applicationId "com.fankes.miui.notify" - minSdk 23 + minSdk 28 targetSdk 31 versionCode rootProject.ext.appVersionCode versionName rootProject.ext.appVersionName @@ -42,6 +42,9 @@ android { kotlinOptions { jvmTarget = '1.8' } + buildFeatures { + viewBinding true + } } /** 移除无效耗时 lint Task */ @@ -57,6 +60,9 @@ tasks.whenTaskAdded { dependencies { 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' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' compileOnly 'de.robv.android.xposed:api:82' implementation 'com.highcapable.yukihookapi:api:1.0.1' ksp 'com.highcapable.yukihookapi:ksp-xposed:1.0.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e84fd70..596c36e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,43 +16,42 @@ - - - + - + android:targetActivity=".ui.MainActivity"> + + \ No newline at end of file diff --git a/app/src/main/java/com/fankes/miui/notify/data/LoginDataSource.kt b/app/src/main/java/com/fankes/miui/notify/data/LoginDataSource.kt new file mode 100644 index 0000000..b295bf6 --- /dev/null +++ b/app/src/main/java/com/fankes/miui/notify/data/LoginDataSource.kt @@ -0,0 +1,24 @@ +package com.fankes.miui.notify.data + +import com.fankes.miui.notify.data.model.LoggedInUser +import java.io.IOException + +/** + * Class that handles authentication w/ login credentials and retrieves user information. + */ +class LoginDataSource { + + fun login(username: String, password: String): Result { + try { + // TODO: handle loggedInUser authentication + val fakeUser = LoggedInUser(java.util.UUID.randomUUID().toString(), "Jane Doe") + return Result.Success(fakeUser) + } catch (e: Throwable) { + return Result.Error(IOException("Error logging in", e)) + } + } + + fun logout() { + // TODO: revoke authentication + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fankes/miui/notify/data/LoginRepository.kt b/app/src/main/java/com/fankes/miui/notify/data/LoginRepository.kt new file mode 100644 index 0000000..43dc2d9 --- /dev/null +++ b/app/src/main/java/com/fankes/miui/notify/data/LoginRepository.kt @@ -0,0 +1,46 @@ +package com.fankes.miui.notify.data + +import com.fankes.miui.notify.data.model.LoggedInUser + +/** + * Class that requests authentication and user information from the remote data source and + * maintains an in-memory cache of login status and user credentials information. + */ + +class LoginRepository(val dataSource: LoginDataSource) { + + // in-memory cache of the loggedInUser object + var user: LoggedInUser? = null + private set + + val isLoggedIn: Boolean + get() = user != null + + init { + // If user credentials will be cached in local storage, it is recommended it be encrypted + // @see https://developer.android.com/training/articles/keystore + user = null + } + + fun logout() { + user = null + dataSource.logout() + } + + fun login(username: String, password: String): Result { + // handle login + val result = dataSource.login(username, password) + + if (result is Result.Success) { + setLoggedInUser(result.data) + } + + return result + } + + private fun setLoggedInUser(loggedInUser: LoggedInUser) { + this.user = loggedInUser + // If user credentials will be cached in local storage, it is recommended it be encrypted + // @see https://developer.android.com/training/articles/keystore + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fankes/miui/notify/data/Result.kt b/app/src/main/java/com/fankes/miui/notify/data/Result.kt new file mode 100644 index 0000000..01cbbeb --- /dev/null +++ b/app/src/main/java/com/fankes/miui/notify/data/Result.kt @@ -0,0 +1,18 @@ +package com.fankes.miui.notify.data + +/** + * A generic class that holds a value with its loading status. + * @param + */ +sealed class Result { + + data class Success(val data: T) : Result() + data class Error(val exception: Exception) : Result() + + override fun toString(): String { + return when (this) { + is Success<*> -> "Success[data=$data]" + is Error -> "Error[exception=$exception]" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fankes/miui/notify/data/model/LoggedInUser.kt b/app/src/main/java/com/fankes/miui/notify/data/model/LoggedInUser.kt new file mode 100644 index 0000000..d2f3ff5 --- /dev/null +++ b/app/src/main/java/com/fankes/miui/notify/data/model/LoggedInUser.kt @@ -0,0 +1,9 @@ +package com.fankes.miui.notify.data.model + +/** + * Data class that captures user information for logged in users retrieved from LoginRepository + */ +data class LoggedInUser( + val userId: String, + val displayName: String +) \ No newline at end of file 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 3e5acf9..9bb0623 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 @@ -35,6 +35,7 @@ import android.widget.ListView import android.widget.TextView import android.widget.Toast import androidx.constraintlayout.utils.widget.ImageFilterView +import androidx.core.view.isVisible import com.fankes.miui.notify.R import com.fankes.miui.notify.hook.factory.isAppNotifyHookAllOf import com.fankes.miui.notify.hook.factory.isAppNotifyHookOf @@ -43,20 +44,58 @@ import com.fankes.miui.notify.hook.factory.putAppNotifyHookOf import com.fankes.miui.notify.params.IconPackParams import com.fankes.miui.notify.ui.base.BaseActivity import com.fankes.miui.notify.utils.SystemUITool +import com.fankes.miui.notify.utils.showDialog import com.fankes.miui.notify.view.MaterialSwitch +import com.google.android.material.textfield.TextInputEditText class ConfigureActivity : BaseActivity() { + /** 当前筛选条件 */ + private var filterText = "" + + /** 回调适配器改变 */ + private var onChanged: (() -> Unit)? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_config) /** 返回按钮点击事件 */ findViewById(R.id.title_back_icon).setOnClickListener { onBackPressed() } - /** 设置标题个数文本 */ - findViewById(R.id.config_title_count_text).text = "已适配 ${IconPackParams.iconDatas.size} 个 APP 的通知图标" - /** 设置搜索按钮点击事件 */ - findViewById(R.id.config_title_search).setOnClickListener { - Toast.makeText(this, "后期开放", Toast.LENGTH_SHORT).show() + /** 刷新适配器结果相关 */ + refreshAdapterResult() + /** 设置过滤按钮点击事件 */ + findViewById(R.id.config_title_filter).setOnClickListener { + showDialog { + title = "按条件过滤" + var editText: TextInputEditText + addView(R.layout.dia_icon_filter).apply { + editText = findViewById(R.id.dia_icon_filter_input_edit).apply { + requestFocus() + invalidate() + if (filterText.isNotBlank()) { + setText(filterText) + setSelection(filterText.length) + } + } + } + confirmButton { + if (editText.text.toString().isNotBlank()) { + filterText = editText.text.toString().trim() + onChanged?.invoke() + refreshAdapterResult() + } else { + Toast.makeText(applicationContext, "条件不能为空", Toast.LENGTH_SHORT).show() + it.performClick() + } + } + cancelButton() + if (filterText.isNotBlank()) + neutralButton(text = "清除条件") { + filterText = "" + onChanged?.invoke() + refreshAdapterResult() + } + } } /** 设置列表元素和 Adapter */ findViewById(R.id.config_list_view).apply { @@ -64,9 +103,9 @@ class ConfigureActivity : BaseActivity() { private val inflater = LayoutInflater.from(context) - override fun getCount() = IconPackParams.iconDatas.size + override fun getCount() = iconDatas.size - override fun getItem(position: Int) = IconPackParams.iconDatas[position] + override fun getItem(position: Int) = iconDatas[position] override fun getItemId(position: Int) = position.toLong() @@ -118,7 +157,7 @@ class ConfigureActivity : BaseActivity() { lateinit var switchOpen: MaterialSwitch lateinit var switchAll: MaterialSwitch } - } + }.apply { onChanged = { notifyDataSetChanged() } } } /** 设置点击事件 */ findViewById(R.id.config_cbr_button).setOnClickListener { @@ -134,4 +173,22 @@ class ConfigureActivity : BaseActivity() { } } } + + /** 刷新适配器结果相关 */ + private fun refreshAdapterResult() { + findViewById(R.id.config_title_count_text).text = + if (filterText.isBlank()) "已适配 ${iconDatas.size} 个 APP 的通知图标" + else "“${filterText}” 匹配到 ${iconDatas.size} 个结果" + findViewById(R.id.config_list_no_data_view).isVisible = iconDatas.isEmpty() + } + + /** + * 当前结果下的图标数组 + * @return [Array] + */ + private val iconDatas + get() = if (filterText.isBlank()) IconPackParams.iconDatas + else IconPackParams.iconDatas.filter { + it.appName.lowercase().contains(filterText.lowercase()) || it.packageName.lowercase().contains(filterText.lowercase()) + }.toTypedArray() } \ No newline at end of file diff --git a/app/src/main/java/com/fankes/miui/notify/utils/DialogBuilder.kt b/app/src/main/java/com/fankes/miui/notify/utils/DialogBuilder.kt index 605ca66..5b98646 100644 --- a/app/src/main/java/com/fankes/miui/notify/utils/DialogBuilder.kt +++ b/app/src/main/java/com/fankes/miui/notify/utils/DialogBuilder.kt @@ -20,7 +20,7 @@ * * This file is Created by fankes on 2022/1/7. */ -@file:Suppress("unused") +@file:Suppress("unused", "DEPRECATION") package com.fankes.miui.notify.utils @@ -28,7 +28,11 @@ import android.app.AlertDialog import android.content.Context import android.graphics.Color import android.graphics.drawable.GradientDrawable - +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import kotlin.math.round /** * 构造对话框 @@ -44,6 +48,8 @@ class DialogBuilder(private val context: Context) { private var instance: AlertDialog.Builder? = null // 实例对象 + private var customLayoutView: View? = null // 自定义布局 + init { instance = AlertDialog.Builder(context, android.R.style.Theme_Material_Light_Dialog) } @@ -65,6 +71,16 @@ class DialogBuilder(private val context: Context) { instance?.setMessage(value) } + /** + * 设置对话框自定义布局 + * @param resId 属性资源 Id + * @return [View] + */ + fun addView(resId: Int): View { + customLayoutView = LayoutInflater.from(context).inflate(resId, null) + return customLayoutView ?: error("Inflate $resId failed") + } + /** * 设置对话框确定按钮 * @param text 按钮文本内容 @@ -91,6 +107,9 @@ class DialogBuilder(private val context: Context) { /** 显示对话框 */ internal fun show() = instance?.create()?.apply { + val dm = DisplayMetrics() + (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.getMetrics(dm) + customLayoutView?.let { setView(it.apply { minimumWidth = round(dm.widthPixels / 1.3).toInt() }) } window?.setBackgroundDrawable(GradientDrawable( GradientDrawable.Orientation.TOP_BOTTOM, intArrayOf(Color.WHITE, Color.WHITE) diff --git a/app/src/main/res/layout/activity_config.xml b/app/src/main/res/layout/activity_config.xml index c782c84..0690891 100644 --- a/app/src/main/res/layout/activity_config.xml +++ b/app/src/main/res/layout/activity_config.xml @@ -26,11 +26,13 @@ android:layout_marginStart="10dp" android:layout_marginEnd="25dp" android:src="@mipmap/back" - android:tint="@color/colorTextGray" /> + android:tint="@color/colorTextGray" + android:tooltipText="返回" /> @@ -56,12 +58,13 @@ + android:src="@mipmap/icon_filter" + android:tint="@color/colorTextGray" + android:tooltipText="按条件过滤" /> - + android:layout_weight="1"> + + + + + @@ -359,7 +359,7 @@ android:layout_marginBottom="10dp" android:alpha="0.8" android:lineSpacingExtra="6dp" - android:text="Q.这个模块是如何诞生的?\nA.这个模块诞生来源于 MIUI 的乱改和不规范,本来 MIUI 9 之后,官方给出了原生通知图标样式,后面由于用户反应通知栏经常出现黑白块,这当然不是系统的错,而是国内 APP 极其不规范的通知图标设计,于是 MIUI 选择直接忽略这个问题把全部图标都改成了 APP 的彩色图标,使得之前拥有自有样式的原生图标也被破坏,通知中“setSmallIcon”不再有效,这个模块就是为了修复被 MIUI 开发组忽略的图标问题,并完美地给 MIUI 修复了黑白块图标的问题。" + android:text="Q.这个模块是如何诞生的?\nA.这个模块诞生来源于 MIUI 的乱改和不规范,本来 MIUI 9 之后,官方给出了原生通知图标样式,后面由于用户反应通知栏经常出现黑白块。\n这当然不是系统的错,而是国内 APP 和 MIPUSH 的通知极其不规范的通知图标设计。\n但是呢,接到反馈后 MIUI 开发组选择直接忽略这个问题,在 2021-5-18 的开发版开始,把全部通知图标都改成了 APP 的彩色图标,使得之前拥有自有样式的原生图标也被破坏。\n对于 Android 开发者来说,官方文档中的 “setSmallIcon” 不再适用于魔改后的 MIUI,这将会严重破坏非常多的状态图标。\n当然,国内的手机生态除了 MIPUSH 的营销通知就是社交软件的通知,可能大部分人都不会在意这件事情。\n但是,这个模块就是为了修复被 MIUI 开发组忽略的图标问题才诞生的,并完美地给 MIUI 修复了黑白块图标的问题。" android:textColor="@color/colorTextDark" android:textSize="12sp" /> @@ -435,7 +435,7 @@ android:layout_height="wrap_content" android:alpha="0.8" android:lineSpacingExtra="6dp" - android:text="本软件是免费开源项目,遵循 GPL 协议,你可以点击这里前往 Github 查看源码以及获取模块更新。\n严禁以任何形式贩卖、商用本软件,否则开发者有权追究其法律责任。" + android:text="本软件是免费开源项目,遵循 AGPL3.0 协议,你可以点击这里前往 Github 查看源码以及获取模块更新。" android:textColor="@color/colorTextDark" android:textSize="12sp" /> diff --git a/app/src/main/res/layout/dia_icon_filter.xml b/app/src/main/res/layout/dia_icon_filter.xml new file mode 100644 index 0000000..dfdbfbb --- /dev/null +++ b/app/src/main/res/layout/dia_icon_filter.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-xxhdpi/icon_search.png b/app/src/main/res/mipmap-xxhdpi/icon_search.png deleted file mode 100644 index 35ebe1d2a9da19b5b0f38632f66a93b9580e2ad3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5416 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