/*
* 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.annotation.TargetApi;
import android.app.Notification;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.SystemClock;
import android.service.notification.StatusBarNotification;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.graphics.Palette;
import android.text.TextUtils;
import android.util.Log;
import com.achep.acdisplay.Config;
import com.achep.acdisplay.utils.BitmapUtils;
import com.achep.base.Device;
import com.achep.base.async.AsyncTask;
import com.achep.base.interfaces.IOnLowMemory;
import com.achep.base.interfaces.ISubscriptable;
import com.achep.base.tests.Check;
import com.achep.base.utils.Operator;
import com.achep.base.utils.PackageUtils;
import com.achep.base.utils.smiley.SmileyParser;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import static com.achep.base.Build.DEBUG;
/**
* @author Artem Chepurnoy
*/
public abstract class OpenNotification implements
ISubscriptable<OpenNotification.OnNotificationDataChangedListener>,
IOnLowMemory {
private static final String TAG = "OpenNotification";
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
@NonNull
static OpenNotification newInstance(@NonNull StatusBarNotification sbn) {
Notification n = sbn.getNotification();
if (Device.hasLollipopApi()) {
return new OpenNotificationLollipop(sbn, n);
} else if (Device.hasKitKatWatchApi()) {
return new OpenNotificationKitKatWatch(sbn, n);
}
return new OpenNotificationJellyBeanMR2(sbn, n);
}
@NonNull
public static OpenNotification newInstance(@NonNull Notification n) {
if (Device.hasJellyBeanMR2Api()) {
throw new RuntimeException("You must use the StatusBarNotification!");
}
return new OpenNotificationJellyBean(n);
}
//-- DEBUG ----------------------------------------------------------------
/* Only for debug purposes! */
private final Object dFinalizeWatcher = DEBUG ? new Object() {
/**
* Logs the notifications' removal.
*/
@Override
protected void finalize() throws Throwable {
try {
Log.d(TAG, "Removing the notification[recycled=" + isRecycled()
+ "] from the heap: " + OpenNotification.this);
} finally {
super.finalize();
}
}
} : null;
//-- BEGIN ----------------------------------------------------------------
/**
* Notification visibility: Show this notification in its entirety on all lockscreens.
* <p>
* {@see #getVisibility()}
*/
public static final int VISIBILITY_PUBLIC = 1;
/**
* Notification visibility: Show this notification on all lockscreens, but conceal sensitive or
* private information on secure lockscreens.
* <p>
* {@see #getVisibility()}
*/
public static final int VISIBILITY_PRIVATE = 0;
/**
* Notification visibility: Do not reveal any part of this notification on a secure lockscreen.
* <p>
* {@see #getVisibility()}
*/
public static final int VISIBILITY_SECRET = -1;
// Events
public static final int EVENT_ICON = 1;
public static final int EVENT_READ = 2;
public static final int EVENT_BACKGROUND = 3;
public static final int EVENT_BRAND_COLOR = 4;
@Nullable
private final StatusBarNotification mStatusBarNotification;
@NonNull
private final Notification mNotification;
@Nullable
private Action[] mActions;
private boolean mEmoticonsEnabled;
private boolean mMine;
private boolean mRead;
private boolean mRecycled;
private long mLoadedTimestamp;
private int mNumber;
// Extracted
@Nullable
public CharSequence titleBigText;
@Nullable
public CharSequence titleText;
@Nullable
public CharSequence messageBigText;
private CharSequence messageBigTextOrigin;
@Nullable
public CharSequence messageText;
private CharSequence messageTextOrigin;
@Nullable
public CharSequence[] messageTextLines;
private CharSequence[] messageTextLinesOrigin;
@Nullable
public CharSequence infoText;
@Nullable
public CharSequence subText;
@Nullable
public CharSequence summaryText;
// Notification icon.
@Nullable
private Bitmap mIconBitmap;
@Nullable
private static WeakReference<IconFactory> sIconFactoryRef;
private IconFactory mIconFactory;
@NonNull
private final IconFactory.IconAsyncListener mIconCallback =
new IconFactory.IconAsyncListener() {
@Override
public void onGenerated(@NonNull Bitmap bitmap) {
mIconFactory = null;
setIcon(bitmap);
}
};
// Dynamic background.
@Nullable
private Bitmap mBackgroundBitmap;
@Nullable
private static WeakReference<IconFactory> sBackgroundFactoryRef;
private IconFactory mBackgroundFactory;
@NonNull
private final BackgroundFactory.BackgroundAsyncListener mBackgroundCallback =
new BackgroundFactory.BackgroundAsyncListener() {
@Override
public void onGenerated(@NonNull Bitmap bitmap) {
mBackgroundFactory = null;
setBackground(bitmap);
}
};
// Brand color.
private int mBrandColor = Color.WHITE;
@Nullable
private android.os.AsyncTask<Bitmap, Void, Palette> mPaletteWorker;
// Listeners
@NonNull
private final ArrayList<OnNotificationDataChangedListener> mListeners = new ArrayList<>(3);
protected OpenNotification(@Nullable StatusBarNotification sbn, @NonNull Notification n) {
mStatusBarNotification = sbn;
mNotification = n;
}
public void load(@NonNull Context context) {
mLoadedTimestamp = SystemClock.elapsedRealtime();
mMine = TextUtils.equals(getPackageName(), PackageUtils.getName(context));
mActions = Action.makeFor(mNotification);
mNumber = mNotification.number;
// Load the brand color.
loadBrandColor(context);
// Load notification icon.
if (sIconFactoryRef == null || (mIconFactory = sIconFactoryRef.get()) == null) {
sIconFactoryRef = new WeakReference<>(mIconFactory = new IconFactory());
}
mIconFactory.remove(this);
mIconFactory.add(context, this, mIconCallback);
// Load all other things, such as title text, message text
// and more and more.
new Extractor().loadTexts(context, this);
messageText = ensureNotEmpty(messageText);
messageBigText = ensureNotEmpty(messageBigText);
messageTextOrigin = messageText;
messageBigTextOrigin = messageBigText;
messageTextLinesOrigin = messageTextLines == null ? null : messageTextLines.clone();
// Initially load emoticons.
if (mEmoticonsEnabled) {
mEmoticonsEnabled = false;
setEmoticonsEnabled(true);
}
}
/**
* {@inheritDoc}
*/
@Override
public void onLowMemory() {
}
@Nullable
private CharSequence ensureNotEmpty(@Nullable CharSequence cs) {
return TextUtils.isEmpty(cs) ? null : cs;
}
/**
* @return The {@link android.service.notification.StatusBarNotification} or
* {@code null}.
*/
@Nullable
public StatusBarNotification getStatusBarNotification() {
return mStatusBarNotification;
}
/**
* @return The {@link Notification} supplied to
* {@link android.app.NotificationManager#notify(int, Notification)}.
*/
@NonNull
public Notification getNotification() {
return mNotification;
}
/**
* Array of all {@link Action} structures attached to this notification.
*/
@Nullable
public Action[] getActions() {
return mActions;
}
@Nullable
public Bitmap getBackground() {
return mBackgroundBitmap;
}
@Nullable
public Bitmap getIcon() {
return mIconBitmap;
}
/**
* The number of events that this notification represents. For example, in a new mail
* notification, this could be the number of unread messages.
* <p>
* The system may or may not use this field to modify the appearance of the notification. For
* example, before {@link android.os.Build.VERSION_CODES#HONEYCOMB}, this number was
* superimposed over the icon in the status bar. Starting with
* {@link android.os.Build.VERSION_CODES#HONEYCOMB}, the template used by
* {@link Notification.Builder} has displayed the number in the expanded notification view.
* <p>
* If the number is 0 or negative, it is never shown.
*/
public int getNumber() {
return mNumber;
}
/**
* Sphere of visibility of this notification, which affects how and when the SystemUI reveals
* the notification's presence and contents in untrusted situations (namely, on the secure
* lockscreen).
* <p>
* The default level, {@link #VISIBILITY_PRIVATE}, behaves exactly as notifications have always
* done on Android: The notification's {@link #getIcon()} (if available) are
* shown in all situations, but the contents are only available if the device is unlocked for
* the appropriate user.
* <p>
* A more permissive policy can be expressed by {@link #VISIBILITY_PUBLIC}; such a notification
* can be read even in an "insecure" context (that is, above a secure lockscreen).
* <p>
* Finally, a notification can be made {@link #VISIBILITY_SECRET}, which will suppress its icon
* and ticker until the user has bypassed the lockscreen.
*/
public int getVisibility() {
return VISIBILITY_PRIVATE;
}
/**
* @return {@code true} if user has seen the notification,
* {@code false} otherwise.
* @see #markAsRead()
* @see #setRead(boolean)
*/
public boolean isRead() {
return mRead;
}
//-- COMPARING INSTANCES --------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return String.format("OpenNotification(pkg=%s, g_key=%s, g_summary=%b, g_child=%b)",
getPackageName(), getGroupKey(), isGroupSummary(), isGroupChild());
}
/**
* {@inheritDoc}
*/
@Override
public abstract int hashCode();
/**
* {@inheritDoc}
*/
@SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
@Override
public abstract boolean equals(Object o);
/**
* Note, that method does not equals with {@link #equals(Object)} method.
*
* @param n notification to compare with.
* @return {@code true} if notifications are from the same source and will
* be handled by system as same notifications, {@code false} otherwise.
*/
@SuppressLint("NewApi")
@SuppressWarnings("ConstantConditions")
public abstract boolean hasIdenticalIds(@Nullable OpenNotification n);
//-- NOTIFICATION DATA ----------------------------------------------------
/**
* Interface definition for a callback to be invoked
* when date of notification is changed.
*/
public interface OnNotificationDataChangedListener {
/**
* @see #EVENT_BACKGROUND
* @see #EVENT_ICON
* @see #EVENT_READ
*/
void onNotificationDataChanged(@NonNull OpenNotification notification, int event);
}
/**
* {@inheritDoc}
*/
@Override
public void registerListener(@NonNull OnNotificationDataChangedListener listener) {
mListeners.add(listener);
}
/**
* {@inheritDoc}
*/
@Override
public void unregisterListener(@NonNull OnNotificationDataChangedListener listener) {
mListeners.remove(listener);
}
/**
* Notifies all listeners about this event.
*
* @see com.achep.acdisplay.notifications.OpenNotification.OnNotificationDataChangedListener
* @see #registerListener(com.achep.acdisplay.notifications.OpenNotification.OnNotificationDataChangedListener)
*/
private void notifyListeners(int event) {
for (OnNotificationDataChangedListener listener : mListeners) {
listener.onNotificationDataChanged(this, event);
}
}
private void setIcon(@Nullable Bitmap bitmap) {
if (mIconBitmap == (mIconBitmap = bitmap)) return;
notifyListeners(EVENT_ICON);
}
//-- BRAND COLOR ----------------------------------------------------------
protected void setBrandColor(int color) {
if (mBrandColor == (mBrandColor = color)) return;
notifyListeners(EVENT_BRAND_COLOR);
}
protected void loadBrandColor(@NonNull Context context) {
try {
String packageName = getPackageName();
Drawable appIcon;
try {
appIcon = context.getPackageManager().getApplicationIcon(packageName);
} catch (OutOfMemoryError e) {
Log.e(TAG, "Failed to get application\'s icon due to OutOfMemoryError!");
return;
}
final Bitmap bitmap = Bitmap.createBitmap(
appIcon.getMinimumWidth(),
appIcon.getMinimumHeight(),
Bitmap.Config.ARGB_4444);
if (bitmap == null) {
// This had happened on somewhat strange
// chinese phone.
return;
}
appIcon.draw(new Canvas(bitmap));
AsyncTask.stop(mPaletteWorker);
mPaletteWorker = new Palette.Builder(bitmap)
.maximumColorCount(16)
.generate(new Palette.PaletteAsyncListener() {
@Override
public void onGenerated(Palette palette) {
setBrandColor(palette.getVibrantColor(Color.WHITE));
bitmap.recycle();
}
});
} catch (PackageManager.NameNotFoundException e) { /* do nothing */ }
}
public int getBrandColor(int defaultColor) {
if (mBrandColor == Color.BLACK || mBrandColor == Color.WHITE) {
return defaultColor;
}
return mBrandColor;
}
//-- BACKGROUND -----------------------------------------------------------
private void setBackground(@Nullable Bitmap bitmap) {
if (mBackgroundBitmap == (mBackgroundBitmap = bitmap)) return;
notifyListeners(EVENT_BACKGROUND);
}
/**
* Asynchronously generates the background of notification. The background is
* used by {@link com.achep.acdisplay.ui.fragments.AcDisplayFragment}.
*
* @see #clearBackground()
*/
public void loadBackgroundAsync() {
// Clear old background.
clearBackground();
// Generate new background.
Bitmap bitmap = mNotification.largeIcon;
if (isBackgroundFine(bitmap)) {
if (sBackgroundFactoryRef == null || (mBackgroundFactory = sBackgroundFactoryRef.get()) == null) {
sBackgroundFactoryRef = new WeakReference<>(mBackgroundFactory = new BackgroundFactory());
}
//noinspection ConstantConditions
mBackgroundFactory.add(null, this, mBackgroundCallback);
}
}
/**
* Stops the {@link #mBackgroundFactory background loader} and sets the background
* to {@code null}.
*
* @see #loadBackgroundAsync()
*/
public void clearBackground() {
if (mBackgroundFactory != null) {
mBackgroundFactory.remove(this);
mBackgroundFactory = null;
}
setBackground(null);
}
private boolean isBackgroundFine(@Nullable Bitmap bitmap) {
return bitmap != null && !BitmapUtils.hasTransparentCorners(bitmap);
}
//-- EMOTICONS ------------------------------------------------------------
public void setEmoticonsEnabled(boolean enabled) {
if (mEmoticonsEnabled == (mEmoticonsEnabled = enabled)) return;
reformatTexts();
}
//-- BASICS ---------------------------------------------------------------
private void reformatTexts() {
messageText = reformatMessage(messageTextOrigin);
messageBigText = reformatMessage(messageBigTextOrigin);
if (messageTextLines != null) {
for (int i = 0; i < messageTextLines.length; i++) {
messageTextLines[i] = reformatMessage(messageTextLinesOrigin[i]);
}
}
}
private CharSequence reformatMessage(@Nullable CharSequence cs) {
if (cs == null) return null;
if (mEmoticonsEnabled) cs = SmileyParser.getInstance().addSmileySpans(cs);
return cs;
}
/**
* Marks the notification as read.
*
* @see #setRead(boolean)
*/
public void markAsRead() {
setRead(true);
}
/**
* Sets the state of the notification.
*
* @param isRead {@code true} if user has seen the notification,
* {@code false} otherwise.
* @see #markAsRead()
*/
void setRead(boolean isRead) {
List<OpenNotification> list = getGroupNotifications();
if (list != null) {
for (OpenNotification n : list) n.setRead(isRead);
}
if (mRead == (mRead = isRead)) return;
notifyListeners(EVENT_READ);
}
/**
* Dismisses this notification from system.
*
* @see NotificationUtils#dismissNotification(OpenNotification)
*/
public void dismiss() {
NotificationUtils.dismissNotification(this);
}
/**
* Performs a click on notification.<br/>
* To be clear it is not a real click but launching its content intent.
*
* @return {@code true} if succeed, {@code false} otherwise
* @see NotificationUtils#startContentIntent(OpenNotification)
*/
public boolean click() {
return NotificationUtils.startContentIntent(this);
}
/**
* Clears some notification's resources.
*/
void recycle() {
Check.getInstance().isFalse(mRecycled);
mRecycled = true;
clearBackground();
AsyncTask.stop(mPaletteWorker);
if (mIconFactory != null) {
mIconFactory.remove(this);
mIconFactory = null;
}
}
/* Only for debug purposes */
boolean isRecycled() {
return mRecycled;
}
/**
* @return {@code true} if notification has been posted from my own application,
* {@code false} otherwise (or the package name can not be get).
*/
public boolean isMine() {
return mMine;
}
/**
* @return {@code true} if notification can be dismissed by user, {@code false} otherwise.
*/
public boolean isDismissible() {
return isClearable();
}
/**
* Convenience method to check the notification's flags for
* either {@link Notification#FLAG_ONGOING_EVENT} or
* {@link Notification#FLAG_NO_CLEAR}.
*/
public boolean isClearable() {
return !Operator.bitAnd(mNotification.flags, Notification.FLAG_ONGOING_EVENT)
&& !Operator.bitAnd(mNotification.flags, Notification.FLAG_NO_CLEAR);
}
public boolean isContentSecret(@NonNull Context context) {
return NotificationUtils.isSecret(context, this, VISIBILITY_PRIVATE,
Config.PRIVACY_HIDE_CONTENT_MASK);
}
/**
* @return the package name of notification, or a random string
* if not possible to get the package name.
*/
@NonNull
public abstract String getPackageName();
/**
* Time since notification has been loaded; in {@link android.os.SystemClock#elapsedRealtime()}
* format.
*/
public long getLoadTimestamp() {
return mLoadedTimestamp;
}
//-- GROUPS ---------------------------------------------------------------
/**
* @return a key that indicates the group with which this message ranks,
* or a {@code null} on deprecated systems.
* @see #getGroupNotifications()
*/
@Nullable
public String getGroupKey() {
return null;
}
/**
* @return the list of notifications of this group (without its summary),
* or a {@code null} on deprecated systems.
* @see #isGroupChild()
* @see #isGroupSummary()
*/
@Nullable
public List<OpenNotification> getGroupNotifications() {
return null;
}
/**
* @return {@code true} if this notification is a child of the {@link #getGroupKey() group},
* {@code false} otherwise.
*/
public boolean isGroupChild() {
return false;
}
/**
* @return {@code true} if this notification is the summary (short summary of all notifications)
* of the {@link #getGroupKey() group}, {@code false} otherwise.
*/
public boolean isGroupSummary() {
return false;
}
}