/
Extractor.java
301 lines (264 loc) · 10.7 KB
/
Extractor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
/*
* Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package com.achep.acdisplay.notifications;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.TextAppearanceSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RemoteViews;
import android.widget.TextView;
import com.achep.acdisplay.R;
import com.achep.base.Device;
import java.lang.reflect.Field;
import java.util.ArrayList;
/**
* Created by Artem on 04.03.14.
*/
final class Extractor {
private static final String TAG = "Extractor";
/**
* Removes all kinds of multiple spaces from given string.
*/
@Nullable
private static String removeSpaces(@Nullable CharSequence cs) {
if (cs == null) return null;
String string = cs instanceof String
? (String) cs : cs.toString();
return string
.replaceAll("(\\s+$|^\\s+)", "")
.replaceAll("\n+", "\n");
}
/**
* Removes both {@link ForegroundColorSpan} and {@link BackgroundColorSpan} from given string.
*/
@Nullable
private static CharSequence removeColorSpans(@Nullable CharSequence cs) {
if (cs == null) return null;
if (cs instanceof Spanned) {
cs = new SpannableStringBuilder(cs);
}
if (cs instanceof Spannable) {
CharacterStyle[] styles;
Spannable spanned = (Spannable) cs;
styles = spanned.getSpans(0, spanned.length(), TextAppearanceSpan.class);
for (CharacterStyle style : styles) spanned.removeSpan(style);
styles = spanned.getSpans(0, spanned.length(), ForegroundColorSpan.class);
for (CharacterStyle style : styles) spanned.removeSpan(style);
styles = spanned.getSpans(0, spanned.length(), BackgroundColorSpan.class);
for (CharacterStyle style : styles) spanned.removeSpan(style);
}
return cs;
}
@SuppressLint("InlinedApi")
public void loadTexts(@NonNull Context context, @NonNull OpenNotification n) {
final Notification notification = n.getNotification();
final Bundle extras = getExtras(notification);
if (extras != null) loadFromExtras(n, extras);
if (TextUtils.isEmpty(n.titleText)
&& TextUtils.isEmpty(n.titleBigText)
&& TextUtils.isEmpty(n.messageText)
&& n.messageTextLines == null) {
loadFromView(context, n);
}
}
/**
* Gets a bundle with additional data from notification.
*/
@Nullable
@SuppressLint("NewApi")
private Bundle getExtras(@NonNull Notification notification) {
if (Device.hasKitKatApi()) {
return notification.extras;
}
// Access extras using reflections.
try {
Field field = notification.getClass().getDeclaredField("extras");
field.setAccessible(true);
return (Bundle) field.get(notification);
} catch (Exception e) {
Log.w(TAG, "Failed to access extras on Jelly Bean.");
return null;
}
}
@Nullable
private CharSequence[] doIt(@Nullable CharSequence[] lines) {
if (lines != null) {
// Filter empty lines.
ArrayList<CharSequence> list = new ArrayList<>();
for (CharSequence msg : lines) {
msg = removeSpaces(msg);
if (!TextUtils.isEmpty(msg)) {
list.add(removeColorSpans(msg));
}
}
// Create new array.
if (list.size() > 0) {
return list.toArray(new CharSequence[list.size()]);
}
}
return null;
}
//-- LOADING FROM EXTRAS --------------------------------------------------
/**
* Loads all possible texts from given {@link Notification#extras extras}.
*
* @param extras extras to load from
*/
@SuppressLint("InlinedApi")
private void loadFromExtras(@NonNull OpenNotification n, @NonNull Bundle extras) {
n.titleBigText = extras.getCharSequence(Notification.EXTRA_TITLE_BIG);
n.titleText = extras.getCharSequence(Notification.EXTRA_TITLE);
n.infoText = extras.getCharSequence(Notification.EXTRA_INFO_TEXT);
n.subText = extras.getCharSequence(Notification.EXTRA_SUB_TEXT);
n.summaryText = extras.getCharSequence(Notification.EXTRA_SUMMARY_TEXT);
n.messageBigText = removeColorSpans(extras.getCharSequence(Notification.EXTRA_BIG_TEXT));
n.messageText = removeColorSpans(extras.getCharSequence(Notification.EXTRA_TEXT));
CharSequence[] lines = extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
n.messageTextLines = doIt(lines);
}
//-- LOADING FROM VIEWS ---------------------------------------------------
private void loadFromView(@NonNull Context context, @NonNull OpenNotification n) {
ViewGroup view;
{
final Context contextNotify = NotificationUtils.createContext(context, n);
if (contextNotify == null) return;
final Notification notification = n.getNotification();
final RemoteViews rvs = notification.bigContentView == null
? notification.contentView
: notification.bigContentView;
// Try to load the view from remote views.
LayoutInflater inflater = (LayoutInflater) contextNotify.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
try {
view = (ViewGroup) inflater.inflate(rvs.getLayoutId(), null);
rvs.reapply(contextNotify, view);
} catch (Exception e) {
return;
}
}
ArrayList<TextView> textViews = new RecursiveFinder<>(TextView.class).expand(view);
removeClickableViews(textViews);
removeSubtextViews(context, textViews);
removeActionViews(n.getActions(), textViews);
// No views
if (textViews.size() == 0)
return;
TextView title = findTitleTextView(textViews);
textViews.remove(title); // no need of title view anymore
n.titleText = title.getText();
// No views
if (textViews.size() == 0)
return;
// Pull all other texts and merge them.
int length = textViews.size();
CharSequence[] messages = new CharSequence[length];
for (int i = 0; i < length; i++) messages[i] = textViews.get(i).getText();
n.messageTextLines = doIt(messages);
}
private void removeActionViews(@Nullable Action[] actions,
@NonNull ArrayList<TextView> textViews) {
if (actions == null) {
return;
}
for (Action action : actions) {
for (int i = textViews.size() - 1; i >= 0; i--) {
CharSequence text = textViews.get(i).getText();
if (text != null && text.equals(action.title)) {
textViews.remove(i);
break;
}
}
}
}
private void removeClickableViews(@NonNull ArrayList<TextView> textViews) {
for (int i = textViews.size() - 1; i >= 0; i--) {
TextView child = textViews.get(i);
if (child.isClickable() || child.getVisibility() != View.VISIBLE) {
textViews.remove(i);
break;
}
}
}
private void removeSubtextViews(@NonNull Context context,
@NonNull ArrayList<TextView> textViews) {
float subtextSize = context.getResources().getDimension(R.dimen.notification_subtext_size);
for (int i = textViews.size() - 1; i >= 0; i--) {
final TextView child = textViews.get(i);
final String text = child.getText().toString();
if (child.getTextSize() == subtextSize
// empty textviews
|| text.matches("^(\\s*|)$")
// clock textviews
|| text.matches("^\\d{1,2}:\\d{1,2}(\\s?\\w{2}|)$")) {
textViews.remove(i);
}
}
}
@NonNull
private TextView findTitleTextView(@NonNull ArrayList<TextView> textViews) {
// The idea is that title text is the
// largest one.
TextView largest = null;
for (TextView textView : textViews) {
if (largest == null || textView.getTextSize() > largest.getTextSize()) {
largest = textView;
}
}
assert largest != null; // cause the count of views is always >= 1
return largest;
}
private static class RecursiveFinder<T extends View> {
private final ArrayList<T> list;
private final Class<T> clazz;
public RecursiveFinder(@NonNull Class<T> clazz) {
this.list = new ArrayList<>();
this.clazz = clazz;
}
public ArrayList<T> expand(@NonNull ViewGroup viewGroup) {
int offset = 0;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = viewGroup.getChildAt(i + offset);
if (child == null) {
continue;
}
if (clazz.isAssignableFrom(child.getClass())) {
//noinspection unchecked
list.add((T) child);
} else if (child instanceof ViewGroup) {
expand((ViewGroup) child);
}
}
return list;
}
}
}