/* * 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; } } }