├── .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 |
--------------------------------------------------------------------------------