Android关于Notifiacation的封装

之前在框架封装中提到了有关Notification的处理,觉得很有必要详细整理一下封装的思路。

Notification是一个Android中经常使用的组件了,但是原生API提供的功能并不能够很好的满足商业应用开发时的需求,初始化也比较繁琐,个人认为在开发时有必要在现有API的基础上做一下封装,目的有二,其一是集合应用中针对Notification所需的定制功能,其二是提升项目的代码整洁性和模块化程度。


本文只贴出我认为重要的代码:完整源码以及可演示的demo请查看 QuickDevFramework


0. Notification构建的封装

目前Android中Notification构建是通过NotificationCompat.Builder来完成的,在Builder类中配置在通知栏上显示所需的各项参数, 其中有三个参数是必须的:smallIcon, contentText, contentTitle,这三个元素在我的原生Android 7.0中显示效果如下

这三个参数如果任意一条不配置,则Notification不会显示,但是也不会有任何编译错误或运行时异常,因此在构建Notification封装的过程中将这三个参数作为创建一条Notification的必须参数是一个很好的避免犯浑出错的方法;其中,smallIcon对于一个应用来说,应该是一样的,因此可以将其写为常量或者以常量作为默认值重载一个无需SmallIcon的构造方法,我个人选择了后者作为封装方案
// source code from NotificationWrapper

private static int defaultIconRes = R.drawable.ic_notify_default;

public static void setDefaultIconRes(@DrawableRes int resId) {
    defaultIconRes = resId;
}

public static int getDefaultIconRes() {
    return defaultIconRes;
}

//source code from SimpleNotificationBuilder

private Context mContext;
private NotificationCompat.Builder mBuilder;

/**
 * 构造方法
 * <p>此处将Notification的icon设定为只用NotificationWrapper中所配置的默认icon</p>
 * */
public SimpleNotificationBuilder(Context context, @NonNull String contentTitle, @NonNull String contentText) {
    this(context, NotificationWrapper.getDefaultIconRes(), contentTitle, contentText);
}

/**
 * 构造方法
 * <p>icon,contentTitle,contentText为构建一个Notification的必须参数,
 * 如果不传递这三个参数,代码不会报错,但通知不会显示</p>
 * */
public SimpleNotificationBuilder(Context context, @DrawableRes int icon, @NonNull String contentTitle, @NonNull String contentText) {
    this.mContext = context;
    mBuilder = new NotificationCompat.Builder(context);
    mBuilder.setSmallIcon(icon)
    .setContentTitle(contentTitle)
    .setContentText(contentText);
}

1. Notification常见样式的封装

出去上面只包含最基本元素的Notification外,Notification有多种样式,常见的有如下三种,BigText、BigPicture、Inbox,分别是大文本,大图,多行


这三种样式分别对应NotificationCompat下属的BigTextStyle, BigPictureStyle和InboxStyle,在配置好Style之后通过NotificationCompat.Builder.setStyle()方法设置样式生效。可以看到上述三种样式其实也是比较简单的,完全可以在封装中简化配置过程,将样式配置归一化处理
//source code from simpleNotificationBuilder

public SimpleNotificationBuilder setBigText(String title, String contentText, String summaryText) {
    NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle();
    bigTextStyle.setBigContentTitle(title);
    bigTextStyle.bigText(contentText);
    if (!TextUtils.isEmpty(summaryText)) {
        bigTextStyle.setSummaryText(summaryText);
    }
    setStyle(bigTextStyle);
    return this;
}

public SimpleNotificationBuilder setBigPicture(String title, Bitmap picture, String summaryText) {
    NotificationCompat.BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
    bigPictureStyle.setBigContentTitle(title);
    bigPictureStyle.bigPicture(picture);
    if (!TextUtils.isEmpty(summaryText)) {
        bigPictureStyle.setSummaryText(summaryText);
    }
    setStyle(bigPictureStyle);
    return this;
}

public SimpleNotificationBuilder setInboxMessages(String title, String summaryText, List<String> lines) {
    NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
    inboxStyle.setBigContentTitle(title);
    if (!TextUtils.isEmpty(summaryText)) {
        inboxStyle.setSummaryText(summaryText);
    }
    for (String line : lines) {
        inboxStyle.addLine(line);
    }
    setStyle(inboxStyle);
    return this;
}

2. Notification点击操作的封装

Notification点击产生的操作时通过PendingIntent来设定的,PendingIntent可以看做是Intent的一个包装,使用它的目的就是为了在App未启动的状态下,也可以通过PendingIntent里面的Context执行Intent,从而达到点击通知时无论应用是否启动都能做出操作的目的。
直接使用PendingIntent来打开App中的页面是可以的,但是所能做到的事情就是直接通过Intent唤起一个组件而已,如果想退回上一级页面目前网上很多博客都在说TaskStackBuilder、ParentStack,但这个方法只是形式上的指定“这个页面的上一级页面是什么”,两个页面可以完全没有关系,在实际操作中也不是先启动上一级页面再进入目标页面,而是在当前页面退出后如果存在Parent就打开Parent,与我们在进行商业应用开发中所需的逻辑相去甚远。

仔细把玩一下目前的主流应用可以看到Notification的处理有如下逻辑:
收到一条通知后,如果应用处于启动状态(无论是处于前台还是后台),点击通知时直接打开目标页面,不破坏当前的Activity栈;如果应用处于未启动状态,则先启动应用,然后打开目标页面。
如果想要实现这样的功能单靠PendingIntent就不行了, 这个功能大概有两个重点:1. 如何判断应用是否启动,2. 在什么时机和地方去判断应用是否启动并执行相应操作
这两个问题需要结合在一起看,因为判断的时机不同判断的方法也可能不同。使用PendingIntent可以做到唤起Activity之类的组件,那么在什么地方和时机去执行判断操作这个问题就找到了突破口:让PendingIntent启动一个Activity或者BroadcastReceiver,在其中进行判断,第一个原因的考虑,我选择了使用BroadcastReceiver,下面叙述。
对于应用是否启动的判断,思路有如下:
  • 判断进程是否存在
  • 获取应用当前Activity的数量判断是否存在启动的前台页面。
根据这个思路我找到了判断进程是否存在的方法
/**
 * 判断应用是否已经启动
 *
 * @param context     一个context
 * @param packageName 要判断应用的包名
 * @return boolean
 */
public static boolean isProcessRunning(Context context, String packageName) {
    ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

    List<ActivityManager.RunningAppProcessInfo> processInfo = activityManager.getRunningAppProcesses();
    for (int i = 0; i < processInfo.size(); i++) {
        if (processInfo.get(i).processName.equals(packageName)) {
            return true;
        }
    }
    return false;
}
使用这个方法可以检查一个进程是否正在运行,而且不需要多余的权限,是比较好的手段,但存在一个问题就是如果检查函数本身就处于被检查的进程中,那么自己检查自己肯定是运行状态。这时候,要么检查activity数量,要么将检查组件置于不同进程中。出于从简处理的目的我自己的实现方案没有选择使用不同进程,而是采取通过ActivityLifeCycle来维护一个Activity计数。以此来判断应用是否正在运行,此时可以明显看到使用BroadcastReceiver比Activity要合适。
这个检测方法顺便也解决了判断应用前后台的问题 :)
public atract class BaseApplication extends Application {
    /**
     * 用于判断一个app是否处于前台
     */
    private static int mForegroundCount = 0;

    /**
     * 应用内Activity的数量,如果数量为0,则可以判断当前应用未启动
     * <p>如果有什么一像素Activity等东西存在请另改值</p>
     * */
    private static int mActivityCount = 0;

     @Override
    public void onCreate() {
        super.onCreate();
        this.registerActivityLifecycleCallbacks(new FrameworkActivityLifeCycleCallback());
    }

    ……

    /**
     * 检查应用主进程是否正在运行
     * */
    public static boolean isAppRunning() {
        Context mContext = getAppContext();
        String packageName = mContext.getPackageName();
        ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningAppProcessInfo> processInfo = activityManager.getRunningAppProcesses();
        for(int i = 0; i < processInfo.size(); i++){
            if(processInfo.get(i).processName.equals(packageName)){
                //如果有没被销毁的Activity,则App至少处于后台正在运行,否则App应处于未运行状态
                return mActivityCount > 0;
            }
        }
        return false;
    }

    /**
     * 检查App是否处于前台
     * */
    public static boolean isAppForeground() {
        return isAppRunning() && mForegroundCount > 0;
    }


    private class FrameworkActivityLifeCycleCallback implements ActivityLifecycleCallbacks {

        @Override
        public void onActivityCreated(Activity activity, Bundle bundle) {
            mActivityCount++;
        }

        @Override
        public void onActivityStarted(Activity activity) {
            mForegroundCount++;
        }

        @Override
        public void onActivityResumed(Activity activity) {}

        @Override
        public void onActivityPaused(Activity activity) {}

        @Override
        public void onActivityStopped(Activity activity) {
            mForegroundCount--;
        }

        @Override
        public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {}

        @Override
        public void onActivityDestroyed(Activity activity) {
            mActivityCount--;
        }
    }
}
负责跳转的Intent封装
Intent broadcastIntent = new Intent(mContext, NotificationReceiver.class);
Bundle bundle = new Bundle();
if (targetActivityIntent.getExtras() != null) {
    bundle.putAll(targetActivityIntent.getExtras());
}
bundle.putString(NotificationWrapper.KEY_TARGET_ACTIVITY_NAME, targetActivityIntent.getComponent().getClassName());
broadcastIntent.putExtra(NotificationWrapper.KEY_NOTIFICATION_EXTRA, bundle);
PendingIntent mPendingIntent = PendingIntent.getBroadcast(mContext, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
负责判断逻辑处理的BroadcastReceiver
public abspublic class NotificationReceiver extends BroadcastReceiver {

    private static final String TAG = NotificationReceiver.class.getSimpleName();

    @Override
    public void onReceive(Context context, Intent intent) {
        Bundle notificationExtra = intent.getBundleExtra(NotificationWrapper.KEY_NOTIFICATION_EXTRA);
        if (notificationExtra == null) {
            return;
        }
        if (BaseApplication.isAppRunning()) {
            Intent targetIntent = new Intent();
            String targetKey = notificationExtra.getString(NotificationWrapper.KEY_TARGET_ACTIVITY_NAME);
            if (TextUtils.isEmpty(targetKey)) {
                targetKey = NotificationResumeActivity.class.getName();
            }
            try {
                Class<?> targetActivityClass = Class.forName(targetKey);
                targetIntent.setClass(context, targetActivityClass);
                targetIntent.putExtras(notificationExtra);
            } catch (ClassNotFoundException e) {
                targetIntent.setClass(context, NotificationResumeActivity.class);
            }
            targetIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(targetIntent);
        }
        else {
            Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(
                    BaseApplication.getAppContext().getPackageName());
            launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
            launchIntent.putExtra(NotificationWrapper.KEY_NOTIFICATION_EXTRA, notificationExtra);
            context.startActivity(launchIntent);
        }
    }
}
在应用启动/初始化Activity中的处理
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Intent intent = getIntent();
    notificationExtra = intent.getBundleExtra(NotificationWrapper.KEY_NOTIFICATION_EXTRA);
    isHandleNotification = notificationExtra != null;
}

@Override
protected void onDestroy() {
    super.onDestroy();
    handleNotification();
}

protected void handleNotification() {
    if (!isHandleNotification) {
        return;
    }
    String targetKey = notificationExtra.getString(NotificationWrapper.KEY_TARGET_ACTIVITY_NAME);
    if (TextUtils.isEmpty(targetKey)) {
        Logger.e(TAG, "handleNotification: target key is null !");
        return;
    }
    try {
        Class<?> destActivityClass = Class.forName(targetKey);
        Intent destIntent = new Intent(this, destActivityClass);
        destIntent.putExtras(notificationExtra);
        startActivity(destIntent);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
        Logger.e(TAG, "handleNotification: reflect to get activity class failed !");
    }
}
这样就在同一进程的方案下实现了Notification所需的跳转功能。

3. 扩展预备

上述内容只是对最为常见的一些Notification构建、样式、操作的基础封装,在未来的使用中必然还会有更多的需求,因此自定义的Builder以及一些独特的功能应该有预留的扩展方法,至少不至于在当前Notification构建满足不了需求的时候就只能copy代码...
我在项目中编写的SimpleNotificationBuilder代理了常用的NotificationCompat.Builder所含方法,但并没有全部代理,主要是考虑到潜在的SDK更新后源码变更的可能性,因此配置了一个接口用于对类中的NotificationCompat.Builder进行编辑。
另一方面可能某些自己构建的Notification也会用到根据应用启动状态判断打开方式的功能,因此将打开BroadcastReceiver的Intent包装功能单独做了一个方法以供使用:
/**
 * 将Notification的Intent转换成用广播传递的Intent
 * <p>
 * 主要用于自定义Notification时处理点击打开Activity的事件,使用此方法
 * 将会在应用启动时直接打开目标Activity,应用未启动时先启动应用再打开Activity
 * </p>
 * */
public static Intent getBroadcastIntent(Context context, Intent targetActivityIntent) {
    Intent broadcastIntent = new Intent(context, NotificationReceiver.class);
    Bundle bundle = new Bundle();
    bundle.putAll(targetActivityIntent.getExtras());
    bundle.putString(NotificationWrapper.KEY_TARGET_ACTIVITY_NAME, targetActivityIntent.getComponent().getClassName());
    broadcastIntent.putExtra(NotificationWrapper.KEY_NOTIFICATION_EXTRA, bundle);
    return broadcastIntent;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值