├── .gitignore ├── LICENSE ├── README.md ├── build.gradle └── src └── main ├── AndroidManifest.xml ├── java ├── com │ └── notxx │ │ └── icon │ │ ├── CachedSmallIconDecorator.kt │ │ ├── DirectSmallIconDecorator.kt │ │ ├── MainActivity.java │ │ └── SmallIconDecoratorBase.kt └── top │ └── trumeet │ └── common │ ├── cache │ ├── AbstractCacheAspect.java │ └── IconCache.kt │ └── utils │ └── ImgUtils.java └── res ├── drawable-anydpi └── default_notification_icon.xml ├── layout └── app_info.xml ├── values-v26 └── flags.xml ├── values-zh └── strings.xml └── values ├── flags.xml └── strings.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | build/ 3 | *.apk 4 | local.properties 5 | adb 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 女娲石 - 小图标通知优化 2 | 3 | 优化MIUI/EMUI推送的小图标,从五彩斑斓变成黑白分明 4 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.gradle_version = '3.3.2' 3 | ext.kotlin_version = '1.3.41' 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | 10 | dependencies { 11 | classpath "com.android.tools.build:gradle:$gradle_version" 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | 24 | if (! project.plugins.hasPlugin("com.android.feature")) apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply plugin: 'kotlin-android-extensions' 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | defaultConfig { 32 | minSdkVersion 26 33 | targetSdkVersion 28 34 | resConfigs "en", "zh" 35 | } 36 | 37 | compileOptions { 38 | sourceCompatibility JavaVersion.VERSION_1_8 39 | targetCompatibility JavaVersion.VERSION_1_8 40 | } 41 | 42 | signingConfigs { 43 | release { 44 | storeFile file(RELEASE_STORE_FILE) 45 | storePassword RELEASE_STORE_PASSWORD 46 | keyAlias RELEASE_KEY_ALIAS 47 | keyPassword RELEASE_KEY_PASSWORD 48 | } 49 | } 50 | 51 | buildTypes { 52 | release { 53 | signingConfig signingConfigs.release 54 | } 55 | } 56 | 57 | } 58 | 59 | dependencies { 60 | implementation 'com.oasisfeng.nevo:sdk:2.0.0-rc01' 61 | implementation 'com.android.support:support-compat:28.0.0' 62 | implementation 'com.android.support:palette-v7:28.0.0' 63 | } 64 | -------------------------------------------------------------------------------- /src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 20 | 27 | 28 | 35 | 36 | 37 | 38 | 39 | 40 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/main/java/com/notxx/icon/CachedSmallIconDecorator.kt: -------------------------------------------------------------------------------- 1 | package com.notxx.icon 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.content.pm.ApplicationInfo 6 | import android.content.pm.PackageManager 7 | import android.content.res.Resources 8 | import android.graphics.Color 9 | import android.graphics.drawable.Icon 10 | import android.os.Build 11 | import android.os.Bundle 12 | import android.provider.Settings 13 | import android.support.annotation.RequiresApi 14 | import android.util.Log 15 | 16 | import java.util.Optional 17 | 18 | import com.oasisfeng.nevo.sdk.MutableNotification 19 | import com.oasisfeng.nevo.sdk.MutableStatusBarNotification 20 | 21 | import top.trumeet.common.cache.IconCache 22 | import top.trumeet.common.utils.ImgUtils 23 | 24 | class CachedSmallIconDecorator:SmallIconDecoratorBase() { 25 | private lateinit var defIcon:Icon 26 | 27 | protected override fun onConnected() { 28 | // Log.d(T, "begin onConnected") 29 | defIcon = Icon.createWithResource(this, R.drawable.default_notification_icon) 30 | // Log.d(T, "defIcon " + defIcon) 31 | // Log.d(T, "end onConnected") 32 | } 33 | 34 | protected override fun applySmallIcon(evolving:MutableStatusBarNotification, n:MutableNotification) { 35 | var resources = getAppResources(RES_PACKAGE) 36 | val packageName = evolving.getPackageName() 37 | val appResources = getAppResources(packageName) 38 | val cache = IconCache.getInstance() 39 | var iconId:Int? 40 | var colorId:Int? 41 | val key = packageName.toLowerCase().replace(("\\.").toRegex(), "_") 42 | 43 | iconId = resources?.getIdentifier(key, "drawable", RES_PACKAGE) 44 | if (iconId != null && iconId != 0) { // has icon in icon-res 45 | // Log.d(T, "res $packageName iconId: $iconId") 46 | val ref = iconId 47 | val cached = cache.getIcon(this, packageName, 48 | { _, _ -> IconCache.render(resources!!.getDrawable(ref, null)) }) // TODO 49 | if (cached != null) { 50 | n.setSmallIcon(cached) 51 | } else { 52 | iconId = null 53 | } 54 | } 55 | colorId = resources?.getIdentifier(key, "string", RES_PACKAGE) 56 | if (colorId != null && colorId != 0) { // has color in icon-res 57 | // Log.d(T, "res colorId: " + colorId) 58 | n.color = Color.parseColor(resources!!.getString(colorId)) 59 | } 60 | if (iconId != null && iconId != 0) { // do nothing 61 | // Log.d(T, "do nothing $packageName iconId: $iconId") 62 | } else if (appResources != null) { 63 | // Log.d(T, "$packageName appResources: $appResources") 64 | iconId = appResources.getIdentifier(MIPUSH_SMALL_ICON, "drawable", packageName) 65 | if (iconId != 0) { // has embed icon 66 | // Log.d(T, "mipush_small $packageName iconId: $iconId") 67 | val cached = cache.getMiPushIcon(appResources, iconId, packageName) 68 | if (cached != null) { 69 | n.setSmallIcon(cached) 70 | } else { 71 | iconId = null 72 | } 73 | } 74 | if (iconId == null || iconId == 0) { // does not have icon 75 | // Log.d(T, "generate $packageName icon") 76 | val cached = cache.getIcon(this, packageName) 77 | if (cached != null) { 78 | n.setSmallIcon(cached) 79 | } else { 80 | n.setSmallIcon(defIcon) 81 | } 82 | } 83 | } 84 | if (colorId != null && colorId != 0) { // do nothing 85 | // Log.d(T, "do nothing $packageName colorId: $colorId") 86 | } else { 87 | // Log.d(T, "generate $packageName color") 88 | n.color = cache.getAppColor(this, packageName) 89 | } 90 | } 91 | 92 | companion object { 93 | @JvmField public val MIPUSH_SMALL_ICON = "mipush_small_notification" 94 | private val T = "CachedSmallIconDecorator" 95 | private val RES_PACKAGE = "com.notxx.icon.res" 96 | } 97 | } -------------------------------------------------------------------------------- /src/main/java/com/notxx/icon/DirectSmallIconDecorator.kt: -------------------------------------------------------------------------------- 1 | package com.notxx.icon 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Color 5 | import android.graphics.drawable.Icon 6 | 7 | import com.oasisfeng.nevo.sdk.MutableNotification 8 | import com.oasisfeng.nevo.sdk.MutableStatusBarNotification 9 | 10 | import top.trumeet.common.cache.IconCache 11 | 12 | class DirectSmallIconDecorator:SmallIconDecoratorBase() { 13 | protected override fun applySmallIcon(evolving:MutableStatusBarNotification, n:MutableNotification) { 14 | val original = n.getSmallIcon() 15 | if (original == null) return 16 | var bitmap = IconCache.render(original.loadDrawable(this)) 17 | val packageName = evolving.getPackageName() 18 | n.color = IconCache.backgroundColor(packageName, bitmap) 19 | bitmap = IconCache.whiten(this, bitmap) 20 | n.setSmallIcon(Icon.createWithBitmap(bitmap)) 21 | } 22 | 23 | companion object { 24 | @JvmStatic private val T = "DirectSmallIconDecorator" 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/com/notxx/icon/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.notxx.icon; 2 | 3 | import android.app.Activity; 4 | import android.app.Notification; 5 | import android.app.NotificationChannel; 6 | import android.app.NotificationManager; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.pm.ActivityInfo; 10 | import android.content.pm.PackageManager; 11 | import android.content.pm.ResolveInfo; 12 | import android.graphics.Bitmap; 13 | import android.graphics.Color; 14 | import android.graphics.drawable.Icon; 15 | import android.net.Uri; 16 | import android.os.Build; 17 | import android.os.Bundle; 18 | import android.util.Log; 19 | import android.view.View; 20 | import android.view.ViewGroup; 21 | import android.widget.AdapterView; 22 | import android.widget.BaseAdapter; 23 | import android.widget.ImageView; 24 | import android.widget.ListView; 25 | import android.widget.TextView; 26 | 27 | import java.util.List; 28 | import java.util.LinkedList; 29 | import java.util.Map; 30 | import java.util.SortedMap; 31 | import java.util.TreeMap; 32 | 33 | import top.trumeet.common.cache.IconCache; 34 | import top.trumeet.common.utils.ImgUtils; 35 | 36 | public class MainActivity extends Activity { 37 | private class PackagesAdapter extends BaseAdapter implements AdapterView.OnItemClickListener { 38 | private final IconCache cache = IconCache.getInstance(); 39 | private final PackageManager manager; 40 | private final List> infos = new LinkedList<>(); 41 | private final Context context; 42 | 43 | PackagesAdapter(PackageManager manager, Context context) { 44 | // manager 45 | this.manager = manager; 46 | // infos 47 | Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); 48 | mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); 49 | List query = manager.queryIntentActivities(mainIntent, 0); 50 | SortedMap map = new TreeMap<>(); 51 | for (ResolveInfo info : query) { 52 | ActivityInfo ai = info.activityInfo; 53 | CharSequence label = manager.getApplicationLabel(ai.applicationInfo); 54 | map.put(label, ai); 55 | } 56 | for (Map.Entry entry : map.entrySet()) { 57 | this.infos.add(entry); 58 | } 59 | this.context = context; 60 | } 61 | 62 | public void onItemClick(AdapterView parent, View v, int position, long id) { 63 | Map.Entry item = getItem(position); 64 | CharSequence label = item.getKey(); 65 | ActivityInfo info = item.getValue(); 66 | // Log.d("SmallIcon", "item click"); 67 | Notification.Builder n = new Notification.Builder(MainActivity.this, CHANNEL_ID) 68 | .setSmallIcon(R.drawable.default_notification_icon) 69 | .setContentTitle(label) 70 | .setContentText(info.packageName); 71 | Icon cached = cache.getIcon(MainActivity.this, info.packageName); 72 | if (cached != null) { n.setSmallIcon(cached); } 73 | int color = cache.getAppColor(MainActivity.this, info.packageName); 74 | if (color != -1) { 75 | n.setColor(color); 76 | n.setColorized(false); 77 | } 78 | NotificationManager notificationManager = getSystemService(NotificationManager.class); 79 | notificationManager.notify(notificationId++, n.build()); 80 | } 81 | 82 | @Override 83 | public int getCount() { 84 | return this.infos.size(); 85 | } 86 | 87 | @Override 88 | public Map.Entry getItem(int position) { 89 | return this.infos.get(position); 90 | } 91 | 92 | @Override 93 | public long getItemId(int position) { 94 | return this.infos.get(position).hashCode(); 95 | } 96 | 97 | @Override 98 | public View getView(int position, View convertView, ViewGroup parent) { 99 | View view = null; 100 | 101 | // 如果convertView不为空则复用 102 | if (convertView == null) { 103 | view = View.inflate(MainActivity.this, R.layout.app_info, null); 104 | }else { 105 | view = convertView; 106 | } 107 | 108 | Map.Entry item = getItem(position); 109 | CharSequence label = item.getKey(); 110 | ActivityInfo info = item.getValue(); 111 | // applicationLabel 112 | TextView appName = (TextView) view.findViewById(R.id.appName); 113 | appName.setText(label); 114 | // applicationIcon 115 | ImageView appIcon = (ImageView) view.findViewById(R.id.appIcon); 116 | appIcon.setImageDrawable(manager.getApplicationIcon(info.applicationInfo)); 117 | // applicationLogo 118 | ImageView appLogo = (ImageView) view.findViewById(R.id.appLogo); 119 | appLogo.setImageDrawable(manager.getApplicationLogo(info.applicationInfo)); 120 | // embedIcon 121 | ImageView embedIcon = (ImageView) view.findViewById(R.id.embed); 122 | // mipushIcon 123 | ImageView mipushIcon = (ImageView) view.findViewById(R.id.mipush); 124 | // background 125 | ImageView background = (ImageView) view.findViewById(R.id.background); 126 | // foreground 127 | ImageView foreground = (ImageView) view.findViewById(R.id.foreground); 128 | // whiten 129 | ImageView whiten = (ImageView) view.findViewById(R.id.whiten); 130 | // gen 131 | ImageView gen = (ImageView) view.findViewById(R.id.gen); 132 | try { 133 | final Context appContext = createPackageContext(info.packageName, 0); 134 | int iconId = 0, colorId = 0; 135 | // embedIcon 136 | final String key = info.packageName.toLowerCase().replaceAll("\\.", "_"); 137 | if (context != null && (iconId = context.getResources().getIdentifier(key, "drawable", RES_PACKAGE)) != 0) // has icon 138 | embedIcon.setImageIcon(Icon.createWithResource(RES_PACKAGE, iconId)); 139 | else 140 | embedIcon.setImageIcon(null); 141 | if (context != null && (colorId = context.getResources().getIdentifier(key, "string", RES_PACKAGE)) != 0) // has icon 142 | embedIcon.setColorFilter(Color.parseColor(context.getResources().getString(colorId))); 143 | // mipushIcon 144 | // Log.d("SmallIcon", "appResources: " + appContext.getResources()); 145 | if ((iconId = appContext.getResources().getIdentifier(CachedSmallIconDecorator.MIPUSH_SMALL_ICON, "drawable", info.packageName)) != 0) { // has icon 146 | // mipushIcon.setImageIcon(Icon.createWithResource(info.packageName, iconId)); 147 | // else if ((iconId = appContext.getResources().getIdentifier(NOTIFICATION_ICON, "drawable", info.packageName)) != 0) 148 | // mipushIcon.setImageIcon(Icon.createWithResource(info.packageName, iconId)); 149 | final int ref = iconId; 150 | mipushIcon.setImageIcon(cache.getMiPushIcon(appContext.getResources(), iconId, info.packageName)); 151 | } else 152 | mipushIcon.setImageIcon(null); 153 | mipushIcon.setColorFilter(cache.getAppColor(appContext, info.packageName)); 154 | // background 155 | Bitmap iconBackground = cache.getIconBackground(appContext, info.packageName); 156 | if (iconBackground != null) { 157 | // IconCache.removeBackground(iconBackground, Color.RED); // TODO 测试移除背景 158 | background.setImageBitmap(iconBackground); 159 | } else { 160 | background.setImageIcon(null); 161 | } 162 | // foreground & white 163 | Bitmap iconForeground = cache.getIconForeground(appContext, info.packageName); 164 | if (iconForeground != null) { 165 | foreground.setImageBitmap(iconForeground); 166 | whiten.setImageBitmap(IconCache.alphaize(info.packageName, iconForeground)); 167 | } else { 168 | foreground.setImageIcon(null); 169 | whiten.setImageIcon(null); 170 | } 171 | // gen 172 | Icon iconCache = cache.getIcon(MainActivity.this, info.packageName); 173 | if (iconCache != null) { 174 | gen.setImageIcon(iconCache); 175 | gen.setColorFilter(cache.getAppColor(appContext, info.packageName)); 176 | } else { 177 | gen.setImageIcon(null); 178 | } 179 | } catch (IllegalArgumentException | PackageManager.NameNotFoundException ign) { Log.d("inspect", "ex " + info.packageName);} 180 | return view; 181 | } 182 | } 183 | 184 | private static final String MIPUSH_SMALL_ICON = "mipush_small_notification"; 185 | private static final String RES_PACKAGE = "com.notxx.icon.res"; 186 | private static final String CHANNEL_ID = "test_channel"; 187 | 188 | private ListView listView; 189 | private int notificationId; 190 | 191 | private Context createPackageContext(String packageName) { 192 | try { 193 | return createPackageContext(packageName, 0); 194 | } catch (IllegalArgumentException | PackageManager.NameNotFoundException ign) { 195 | Log.d("inspect", "ex " + packageName); 196 | return null; 197 | } 198 | } 199 | 200 | @Override 201 | protected void onCreate(Bundle savedInstanceState) { 202 | super.onCreate(savedInstanceState); 203 | 204 | createNotificationChannel(); 205 | 206 | listView = new ListView(this); 207 | final PackageManager manager = getPackageManager(); 208 | final Context context = createPackageContext(RES_PACKAGE); 209 | PackagesAdapter adapter = new PackagesAdapter(manager, context); 210 | listView.setOnItemClickListener(adapter); 211 | listView.setAdapter(adapter); 212 | setContentView(listView); 213 | } 214 | 215 | private void createNotificationChannel() { 216 | // Create the NotificationChannel, but only on API 26+ because 217 | // the NotificationChannel class is new and not in the support library 218 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 219 | CharSequence name = getString(R.string.test_channel_name); 220 | String description = getString(R.string.test_channel_desc); 221 | int importance = NotificationManager.IMPORTANCE_DEFAULT; 222 | // Log.d("SmallIcon", "new NC " + name + description); 223 | NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); 224 | channel.setDescription(description); 225 | // Register the channel with the system; you can't change the importance 226 | // or other notification behaviors after this 227 | NotificationManager notificationManager = getSystemService(NotificationManager.class); 228 | // Log.d("SmallIcon", "create NC " + channel); 229 | notificationManager.createNotificationChannel(channel); 230 | } 231 | } 232 | } -------------------------------------------------------------------------------- /src/main/java/com/notxx/icon/SmallIconDecoratorBase.kt: -------------------------------------------------------------------------------- 1 | package com.notxx.icon 2 | 3 | import android.app.Notification 4 | import android.app.NotificationChannel 5 | import android.content.pm.ApplicationInfo 6 | import android.content.pm.PackageManager 7 | import android.content.res.Resources 8 | import android.graphics.Color 9 | import android.os.Build 10 | import android.os.Bundle 11 | import android.os.Process 12 | import android.provider.Settings 13 | import android.support.annotation.RequiresApi 14 | import android.util.Log 15 | 16 | import java.util.ArrayList 17 | import java.util.Optional 18 | 19 | import com.oasisfeng.nevo.sdk.MutableNotification 20 | import com.oasisfeng.nevo.sdk.MutableStatusBarNotification 21 | import com.oasisfeng.nevo.sdk.NevoDecoratorService 22 | 23 | abstract class SmallIconDecoratorBase:NevoDecoratorService() { 24 | protected fun getAppResources(appInfo:ApplicationInfo?):Resources? = getAppResources(appInfo?.packageName) 25 | 26 | protected fun getAppResources(packageName:String?):Resources? { 27 | if (packageName == null) return null 28 | try { 29 | return createPackageContext(packageName, 0)?.getResources() 30 | } catch (ign:PackageManager.NameNotFoundException) { 31 | return null 32 | } 33 | } 34 | 35 | protected fun applyBigText(n:MutableNotification, extras:Bundle) { 36 | val text = extras.getCharSequence(Notification.EXTRA_TEXT) 37 | if (text != null) { 38 | extras.putCharSequence(Notification.EXTRA_TITLE_BIG, extras.getCharSequence(Notification.EXTRA_TITLE)) 39 | extras.putCharSequence(Notification.EXTRA_BIG_TEXT, text) 40 | extras.putString(Notification.EXTRA_TEMPLATE, TEMPLATE_BIG_TEXT) 41 | } 42 | } 43 | 44 | protected fun applyChannel(evolving:MutableStatusBarNotification, n:MutableNotification, extras:Bundle) { 45 | val appInfo:ApplicationInfo? = extras.getParcelable("android.appInfo") 46 | val packageName = evolving.getPackageName() 47 | val appResources = getAppResources(packageName) 48 | val channelId = n.getChannelId() 49 | val labelRes = (appInfo?.labelRes) ?: 0 50 | val label = if ((labelRes == 0 || appResources == null)) appInfo?.nonLocalizedLabel.toString() else appResources.getString(labelRes) 51 | // Log.d(T, "label: " + label + " channel: " + channelId) 52 | val channel = getNotificationChannel(packageName, Process.myUserHandle(), channelId) 53 | if (channel == null) return 54 | val newId = "::" + packageName + "::" + channelId 55 | val newName = getString(R.string.decorator_channel_label, label, channel.getName()) 56 | // Log.d(T, "newId: " + newId + " newName: " + newName) 57 | val channels = ArrayList() 58 | channels.add(cloneChannel(channel, newId, newName)) 59 | createNotificationChannels(packageName, Process.myUserHandle(), channels) 60 | n.setChannelId(newId) 61 | // Log.d(T, "original extras " + extras) 62 | } 63 | 64 | protected abstract fun applySmallIcon(evolving:MutableStatusBarNotification, n:MutableNotification) 65 | 66 | protected override fun apply(evolving:MutableStatusBarNotification):Boolean { 67 | val n = evolving.getNotification() 68 | val extras = n.extras 69 | val phase = extras.getByte(EXTRAS_PHASE) 70 | // Log.d(T, "package name: " + packageName) 71 | // bigText 72 | if (phase < PHASE_BIG_TEXT && n.bigContentView == null) { 73 | Log.d(T, "begin modifying bigText") 74 | applyBigText(n, extras) 75 | extras.putByte(EXTRAS_PHASE, PHASE_BIG_TEXT) 76 | } else { 77 | Log.d(T, "skip modifying bigText") 78 | } 79 | // channel 80 | if (phase < PHASE_CHANNEL && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 81 | Log.d(T, "begin modifying channel") 82 | applyChannel(evolving, n, extras) 83 | extras.putByte(EXTRAS_PHASE, PHASE_CHANNEL) 84 | } else { 85 | Log.d(T, "skip modifying channel") 86 | } 87 | // smallIcon 88 | if (phase < PHASE_SMALL_ICON && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 89 | Log.d(T, "begin modifying smallIcon") 90 | applySmallIcon(evolving, n) 91 | extras.putByte(EXTRAS_PHASE, PHASE_SMALL_ICON) 92 | } else { 93 | Log.d(T, "skip modifying smallIcon") 94 | } 95 | Log.d(T, "end modifying") 96 | return true 97 | } 98 | 99 | @RequiresApi(Build.VERSION_CODES.O) private fun cloneChannel(channel:NotificationChannel, id:String, label:String):NotificationChannel { 100 | val clone = NotificationChannel(id, label, channel.getImportance()) 101 | clone.setGroup(channel.getGroup()) 102 | clone.setDescription(channel.getDescription()) 103 | clone.setLockscreenVisibility(channel.getLockscreenVisibility()) 104 | clone.setSound(Optional.ofNullable(channel.getSound()).orElse(Settings.System.DEFAULT_NOTIFICATION_URI), channel.getAudioAttributes()) 105 | clone.setBypassDnd(channel.canBypassDnd()) 106 | clone.setLightColor(channel.getLightColor()) 107 | clone.setShowBadge(channel.canShowBadge()) 108 | clone.setVibrationPattern(channel.getVibrationPattern()) 109 | return clone 110 | } 111 | 112 | companion object { 113 | private val T = "SmallIconDecoratorBase" 114 | 115 | @JvmStatic public val SIZE = 144 116 | 117 | protected val EXTRAS_PHASE = "nevo.smallIcon.phase" 118 | protected val PHASE_BIG_TEXT = 1.toByte() 119 | protected val PHASE_CHANNEL = 2.toByte() 120 | protected val PHASE_SMALL_ICON = 3.toByte() 121 | } 122 | } -------------------------------------------------------------------------------- /src/main/java/top/trumeet/common/cache/AbstractCacheAspect.java: -------------------------------------------------------------------------------- 1 | package top.trumeet.common.cache; 2 | 3 | import android.support.v4.util.LruCache; 4 | 5 | /** 6 | * @author zts 7 | */ 8 | abstract class AbstractCacheAspect { 9 | private LruCache cache; 10 | 11 | AbstractCacheAspect(LruCache cache) { 12 | this.cache = cache; 13 | } 14 | 15 | public T get(String cacheKey) { 16 | T cached = cache.get(cacheKey); 17 | if (cached == null) { 18 | cached = gen(); 19 | if (cached != null) { 20 | cache.put(cacheKey, cached); 21 | } 22 | } 23 | return cached; 24 | } 25 | 26 | /** 27 | * @return from DataSource 28 | */ 29 | abstract T gen(); 30 | } -------------------------------------------------------------------------------- /src/main/java/top/trumeet/common/cache/IconCache.kt: -------------------------------------------------------------------------------- 1 | package top.trumeet.common.cache 2 | 3 | import kotlin.math.max 4 | import kotlin.math.hypot 5 | 6 | import android.content.Context 7 | import android.content.res.Resources 8 | import android.graphics.Bitmap 9 | import android.graphics.Canvas 10 | import android.graphics.Color 11 | import android.graphics.Rect 12 | import android.graphics.drawable.AdaptiveIconDrawable 13 | import android.graphics.drawable.Drawable 14 | import android.graphics.drawable.Icon 15 | import android.support.v4.util.LruCache 16 | import android.util.Log 17 | 18 | import top.trumeet.common.utils.ImgUtils 19 | 20 | /** 21 | * Author: TimothyZhang023 22 | * Icon Cache 23 | * 24 | * Code implements port from 25 | * https://github.com/MiPushFramework/MiPushFramework 26 | */ 27 | class IconCache private constructor() { 28 | private val foregroundCache:LruCache 29 | private val backgroundCache:LruCache 30 | private val iconCache:LruCache 31 | private val mipushCache:LruCache 32 | private val appColorCache:LruCache 33 | 34 | init { 35 | foregroundCache = LruCache(100) 36 | backgroundCache = LruCache(100) 37 | iconCache = LruCache(100) 38 | mipushCache = LruCache(100) 39 | appColorCache = LruCache(100) 40 | //TODO check cacheSizes is correct ? 41 | } 42 | 43 | /** 获取图标前景 */ 44 | fun getIconForeground(ctx:Context, pkg:String):Bitmap? { 45 | return object:AbstractCacheAspect(foregroundCache) { 46 | override fun gen():Bitmap? { 47 | try { 48 | // Log.d(T, "foreground $pkg") 49 | val icon = ctx.getPackageManager().getApplicationIcon(pkg) 50 | var bitmap:Bitmap? 51 | if (icon is AdaptiveIconDrawable) { 52 | // Log.d(T, "foreground $pkg $SIZE") 53 | val recommand = { width:Int -> if (width > 0) width * 72 / 108 else 72 } 54 | val slice = { width:Int -> if (width > 0) width * -18 / 72 else -18 } 55 | bitmap = render(icon.getForeground(), recommand, recommand, 56 | setBounds = { d, width, height -> val w = slice(width); val h = slice(height); d.setBounds(w, h, width - w, height - h) }) 57 | } else { 58 | // Log.d(T, "legacy foreground $pkg $SIZE ${icon.getIntrinsicWidth()}, ${icon.getIntrinsicHeight()}") 59 | bitmap = render(icon) 60 | } 61 | val width = bitmap.getWidth(); val height = bitmap.getHeight() 62 | val pixels = IntArray(width * height) 63 | bitmap.getPixels(pixels, 0, width, 0, 0, width, height) 64 | removeBackground(pkg, pixels, width, height) 65 | bitmap.setPixels(pixels, 0, width, 0, 0, width, height) 66 | var top = height; var bottom = 0 67 | var left = width; var right = 0 68 | top@ for (i in 0 until height) { 69 | for (j in 0 until width) { 70 | val pos = width * i + j // 偏移 71 | val pixel = pixels[pos] // 颜色值 72 | if (pixel != Color.TRANSPARENT) { top = i; break@top } 73 | } 74 | } 75 | bottom@ for (i in height - 1 downTo 0) { 76 | for (j in 0 until width) { 77 | val pos = width * i + j // 偏移 78 | val pixel = pixels[pos] // 颜色值 79 | if (pixel != Color.TRANSPARENT) { bottom = i; break@bottom } 80 | } 81 | } 82 | left@ for (j in 0 until width) { 83 | for (i in 0 until height) { 84 | val pos = width * i + j // 偏移 85 | val pixel = pixels[pos] // 颜色值 86 | if (pixel != Color.TRANSPARENT) { left = j; break@left } 87 | } 88 | } 89 | right@ for (j in width - 1 downTo 0) { 90 | for (i in 0 until height) { 91 | val pos = width * i + j // 偏移 92 | val pixel = pixels[pos] // 颜色值 93 | if (pixel != Color.TRANSPARENT) { right = j; break@right } 94 | } 95 | } 96 | // if (pkg == "com.apple.android.music") { 97 | // Log.d(T, "l,r,t,b = $left,$right,$top,$bottom") 98 | // } 99 | if ((left != 0 || right != width - 1 || top != 0 || bottom != height - 1) && 100 | (left < right && top < bottom)) { 101 | val w = right - left; val h = bottom - top 102 | val side = max(w, h) 103 | val l = left - (side - w) / 2 104 | val t = top - (side - h) / 2 105 | val temp = Bitmap.createBitmap(side, side, Bitmap.Config.ARGB_8888); 106 | val canvas = Canvas(temp); 107 | canvas.drawBitmap(bitmap, Rect(l, t, l + side, t + side), Rect(0, 0, side, side), null) 108 | bitmap.recycle() 109 | bitmap = temp 110 | } 111 | return bitmap 112 | } catch (ignored:Exception) { 113 | Log.d(T, "foreground", ignored) 114 | return null 115 | } 116 | } 117 | }.get(pkg) 118 | } 119 | 120 | /** 获取图标背景 */ 121 | fun getIconBackground(ctx:Context, pkg:String):Bitmap? { 122 | return object:AbstractCacheAspect(backgroundCache) { 123 | override fun gen():Bitmap? { 124 | try { 125 | val icon = ctx.getPackageManager().getApplicationIcon(pkg) 126 | return if (icon is AdaptiveIconDrawable) { 127 | val recommand = { width:Int -> if (width > 0) width * 72 / 108 else 72 } 128 | val slice = { width:Int -> if (width > 0) width * -18 / 72 else -18 } 129 | render(icon.getBackground(), recommand, recommand, 130 | setBounds = { d, width, height -> val w = slice(width); val h = slice(height); d.setBounds(w, h, width - w, height - h) }) 131 | } else { 132 | render(icon) 133 | } 134 | } catch (ignored:Exception) { 135 | Log.d(T, "background", ignored) 136 | return null 137 | } 138 | } 139 | }.get(pkg) 140 | } 141 | 142 | @JvmOverloads fun getIcon(ctx:Context, pkg:String, 143 | gen:((Context, String) -> Bitmap?) = ({ ctx, pkg -> getIconForeground(ctx, pkg) }), 144 | whiten:((Context, Bitmap?) -> Bitmap?) = ({ _, b -> alphaize(pkg, b) }), 145 | iconize:((Context, Bitmap?) -> Icon?) = ({ _, b -> (if (b != null) Icon.createWithBitmap(b) else null) })):Icon? { 146 | return object:AbstractCacheAspect(iconCache) { 147 | override fun gen():Icon? { 148 | var bitmap = gen(ctx, pkg) 149 | if (bitmap == null) { return null } 150 | bitmap = whiten(ctx, bitmap) 151 | return iconize(ctx, bitmap) 152 | } 153 | }.get(pkg) 154 | } 155 | 156 | @JvmOverloads fun getMiPushIcon(resources:Resources, iconId:Int, pkg:String, 157 | gen:((Resources, Int) -> Bitmap?) = { resources, iconId -> render(resources.getDrawable(iconId, null)) }, 158 | whiten:((Bitmap?) -> Bitmap?) = { b -> alphaize(pkg, b) }, 159 | iconize:((Bitmap?) -> Icon?) = { b -> (if (b != null) Icon.createWithBitmap(b) else null) }):Icon? { 160 | return object:AbstractCacheAspect(mipushCache) { 161 | override fun gen():Icon? { 162 | var bitmap = gen(resources, iconId) 163 | if (bitmap == null) { return null } 164 | bitmap = whiten(bitmap) 165 | val icon = iconize(bitmap) 166 | // Log.d(T, "icon: $icon") 167 | return icon 168 | } 169 | }.get(pkg) 170 | } 171 | 172 | @JvmOverloads fun getAppColor(ctx:Context, pkg:String, 173 | convert:((Context, Bitmap?) -> Int) = { _, b -> backgroundColor(pkg, b)}):Int { 174 | return object:AbstractCacheAspect(appColorCache) { 175 | override fun gen():Int { 176 | val background = getIconBackground(ctx, pkg) 177 | if (background == null) { 178 | return -1 179 | } 180 | return convert(ctx, background) 181 | } 182 | }.get(pkg) 183 | } 184 | 185 | private object Holder { 186 | val instance = IconCache() 187 | } 188 | 189 | companion object { 190 | private val T = "SmallIcon" 191 | @JvmField public val SIZE = 144 // 建议的边长 192 | private val ADAPTIVE_CANVAS = Rect(-18, -18, 90, 90) // TODO density 193 | @JvmStatic public val BOUNDS = Rect(0, 0, 72, 72) // TODO density 194 | 195 | @JvmStatic fun getInstance():IconCache { 196 | return Holder.instance 197 | } 198 | 199 | /** 200 | * 转换Drawable为Bitmap 201 | * 202 | * @param drawable 203 | * 204 | * @return 205 | */ 206 | @JvmStatic fun render(drawable:Drawable, 207 | recommandWidth:((Int) -> Int) = { width -> if (width > 0) width else SIZE }, 208 | recommandHeight:((Int) -> Int) = { height -> if (height > 0) height else SIZE }, 209 | createBitmap:((Int, Int) -> Bitmap) = { width, height -> Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) }, 210 | setBounds:((Drawable, Int, Int) -> Unit) = { d, width, height -> d.setBounds(0, 0, width, height) }):Bitmap { 211 | var width = recommandWidth(drawable.getIntrinsicWidth()) 212 | var height = recommandHeight(drawable.getIntrinsicHeight()) 213 | val bitmap = createBitmap(width, height) 214 | val canvas = Canvas(bitmap) 215 | setBounds(drawable, width, height) 216 | drawable.draw(canvas) 217 | return bitmap 218 | } 219 | 220 | @JvmStatic fun whitenBitmap(ctx:Context, b:Bitmap?):Bitmap? { 221 | if (b == null) { return null } 222 | return whiten(ctx, b) 223 | } 224 | 225 | @JvmStatic fun whiten(ctx:Context, b:Bitmap):Bitmap { 226 | val density = ctx.getResources().getDisplayMetrics().density 227 | return ImgUtils.convertToTransparentAndWhite(b, density) 228 | } 229 | 230 | val ALPHA_MAX:Short = 0xFF 231 | val ALPHA_MIN:Short = 0x3F 232 | // 将图像灰度化然后转化为透明度形式 233 | @JvmStatic @JvmOverloads fun alphaize(pkg:String, bitmap:Bitmap?, autoLevel:Boolean = true):Bitmap? { 234 | if (bitmap == null) { return null } 235 | 236 | val width = bitmap.getWidth() 237 | val height = bitmap.getHeight() 238 | val temp = bitmap.copy(Bitmap.Config.ARGB_8888, true) 239 | try { 240 | val pixels = IntArray(width * height) 241 | temp.getPixels(pixels, 0, width, 0, 0, width, height) 242 | 243 | val alphas = ShortArray(width * height) 244 | val map = mutableMapOf() 245 | for (pos in 0 until pixels.size) { 246 | val pixel = pixels[pos] // 颜色值 247 | val alpha = ((pixel.toLong() and 0xFF000000) shr 24).toInt() // 透明度通道 248 | if (alpha == 0) continue 249 | val red = ((pixel and 0x00FF0000) shr 16).toFloat() // 红色通道 250 | val green = ((pixel and 0x0000FF00) shr 8).toFloat() // 绿色通道 251 | val blue = (pixel and 0x000000FF).toFloat() // 蓝色通道 252 | var gray = (alpha * (red * 0.3 + green * 0.59 + blue * 0.11) / 0xFF).toShort() // 混合为明亮度 253 | if (gray == 0.toShort()) gray = 1 // 强制加1,避免与透明背景混同 254 | alphas[pos] = gray 255 | if (map.containsKey(gray)) { 256 | val count = map[gray] 257 | if (count != null) map[gray] = count + 1 258 | } else { 259 | map[gray] = 1 260 | } 261 | } 262 | if (autoLevel) { 263 | val threshold = alphas.size * 0.01 264 | val filtered = map.filter { it.value > threshold } 265 | val max = filtered.maxBy { it.key }; val min = map.minBy { it.key } 266 | // if (pkg.startsWith("com.lastpass")) { 267 | // Log.d(T, "autoLevel $pkg max,min = $max, $min") 268 | // Log.d(T, "autoLevel $pkg threshold,map,filtered = $threshold, ${map.size}, ${filtered.size}") 269 | // } 270 | if (max != null && min != null && max.key <= ALPHA_MAX) { 271 | if (max.key > min.key) { 272 | val q = (ALPHA_MAX - ALPHA_MIN).toFloat() / (max.key - min.key) 273 | // if (pkg.startsWith("com.lastpass")) { 274 | // Log.d(T, "autoLevel $pkg q = $q, ${(max.key - min.key) * q + min.key}") 275 | // } 276 | for (key in map.keys) { 277 | map[key] = if (key >= max.key) { 278 | ALPHA_MAX.toInt() 279 | } else if (key >= min.key) { 280 | ((key - min.key) * q + ALPHA_MIN).toInt() 281 | } else { 282 | ALPHA_MIN.toInt() 283 | } 284 | } 285 | } else { 286 | for (key in map.keys) { 287 | map[key] = if (key >= max.key) { 288 | ALPHA_MAX.toInt() 289 | } else { 290 | ALPHA_MIN.toInt() 291 | } 292 | } 293 | } 294 | } 295 | } 296 | // if (pkg.startsWith("com.lastpass")) { 297 | // Log.d(T, "autoLevel $pkg map = $map") 298 | // } 299 | for (pos in 0 until pixels.size) { 300 | val alpha = alphas[pos] 301 | val a = map[alpha] ?: 0 302 | pixels[pos] = ((a shl 24) or 0xFFFFFF) // 以明亮度作为透明度 303 | } 304 | 305 | val r = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 306 | r.setPixels(pixels, 0, width, 0, 0, width, height) 307 | return r 308 | } finally { temp.recycle() } 309 | } 310 | 311 | // 混合颜色 312 | fun blend(pixels:IntArray) { 313 | for (pos in 0 until pixels.size) { 314 | val pixel = pixels[pos] // 颜色值 315 | if (pixel == Color.TRANSPARENT) continue 316 | val alpha = ((pixel.toLong() and 0xFF000000) shr 24).toFloat() // 透明度通道 317 | val red = ((pixel and 0x00FF0000) shr 16).toFloat() // 红色通道 318 | val green = ((pixel and 0x0000FF00) shr 8).toFloat() // 绿色通道 319 | val blue = (pixel and 0x000000FF).toFloat() // 蓝色通道 320 | pixels[pos] = (0xFF000000 or // 透明度 321 | ((red * alpha / 0xFF + (0xFF - alpha)).toLong() shl 16) or 322 | ((green * alpha / 0xFF + (0xFF - alpha)).toLong() shl 8) or 323 | (blue * alpha / 0xFF + (0xFF - alpha)).toLong()).toInt() 324 | } 325 | } 326 | 327 | fun blend(pixel:Int, background:Int):Int { 328 | if (pixel == Color.TRANSPARENT) return background 329 | val alpha = ((pixel.toLong() and 0xFF000000) shr 24) // 透明度通道 330 | // val a1 = ((background.toLong() and 0xFF000000) shr 24) // 透明度通道 331 | val r0 = ((pixel and 0x00FF0000) shr 16) // 红色通道 332 | val r1 = ((background and 0x00FF0000) shr 16) // 红色通道 333 | val g0 = ((pixel and 0x0000FF00) shr 8) // 绿色通道 334 | val g1 = ((background and 0x0000FF00) shr 8) // 绿色通道 335 | val b0 = (pixel and 0x000000FF) // 蓝色通道 336 | val b1 = (background and 0x000000FF) // 蓝色通道 337 | return (0xFF000000 or // 透明度 338 | (((r0 * alpha + r1 * (0xFF - alpha)) / 0xFF).toLong() shl 16) or 339 | (((g0 * alpha + g1 * (0xFF - alpha)) / 0xFF).toLong() shl 8) or 340 | ((b0 * alpha + b1 * (0xFF - alpha)) / 0xFF).toLong()).toInt() 341 | } 342 | 343 | @JvmStatic fun removeBackground(pkg:String, pixels:IntArray, width:Int, height:Int, dest:Int = Color.TRANSPARENT):Boolean { 344 | // blend(pixels) 345 | 346 | // 方形 347 | // val lt = bitmap.getPixel(0, 0); val rt = bitmap.getPixel(width - 1, 0) 348 | // val lb = bitmap.getPixel(0, height - 1); val rb = bitmap.getPixel(width - 1, height - 1) 349 | // if ((lt == rt) && (rt == lb) && (lb == rb) && (lt != dest)) { // 四角颜色一致 350 | // // Log.d(T, "removeBackground1($pixels, $width, $height, ${Integer.toHexString(lt)}, ${Integer.toHexString(dest)})") 351 | // // floodFill(pixels, width, height, lt, dest) 352 | // removeColor(pixels, lt, dest) 353 | // bitmap.setPixels(pixels, 0, width, 0, 0, width, height) 354 | // return false 355 | // } 356 | 357 | // 圆形 358 | // pixels.fill(0xFFFFFFFF.toInt()) 359 | // // val hypot = hypot(x - cx, y - cy) 360 | // // if (outside < hypot || inside > hypot ) continue 361 | val outside = width.toFloat() / 2; val inside = width.toFloat() / 4 362 | assert(outside > inside) 363 | // circularFill(pixels, width, height) { dx, dy, pixel -> 364 | // val hypot = hypot(dx, dy) 365 | // // Log.d(T, "hypot = $hypot dx,dy = $dx, $dy") 366 | // (outside < hypot || inside > hypot) 367 | // } 368 | val map = mutableMapOf() 369 | val total = circularScan(pixels, width, height, map) { dx, dy, pixel -> 370 | val hypot = hypot(dx, dy) 371 | pixel != Color.TRANSPARENT || outside < hypot || inside > hypot 372 | } 373 | // Log.d(T, "$pkg circularScan() ${map.size} / $total") 374 | if (map.size > 1) { 375 | val maxBy = map.maxBy { it.value } 376 | if (maxBy != null) { 377 | // maxBy?.key 378 | // Log.d(T, "$pkg circularScan() ${Integer.toHexString(maxBy!!.key)} = ${maxBy!!.value} ${maxBy!!.value.toFloat() / total}") 379 | val q = maxBy.value.toFloat() / total 380 | if (q > 0.2 && q < 0.8) { 381 | // Log.d(T, "$pkg removeBackground1($pixels, ${Integer.toHexString(maxBy.key)}, ${Integer.toHexString(dest)})") 382 | removeColor(pkg, pixels, maxBy.key, dest) // TODO 考虑改用播种加洪泛式的颜色移除 383 | return true 384 | } 385 | } 386 | } 387 | 388 | return false 389 | } 390 | 391 | // 环状填充 392 | fun circularFill(pixels:IntArray, width:Int, height:Int, included:(Float,Float,Int) -> Boolean) { 393 | Log.d(T, "circularFill(width,height = $width, $height)") 394 | 395 | val cx = width.toFloat() / 2; val cy = height.toFloat() / 2 396 | for (y in 0 until height) { 397 | for (x in 0 until width) { 398 | val pos = width * y + x // 偏移 399 | if (!included(x - cx, y - cy, pixels[pos])) continue 400 | pixels[pos] = 0xFFFF0000.toInt() 401 | } 402 | } 403 | } 404 | 405 | // 环状扫描 406 | fun circularScan(pixels:IntArray, width:Int, height:Int, colorMap:MutableMap, included:(Float,Float,Int) -> Boolean):Int { 407 | // Log.d(T, "circularScan(width,height = $width, $height)") 408 | 409 | val cx = width.toFloat() / 2; val cy = height.toFloat() / 2; var count = 0 410 | for (y in 0 until height) { 411 | for (x in 0 until width) { 412 | val pos = width * y + x // 偏移 413 | val pixel = pixels[pos] 414 | if (!included(x - cx, y - cy, pixel)) continue 415 | count++ 416 | if (colorMap.containsKey(pixel)) { 417 | var count = colorMap[pixel] 418 | if (count != null) { colorMap[pixel] = count + 1 } 419 | } else { 420 | colorMap[pixel] = 1 421 | } 422 | } 423 | } 424 | return count 425 | } 426 | 427 | private val STATE_SWING:Byte = 0.toByte() // 未决 428 | private val STATE_KEEP:Byte = 1.toByte() // 保留 429 | private val STATE_KEEPR:Byte = 2.toByte() // 保留 430 | private val STATE_REMOVE:Byte = 0x11.toByte() // 移除 431 | private val STATE_REMOVER:Byte = 0x12.toByte() // 移除 432 | 433 | // 从四角开始洪泛法移除相同颜色 434 | fun floodFill(pixels:IntArray, width:Int, height:Int, target:Int, dest:Int = Color.TRANSPARENT) { 435 | val states = ByteArray(width * height) 436 | states.fill(STATE_SWING) 437 | 438 | // pixels[0] = pixels[width - 1] = pixels[width * (height - 1)] = pixels[width * (height - 1) + width - 1] = Color.TRANSPARENT 439 | states[0] = STATE_REMOVE // 种子 440 | states[width - 1] = STATE_REMOVE // 种子 441 | states[width * (height - 1)] = STATE_REMOVE // 种子 442 | states[width * (height - 1) + width - 1] = STATE_REMOVE // 种子 443 | for (y in 0 until height) { // 纵向播种 444 | val pos0 = width * y // 偏移0 445 | if (pixels[pos0] == target) { states[pos0] = STATE_REMOVE } 446 | val pos1 = width * y + width - 1 // 偏移1 447 | if (pixels[pos1] == target) { states[pos1] = STATE_REMOVE } 448 | } 449 | for (pos in 0 until states.size) { // 正向填充 450 | if ((pos % width) + 1 == width) continue // 避免行末出错 451 | val pixel = pixels[pos]; val state = states[pos] 452 | val np = pixels[pos + 1]; val ns = states[pos + 1] 453 | if ((pixel == np) && (ns == STATE_SWING) && (state == STATE_REMOVE || state == STATE_REMOVER)) { 454 | states[pos + 1] = STATE_REMOVE 455 | } 456 | } 457 | for (pos in states.size - 1 downTo 0) { // 反向填充 458 | if ((pos % width) == 0) continue // 避免行首出错 459 | val pixel = pixels[pos]; val state = states[pos] 460 | val np = pixels[pos - 1]; val ns = states[pos - 1] 461 | if ((pixel == np) && (ns == STATE_SWING) && (state == STATE_REMOVE || state == STATE_REMOVER)) { 462 | states[pos - 1] = STATE_REMOVE 463 | } 464 | } 465 | // for (y in 0 until height) { 466 | // for (x in 0 until width) { 467 | // val pos = width * y + x // 偏移 468 | // val state = states[pos] // 状态 469 | // val pixel = pixels[pos] // 颜色值 470 | // } 471 | // } 472 | for (pos in 0 until states.size) { 473 | if (states[pos] == STATE_REMOVE) { 474 | pixels[pos] = dest 475 | } 476 | } 477 | } 478 | 479 | fun _h(_int:Int) = Integer.toHexString(_int) 480 | 481 | // 颜色容差 482 | private val DIFF = 1 shl 13 483 | // 直接把特定颜色移除 484 | fun removeColor(pkg:String, pixels:IntArray, target:Int, dest:Int = Color.TRANSPARENT) { 485 | val r = ((target and 0x00FF0000) shr 16) // 红色 486 | val g = ((target and 0x0000FF00) shr 8) // 绿色 487 | val b = (target and 0x000000FF) // 蓝色 488 | for (pos in 0 until pixels.size) { // 正向填充 489 | var pixel = pixels[pos] 490 | val alpha = ((pixel.toLong() and 0xFF000000) ushr 24).toInt() // 透明度通道 491 | if (alpha == 0) continue 492 | pixel = blend(pixel, target) 493 | val dr = ((pixel and 0x00FF0000) shr 16) - r // 红色差异 494 | val dg = ((pixel and 0x0000FF00) shr 8) - g // 绿色差异 495 | val db = (pixel and 0x000000FF) - b // 蓝色差异 496 | val diff = dr * dr + dg * dg + db * db 497 | // if (pkg == "com.apple.android.music" && diff > DIFF) { 498 | // Log.d(T, "$pkg $pos $dr($r) $dg($g) $db($b) ${_h(pixel)} $diff") 499 | // } 500 | if (diff <= DIFF) { 501 | pixels[pos] = dest 502 | } 503 | } 504 | } 505 | 506 | @JvmStatic fun backgroundColor(pkg:String, bitmap:Bitmap?):Int { 507 | if (bitmap == null) { return Color.BLACK } 508 | 509 | val width = bitmap.getWidth(); val height = bitmap.getHeight() 510 | val temp = bitmap.copy(Bitmap.Config.ARGB_8888, true) 511 | try { 512 | val pixels = IntArray(width * height) 513 | temp.getPixels(pixels, 0, width, 0, 0, width, height) 514 | val map = mutableMapOf() 515 | for (pos in 0 until pixels.size) { 516 | val pixel = pixels[pos] 517 | val alpha = ((pixel.toLong() and 0xFF000000) shr 24).toInt() // 透明度通道 518 | if (alpha == 0) continue 519 | val red = ((pixel and 0x00FF0000) shr 16).toFloat() // 红色通道 520 | val green = ((pixel and 0x0000FF00) shr 8).toFloat() // 绿色通道 521 | val blue = (pixel and 0x000000FF).toFloat() // 蓝色通道 522 | if (red == green && green == blue) continue 523 | val rgb = pixel and 0xFFFFFF // RGB颜色值 524 | if (map.containsKey(rgb)) { 525 | val count = map[rgb] 526 | if (count != null) map[rgb] = count + 1 527 | } else { 528 | map[rgb] = 1 529 | } 530 | } 531 | val filtered = map.filter { it.key != 0 && it.key != 0xFFFFFF } // 预先剔除黑色和白色 532 | // if (pkg.startsWith("com.apple")) { 533 | // val view = map.filter { it.value > 10 }.map { "${_h(it.key)} = ${it.value}" } 534 | // Log.d(T, "backgroundColor filtered $view") 535 | // } 536 | val max = filtered.maxBy { it.value } // 获得最多的颜色 537 | return if (max != null) { 538 | if (pkg.startsWith("com.apple")) { Log.d(T, "backgroundColor max ${_h(max.key)} = ${max.value}") } 539 | (max.key.toLong() or 0xFF000000).toInt() 540 | } else { Color.BLACK } 541 | } finally { temp.recycle() } 542 | } 543 | } 544 | } -------------------------------------------------------------------------------- /src/main/java/top/trumeet/common/utils/ImgUtils.java: -------------------------------------------------------------------------------- 1 | package top.trumeet.common.utils; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Rect; 7 | import android.graphics.Matrix; 8 | import android.graphics.drawable.BitmapDrawable; 9 | import android.graphics.drawable.ColorDrawable; 10 | import android.graphics.drawable.Drawable; 11 | import android.graphics.drawable.InsetDrawable; 12 | import android.graphics.drawable.VectorDrawable; 13 | import android.util.Log; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Collections; 17 | 18 | /** 19 | * Code implements port from 20 | * https://www.cnblogs.com/Imageshop/p/3307308.html 21 | * AND 22 | * http://imagej.net/Auto_Threshold 23 | * 24 | * @author zts 25 | */ 26 | public class ImgUtils { 27 | private static int NUM_256 = 256; 28 | 29 | /** 30 | * 把图片切掉一圈. 31 | * 32 | * @param color 33 | * @param width 34 | * @param height 35 | * @param pixels 36 | * @param rExpand 37 | */ 38 | private static void clipImgToCircle(int color, int width, int height, int[] pixels, int rExpand) { 39 | 40 | int r = (width < height ? width : height) / 2 + rExpand; 41 | 42 | for (int i = 0; i < height; i++) { 43 | for (int j = 0; j < width; j++) { 44 | 45 | if ((i - width / 2) * (i - width / 2) + (j - height / 2) * (j - height / 2) > r * r) { 46 | pixels[width * i + j] = color; 47 | } 48 | 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * 把图片转换为二值(白和透明) 55 | */ 56 | public static Bitmap convertToTransparentAndWhite(Bitmap bitmap, float density) { 57 | int r = (int) (-8 * density); 58 | int width = bitmap.getWidth(); 59 | int height = bitmap.getHeight(); 60 | int[] pixels = new int[width * height]; 61 | bitmap.getPixels(pixels, 0, width, 0, 0, width, height); 62 | int calculateThreshold = calculateThreshold(pixels, width, height, r); 63 | 64 | int whiteCnt = 0; 65 | int blackCnt = 0; 66 | 67 | // clipImgToCircle(Color.TRANSPARENT, width, height, pixels, r); 68 | 69 | for (int i = 0; i < height; i++) { 70 | for (int j = 0; j < width; j++) { 71 | if (pixels[width * i + j] == Color.TRANSPARENT) { 72 | // blackCnt++; 73 | continue; 74 | } 75 | int dot = pixels[width * i + j]; 76 | int red = ((dot & 0x00FF0000) >> 16); 77 | int green = ((dot & 0x0000FF00) >> 8); 78 | int blue = (dot & 0x000000FF); 79 | int gray = (int) ((float) red * 0.3 + (float) green * 0.59 + (float) blue * 0.11); 80 | 81 | if (gray > calculateThreshold) { 82 | pixels[width * i + j] = Color.BLACK; 83 | blackCnt++; 84 | } else { 85 | pixels[width * i + j] = Color.WHITE; 86 | whiteCnt++; 87 | } 88 | } 89 | } 90 | 91 | // clipImgToCircle(Color.TRANSPARENT, width, height, pixels, r); 92 | 93 | if (whiteCnt > blackCnt) { 94 | // WHITE => TRANSPARENT 95 | for (int i = 0; i < height; i++) { 96 | for (int j = 0; j < width; j++) { 97 | int dot = pixels[width * i + j]; 98 | if (dot == Color.WHITE) { 99 | pixels[width * i + j] = Color.TRANSPARENT; 100 | } else if (dot == Color.BLACK) { 101 | pixels[width * i + j] = Color.WHITE; 102 | } 103 | } 104 | } 105 | } else { 106 | // BLACK => TRANSPARENT 107 | for (int i = 0; i < height; i++) { 108 | for (int j = 0; j < width; j++) { 109 | int dot = pixels[width * i + j]; 110 | if (dot == Color.BLACK) { 111 | pixels[width * i + j] = Color.TRANSPARENT; 112 | // } else if (dot == Color.WHITE) { 113 | // pixels[width * i + j] = Color.WHITE; 114 | } 115 | } 116 | } 117 | } 118 | 119 | clipImgToCircle(Color.TRANSPARENT, width, height, pixels, r); 120 | 121 | //todo use bwareaopen 122 | // denoiseWhitePoint(width, height, pixels, (int)density); 123 | 124 | int top = 0; 125 | int left = 0; 126 | int right = 0; 127 | int bottom = 0; 128 | 129 | for (int h = 0; h < height; h++) { 130 | boolean holdBlackPix = false; 131 | for (int w = 0; w < width; w++) { 132 | if (pixels[width * h + w] != Color.TRANSPARENT) { 133 | holdBlackPix = true; 134 | break; 135 | } 136 | } 137 | 138 | if (holdBlackPix) { 139 | break; 140 | } 141 | top++; 142 | } 143 | 144 | for (int w = 0; w < width; w++) { 145 | boolean holdBlackPix = false; 146 | for (int h = 0; h < height; h++) { 147 | if (pixels[width * h + w] != Color.TRANSPARENT) { 148 | holdBlackPix = true; 149 | break; 150 | } 151 | } 152 | if (holdBlackPix) { 153 | break; 154 | } 155 | left++; 156 | } 157 | 158 | for (int w = width - 1; w >= left; w--) { 159 | boolean holdBlackPix = false; 160 | for (int h = 0; h < height; h++) { 161 | if (pixels[width * h + w] != Color.TRANSPARENT) { 162 | holdBlackPix = true; 163 | break; 164 | } 165 | } 166 | if (holdBlackPix) { 167 | break; 168 | } 169 | right++; 170 | } 171 | 172 | for (int h = height - 1; h >= top; h--) { 173 | boolean holdBlackPix = false; 174 | for (int w = 0; w < width; w++) { 175 | if (pixels[width * h + w] != Color.TRANSPARENT) { 176 | holdBlackPix = true; 177 | break; 178 | } 179 | } 180 | if (holdBlackPix) { 181 | break; 182 | } 183 | bottom++; 184 | } 185 | 186 | int diff = (bottom + top) - (left + right); 187 | if (diff > 0) { 188 | bottom -= (diff / 2); 189 | top -= (diff / 2); 190 | 191 | bottom = bottom < 0 ? 0 : bottom; 192 | top = top < 0 ? 0 : top; 193 | 194 | } else if (diff < 0) { 195 | left += (diff / 2); 196 | right += (diff / 2); 197 | left = left < 0 ? 0 : left; 198 | right = right < 0 ? 0 : right; 199 | } 200 | 201 | 202 | int cropHeight = height - bottom - top; 203 | int cropWidth = width - left - right; 204 | 205 | int padding = (cropHeight + cropWidth) / 16; 206 | 207 | int[] newPix = new int[cropWidth * cropHeight]; 208 | 209 | int i = 0; 210 | for (int h = top; h < top + cropHeight; h++) { 211 | for (int w = left; w < left + cropWidth; w++) { 212 | newPix[i++] = pixels[width * h + w]; 213 | } 214 | } 215 | 216 | try { 217 | Bitmap newBmp = Bitmap.createBitmap(cropWidth + padding * 2, cropHeight + padding * 2, Bitmap.Config.ARGB_8888); 218 | newBmp.setPixels(newPix, 0, cropWidth, padding, padding, cropWidth, cropHeight); 219 | 220 | return newBmp; 221 | } catch (java.lang.IllegalArgumentException ex) { 222 | Log.d("SmallIcon", "width, height " + width + ", " + height + " " + diff); 223 | Log.d("SmallIcon", width + " " + left + " " + right); 224 | Log.d("SmallIcon", height + " " + bottom + " " + top); 225 | Log.d("SmallIcon", width + ", " + height + " " + cropWidth + ", " + cropHeight + " " + padding); 226 | return null; 227 | } 228 | } 229 | 230 | /** 231 | * 去噪点 232 | * 233 | * @param width 234 | * @param height 235 | * @param pixels 236 | * @param exThre 237 | */ 238 | private static void denoiseWhitePoint(int width, int height, int[] pixels, int exThre) { 239 | for (int i = 1; i < height - 1; i++) { 240 | for (int j = 1; j < width - 1; j++) { 241 | int[] dots = new int[]{ 242 | getPixel(width, pixels, i - 1, j - 1), 243 | getPixel(width, pixels, i - 1, j), 244 | getPixel(width, pixels, i - 1, j + 1), 245 | getPixel(width, pixels, i, j - 1), 246 | // pixels[width * i + j], 247 | getPixel(width, pixels, i, j + 1), 248 | getPixel(width, pixels, i + 1, j - 1), 249 | getPixel(width, pixels, i + 1, j), 250 | getPixel(width, pixels, i + 1, j + 1)}; 251 | 252 | int whCnt = 0; 253 | int trCnt = 0; 254 | 255 | for (int dot : dots) { 256 | if (dot == Color.WHITE) { 257 | whCnt++; 258 | } else { 259 | trCnt++; 260 | } 261 | } 262 | 263 | if (trCnt > (dots.length - exThre)) { 264 | pixels[width * i + j] = Color.TRANSPARENT; 265 | } 266 | } 267 | } 268 | } 269 | 270 | /** 271 | * 获取像素池中的一个像素 272 | */ 273 | private static int getPixel(int width, int[] pixels, int i, int j) { 274 | return pixels[width * i + j]; 275 | } 276 | 277 | /** 278 | * 获取灰度直方图 279 | * 280 | * @param pixels 281 | * @param width 282 | * @param height 283 | * @param histogram 284 | */ 285 | private static void getGreyHistogram(int[] pixels, int width, int height, int[] histogram) { 286 | for (int x = 0; x < width; x++) { 287 | for (int y = 0; y < height; y++) { 288 | int dot = pixels[width * y + x]; 289 | int alpha = ((dot & 0xFF000000) >> 24); 290 | if (alpha == 0xFF) continue; 291 | int red = ((dot & 0x00FF0000) >> 16); 292 | int green = ((dot & 0x0000FF00) >> 8); 293 | int blue = (dot & 0x000000FF); 294 | int gray = (int) ((float) red * 0.3 + (float) green * 0.59 + (float) blue * 0.11); 295 | histogram[gray]++; 296 | } 297 | } 298 | } 299 | 300 | /** 301 | * 直方图是否是双峰 302 | * 303 | * @param histogram 304 | * @return 305 | */ 306 | private static boolean isDimodal(double[] histogram) { 307 | // 对直方图的峰进行计数,只有峰数位2才为双峰 308 | int count = 0; 309 | for (int i = 1; i < (NUM_256 - 1); i++) { 310 | if (histogram[i - 1] < histogram[i] && histogram[i + 1] < histogram[i]) { 311 | count++; 312 | if (count > 2) { 313 | return false; 314 | } 315 | } 316 | } 317 | return count == 2; 318 | } 319 | 320 | /** 321 | * 计算阈值 322 | * 323 | * @param pixels 324 | * @param width 325 | * @param height 326 | * @param rExpand 327 | * @return 328 | */ 329 | private static int calculateThreshold(int[] pixels, int width, int height, int rExpand) { 330 | 331 | clipImgToCircle(Color.TRANSPARENT, width, height, pixels, rExpand); 332 | 333 | int[] histogram = new int[NUM_256]; 334 | getGreyHistogram(pixels, width, height, histogram); 335 | 336 | ArrayList thresholds = new ArrayList<>(); 337 | thresholds.add(calculateThresholdByOSTU(width * height, histogram)); 338 | thresholds.add(calculateThresholdByMinimum(histogram)); 339 | thresholds.add(calculateThresholdByMean(histogram)); 340 | 341 | Collections.sort(thresholds); 342 | 343 | return (thresholds.get(thresholds.size() - 1) * 3 + thresholds.get(thresholds.size() - 2)) / 4; 344 | } 345 | 346 | /** 347 | * OSTU法计算阈值 348 | * 349 | * @param total 350 | * @param histogram 351 | * @return 352 | */ 353 | private static int calculateThresholdByOSTU(int total, int[] histogram) { 354 | 355 | double sum = 0; 356 | for (int i = 0; i < NUM_256; i++) { 357 | sum += i * histogram[i]; 358 | } 359 | 360 | double sumB = 0; 361 | int wB = 0; 362 | 363 | double varMax = 0; 364 | int threshold = 0; 365 | 366 | for (int i = 0; i < NUM_256; i++) { 367 | wB += histogram[i]; 368 | if (wB == 0) { 369 | continue; 370 | } 371 | int wF = total - wB; 372 | 373 | if (wF == 0) { 374 | break; 375 | } 376 | 377 | sumB += (double) (i * histogram[i]); 378 | double mB = sumB / wB; 379 | double mF = (sum - sumB) / wF; 380 | 381 | double varBetween = (double) wB * (double) wF * (mB - mF) * (mB - mF); 382 | 383 | if (varBetween > varMax) { 384 | varMax = varBetween; 385 | threshold = i; 386 | } 387 | } 388 | 389 | return threshold; 390 | } 391 | 392 | /** 393 | * 最小值法计算阈值 394 | */ 395 | private static int calculateThresholdByMinimum(int[] histogram) { 396 | 397 | int y, iter = 0; 398 | double[] histgramc = new double[NUM_256]; 399 | double[] histgramcc = new double[NUM_256]; 400 | for (y = 0; y < NUM_256; y++) { 401 | histgramc[y] = histogram[y]; 402 | histgramcc[y] = histogram[y]; 403 | } 404 | 405 | while (!isDimodal(histgramcc)) { 406 | histgramcc[0] = (histgramc[0] + histgramc[0] + histgramc[1]) / 3; 407 | for (y = 1; y < (NUM_256 - 1); y++) { 408 | histgramcc[y] = (histgramc[y - 1] + histgramc[y] + histgramc[y + 1]) / 3; 409 | } 410 | histgramcc[255] = (histgramc[254] + histgramc[255] + histgramc[255]) / 3; 411 | System.arraycopy(histgramcc, 0, histgramc, 0, NUM_256); 412 | iter++; 413 | if (iter >= 1000) { 414 | return -1; 415 | } 416 | } 417 | // 阈值极为两峰之间的最小值 418 | boolean peakFound = false; 419 | for (y = 1; y < (NUM_256 - 1); y++) { 420 | if (histgramcc[y - 1] < histgramcc[y] && histgramcc[y + 1] < histgramcc[y]) { 421 | peakFound = true; 422 | } 423 | if (peakFound && histgramcc[y - 1] >= histgramcc[y] && histgramcc[y + 1] >= histgramcc[y]) { 424 | return y - 1; 425 | } 426 | } 427 | return -1; 428 | } 429 | 430 | /** 431 | * 平均值法计算阈值 432 | * @param histogram 433 | * @return 434 | */ 435 | private static int calculateThresholdByMean(int[] histogram) { 436 | 437 | int sum = 0, amount = 0; 438 | for (int i = 0; i < NUM_256; i++) { 439 | amount += histogram[i]; 440 | sum += i * histogram[i]; 441 | } 442 | return sum / amount; 443 | } 444 | 445 | /** 446 | * 缩放Bitmap 447 | */ 448 | public static Bitmap scaleImage(Bitmap bitmap, Rect dest, boolean recycle) { 449 | if (bitmap == null) { 450 | return null; 451 | } 452 | int w = bitmap.getWidth(), 453 | h = bitmap.getHeight(), 454 | width = dest.width(), 455 | height = dest.height(); 456 | if (dest.left == 0 && dest.top == 0 && w == width && h == height) { return bitmap; } 457 | Log.d("SmallIcon", "scale dest " + dest + " " + width + ", " + height + " " + w + ", " + h); 458 | float scaleWidth = ((float) width) / w; 459 | float scaleHeight = ((float) height) / h; 460 | Log.d("SmallIcon", "scale " + scaleWidth + ", " + scaleHeight); 461 | Matrix matrix = new Matrix(); 462 | matrix.postScale(scaleWidth, scaleHeight); 463 | Bitmap r = Bitmap.createBitmap(bitmap, dest.left, dest.top, w, h, matrix, true); 464 | if (recycle && !bitmap.isRecycled()) { 465 | bitmap.recycle(); 466 | } 467 | Log.d("SmallIcon", "scaled " + r.getWidth() + ", " + r.getHeight()); 468 | return r; 469 | } 470 | 471 | 472 | } 473 | -------------------------------------------------------------------------------- /src/main/res/drawable-anydpi/default_notification_icon.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/res/layout/app_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 23 | 24 | 29 | 30 | 35 | 36 | 41 | 42 | 47 | 48 | 53 | 54 | 59 | 60 | 65 | 66 | -------------------------------------------------------------------------------- /src/main/res/values-v26/flags.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | -------------------------------------------------------------------------------- /src/main/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 女娲石 - 小图标通知优化 20 | 优化MIUI/EMUI推送的小图标,从五彩斑斓变成黑白分明 21 | 小图标通知优化 (有缓存) 22 | 优化MIUI/EMUI推送的小图标,从五彩斑斓变成黑白分明 23 | 小图标通知优化 (无缓存) 24 | 优化MIUI/EMUI推送的小图标,从五彩斑斓变成黑白分明 25 | %1$s·%2$s 26 | 测试渠道 27 | 用这个渠道发通知来测试小图标 28 | 29 | -------------------------------------------------------------------------------- /src/main/res/values/flags.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | -------------------------------------------------------------------------------- /src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | nevo-decorator-icon 20 | make small-icon of MiPush/HWPush great again... 21 | nevo-decorator-icon (with cache) 22 | make small-icon of MiPush/HWPush great again... 23 | nevo-decorator-icon (without cache) 24 | make small-icon of MiPush/HWPush great again... 25 | %1$s - %2$s 26 | Test Channel 27 | Send notification in this channel to test small icon 28 | 29 | --------------------------------------------------------------------------------