/* * Copyright (C) 2015 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.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.style.StyleSpan; import com.achep.acdisplay.Config; import com.achep.acdisplay.R; import com.achep.acdisplay.interfaces.INotificatiable; import com.achep.base.tests.Check; import com.achep.base.utils.CsUtils; import com.achep.base.utils.NullUtils; import com.achep.base.utils.Operator; import com.achep.base.utils.RefCacheBase; import java.lang.ref.Reference; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.Arrays; /** * Created by Artem Chepurnoy on 11.02.2015. */ public class NotificationUiHelper implements INotificatiable { // Callback events public static final int EVENT_TITLE_CHANGED = 1; public static final int EVENT_MESSAGE_CHANGED = 2; public static final int EVENT_TIMESTAMP_CHANGED = 3; public static final int EVENT_SUBTITLE_CHANGED = 4; public static final int EVENT_ACTIONS_CHANGED = 5; public static final int EVENT_SMALL_ICON_CHANGED = 6; public static final int EVENT_LARGE_ICON_CHANGED = 7; // Pending updates private static final int PENDING_UPDATE_TITLE = 1; private static final int PENDING_UPDATE_SUBTITLE = 1 << 1; private static final int PENDING_UPDATE_TIMESTAMP = 1 << 2; private static final int PENDING_UPDATE_MESSAGE = 1 << 3; private static final int PENDING_UPDATE_ACTIONS = 1 << 4; private static final int PENDING_UPDATE_ICONS = 1 << 5; private static SoftReference<CharSequence[]> sSecureContentLabelRef; static RefCacheBase<Bitmap> sAppIconCache = new RefCacheBase<Bitmap>() { @NonNull @Override protected Reference<Bitmap> onCreateReference(@NonNull Bitmap bitmap) { return new WeakReference<>(bitmap); } }; private OpenNotification mNotification; private CharSequence[] mMessages; private CharSequence mTitle; private CharSequence mTimestamp; private CharSequence mSubtitle; private Action[] mActions; private Bitmap mSmallIcon; private Bitmap mLargeIcon; private boolean mBig; private final Context mContext; private final OnNotificationContentChanged mListener; private final OpenNotification.OnNotificationDataChangedListener mNo = new OpenNotification.OnNotificationDataChangedListener() { @Override public void onNotificationDataChanged( @NonNull OpenNotification notification, int event) { Check.getInstance().isInMainThread(); switch (event) { case OpenNotification.EVENT_ICON: updateIcons(); break; } } }; private int mPendingUpdates; private boolean mResumed; public interface OnNotificationContentChanged { /** * @param n * @param event */ void onNotificationContentChanged( @NonNull NotificationUiHelper helper, final int event); } public NotificationUiHelper( @NonNull Context context, @NonNull OnNotificationContentChanged listener) { mContext = context; mListener = listener; } public void resume() { Check.getInstance().isInMainThread(); mResumed = true; synchronized (NotificationPresenter.getInstance().monitor) { if (Operator.bitAnd(mPendingUpdates, PENDING_UPDATE_TITLE)) updateTitle(); if (Operator.bitAnd(mPendingUpdates, PENDING_UPDATE_SUBTITLE)) updateSubtitle(); if (Operator.bitAnd(mPendingUpdates, PENDING_UPDATE_TIMESTAMP)) updateTimestamp(); if (Operator.bitAnd(mPendingUpdates, PENDING_UPDATE_MESSAGE)) updateMessage(); if (Operator.bitAnd(mPendingUpdates, PENDING_UPDATE_ACTIONS)) updateActions(); if (Operator.bitAnd(mPendingUpdates, PENDING_UPDATE_ICONS)) updateIcons(); } mPendingUpdates = 0; registerNotificationListener(); } public void pause() { Check.getInstance().isInMainThread(); unregisterNotificationListener(); mResumed = false; } /** * @param n */ public void setNotification(@Nullable OpenNotification n) { Check.getInstance().isInMainThread(); unregisterNotificationListener(); mNotification = n; registerNotificationListener(); synchronized (NotificationPresenter.getInstance().monitor) { // Update everything updateTitle(); updateTimestamp(); updateSubtitle(); updateMessage(); updateActions(); updateIcons(); } } /** * {@inheritDoc} */ @Nullable @Override public OpenNotification getNotification() { return mNotification; } /** * Controls the <i>big</i> and <i>small</i> types of the notification * view. */ public void setBig(boolean isBig) { mBig = isBig; } private void registerNotificationListener() { if (mNotification != null) mNotification.registerListener(mNo); } private void unregisterNotificationListener() { if (mNotification != null) mNotification.unregisterListener(mNo); } private boolean isSecret(int minVisibility, int privacyMask) { return NotificationUtils.isSecret(mContext, mNotification, minVisibility, privacyMask); } /** * @param mask one of the following: * {@link #PENDING_UPDATE_TITLE}, {@link #PENDING_UPDATE_SUBTITLE}, * {@link #PENDING_UPDATE_MESSAGE}, {@link #PENDING_UPDATE_ACTIONS}, * {@link #PENDING_UPDATE_TIMESTAMP}, {@link #PENDING_UPDATE_ICONS}. * @return {@code true} if the update should be canceled, {@code false} otherwise. */ private boolean isPendingUpdate(int mask) { if (!mResumed) { mPendingUpdates |= mask; return true; } return false; } //-- ICONS ---------------------------------------------------------------- private void updateIcons() { if (isPendingUpdate(PENDING_UPDATE_ICONS)) return; if (mNotification == null) { setLargeIcon(null); setSmallIcon(null); return; } final boolean secret = isLargeIconSecret(); Bitmap bitmap; if (secret) { // Load application's icon as the large icon. // Store the bitmaps in soft-reference cache map, to // reduce memory usage and improve performance. String packageName = mNotification.getPackageName(); if ((bitmap = sAppIconCache.get(packageName)) == null) { Drawable drawable = getAppIcon(mNotification.getPackageName()); if (drawable != null) { bitmap = Bitmap.createBitmap( drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_4444); drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); drawable.draw(new Canvas(bitmap)); sAppIconCache.put(packageName, bitmap); } else { bitmap = null; sAppIconCache.remove(packageName); } } } else { bitmap = mNotification.getNotification().largeIcon; } if (bitmap == null) { setLargeIcon(mNotification.getIcon()); setSmallIcon(null); } else { setLargeIcon(bitmap); setSmallIcon(mNotification.getIcon()); } } /** * @return {@code true} if the large icon is a secret and should not be visible to * user, {@code false} otherwise. */ protected final boolean isLargeIconSecret() { return isSecret( OpenNotification.VISIBILITY_SECRET, Config.PRIVACY_HIDE_CONTENT_MASK); } @Nullable private Drawable getAppIcon(@NonNull String packageName) { PackageManager pm = mContext.getPackageManager(); try { ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0); return pm.getApplicationIcon(appInfo); } catch (PackageManager.NameNotFoundException e) { return null; } } private void setSmallIcon(@Nullable Bitmap bitmap) { if (sameAs(mSmallIcon, bitmap)) { // No need to notify listeners about this // change. return; } mSmallIcon = bitmap; mListener.onNotificationContentChanged(this, EVENT_SMALL_ICON_CHANGED); } /** * @return the notification's small icon to be displayed. * @see #getLargeIcon() */ @Nullable public Bitmap getSmallIcon() { return mSmallIcon; } private void setLargeIcon(@Nullable Bitmap bitmap) { if (sameAs(mLargeIcon, bitmap)) { // No need to notify listeners about this // change. return; } mLargeIcon = bitmap; mListener.onNotificationContentChanged(this, EVENT_LARGE_ICON_CHANGED); } /** * @return the notification's large icon to be displayed. * @see #getSmallIcon() */ @Nullable public Bitmap getLargeIcon() { return mLargeIcon; } /** * @return {@code true} if both {@link Bitmap bitmaps} are {@code null} * or if the {@link Bitmap bitmaps} are equal according to * {@link android.graphics.Bitmap#sameAs(android.graphics.Bitmap)}, {@code false} otherwise. */ private boolean sameAs(@Nullable Bitmap bitmap, @Nullable Bitmap bitmap2) { return bitmap == bitmap2 || bitmap != null && bitmap2 != null && bitmap.sameAs(bitmap2); } //-- TITLE ---------------------------------------------------------------- private void updateTitle() { if (isPendingUpdate(PENDING_UPDATE_TITLE)) return; if (mNotification == null) { setTitle(null); return; } final boolean secret = isTitleSecret(); CharSequence title; if (secret) { CharSequence appLabel = getAppLabel(mNotification.getPackageName()); title = appLabel != null ? appLabel : "Hidden app"; } else if (mBig) { title = NullUtils.whileNotNull( mNotification.titleBigText, mNotification.titleText ); } else { title = mNotification.titleText; } setTitle(title); } /** * @return {@code true} if the title is a secret and should not be visible to * user, {@code false} otherwise. */ protected final boolean isTitleSecret() { return isSecret( OpenNotification.VISIBILITY_SECRET, Config.PRIVACY_HIDE_CONTENT_MASK); } @Nullable private CharSequence getAppLabel(@NonNull String packageName) { PackageManager pm = mContext.getPackageManager(); try { ApplicationInfo appInfo = pm.getApplicationInfo(packageName, 0); return pm.getApplicationLabel(appInfo); } catch (PackageManager.NameNotFoundException e) { return null; } } private void setTitle(@Nullable CharSequence title) { if (TextUtils.equals(mTitle, title)) { // No need to notify listeners about this // change. return; } mTitle = title; mListener.onNotificationContentChanged(this, EVENT_TITLE_CHANGED); } /** * @return the notification's title to be displayed. * @see #getMessages() * @see OpenNotification#getVisibility() */ @Nullable public CharSequence getTitle() { return mTitle; } //-- TIMESTAMP ------------------------------------------------------------ private void updateTimestamp() { if (isPendingUpdate(PENDING_UPDATE_TIMESTAMP)) return; if (mNotification == null) { setTimestamp(null); return; } final long when = mNotification.getNotification().when; setTimestamp(DateUtils.formatDateTime(mContext, when, DateUtils.FORMAT_SHOW_TIME)); } private void setTimestamp(@Nullable CharSequence timestamp) { if (TextUtils.equals(mTimestamp, timestamp)) { // No need to notify listeners about this // change. return; } mTimestamp = timestamp; mListener.onNotificationContentChanged(this, EVENT_TIMESTAMP_CHANGED); } /** * @return the notification's timestamp to be displayed. * @see android.app.Notification#when */ @Nullable public CharSequence getTimestamp() { return mTimestamp; } //-- SUBTITLE ------------------------------------------------------------- protected void updateSubtitle() { if (isPendingUpdate(PENDING_UPDATE_SUBTITLE)) return; if (mNotification == null) { setSubtitle(null); return; } setSubtitle(CsUtils.join(" ", mNotification.subText, mNotification.infoText)); } private void setSubtitle(@Nullable CharSequence subtitle) { if (TextUtils.equals(mSubtitle, subtitle)) { // No need to notify listeners about this // change. return; } mSubtitle = subtitle; mListener.onNotificationContentChanged(this, EVENT_SUBTITLE_CHANGED); } /** * @return the notification's subtitle to be displayed. */ @Nullable public CharSequence getSubtitle() { return mSubtitle; } //-- MESSAGE -------------------------------------------------------------- /** * Updates message from the current {@link #mNotification notificiation}. * * @see #setMessages(CharSequence[]) * @see #isMessageSecret() */ private void updateMessage() { if (isPendingUpdate(PENDING_UPDATE_MESSAGE)) return; if (mNotification == null) { setMessages(null); return; } final boolean secret = isMessageSecret(); // Get message text if (secret) { CharSequence[] messages; if (sSecureContentLabelRef == null || (messages = sSecureContentLabelRef.get()) == null) { final CharSequence cs = mContext.getString(R.string.privacy_mode_hidden_content); final SpannableString ss = new SpannableString(cs); Check.getInstance().isTrue(ss.length()); ss.setSpan(new StyleSpan(Typeface.ITALIC), 0 /* start */, ss.length() /* end */, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); messages = new CharSequence[]{ss}; sSecureContentLabelRef = new SoftReference<>(messages); } setMessages(messages); } else { CharSequence message; if (mBig) { if (mNotification.messageTextLines != null) { setMessages(mNotification.messageTextLines); return; } message = NullUtils.whileNotNull( mNotification.messageBigText, mNotification.messageText ); } else { message = mNotification.messageText; } setMessages(TextUtils.isEmpty(message) ? null : new CharSequence[]{message}); } } /** * @return {@code true} if the message is a secret and should not be visible to * user, {@code false} otherwise. */ protected final boolean isMessageSecret() { return mNotification.isContentSecret(mContext); } private void setMessages(@Nullable CharSequence[] messages) { if (Arrays.equals(mMessages, messages)) { // No need to notify listeners about this // change. return; } mMessages = messages; mListener.onNotificationContentChanged(this, EVENT_MESSAGE_CHANGED); } /** * @return the notification's messages to be displayed. * @see #getTitle() * @see OpenNotification#getVisibility() */ @Nullable public CharSequence[] getMessages() { return mMessages; } //-- ACTIONS -------------------------------------------------------------- private void updateActions() { if (isPendingUpdate(PENDING_UPDATE_ACTIONS)) return; if (mNotification == null) { setActions(null); return; } final boolean secret = isActionsSecret(); setActions(secret || !mBig ? null : mNotification.getActions()); } /** * @return {@code true} if the actions are secret and should not be visible to * user, {@code false} otherwise. */ protected final boolean isActionsSecret() { return isSecret( OpenNotification.VISIBILITY_PRIVATE, Config.PRIVACY_HIDE_ACTIONS_MASK); } private void setActions(@Nullable Action[] actions) { if (Arrays.equals(mActions, actions)) { // No need to notify listeners about this // change. return; } mActions = actions; mListener.onNotificationContentChanged(this, EVENT_ACTIONS_CHANGED); } /** * @return the notification's actions to be displayed. * @see OpenNotification#getVisibility() */ @Nullable public Action[] getActions() { return mActions; } }