Android通知还能这么玩?

15,316 阅读11分钟

前言

作为安卓用户,大家是不是一直觉得安卓手机上的通知栏≈垃圾场,除了少数有用的社交通讯通知,其他的都是些垃圾推送通知,好多人会选择直接关闭,也有人放任不管。

虽然原生Android、第三方Android系统,近几年都针对通知管理做了优化,但也只是给用户提供了更多选择,重要还是次要、优先还是静默等等,还是显得有点笨拙和难用。

因而市面上出现了一些通知管理的app,它们能更加精准地过滤无用通知,使得通知栏更加清爽干净,同时还基于系统通知提供一些实用和有趣的功能。下面让我们来揭秘下这些功能是如何实现的呢?

如何监听通知

实际上Android系统提供了相应的能力,让我们实现通知的监听。Android4.3加入了通知监听服务NotificationListenerService,我们开发的APP拥有此权限后便可以监听当前系统的通知的变化,Android4.4开始还可以获取通知详情信息。下面我们来看看具体使用。

实现监听三步曲

1.创建一个类,继承NotificationListenerService

@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public class NotificationListener extends NotificationListenerService {
  
  //当系统收到新的通知后出发回调
  @Override
  public void onNotificationPosted(StatusBarNotification sbn) {
    super.onNotificationPosted(sbn);
  }
	
  //当系统通知被删掉后出发回调
  @Override
  public void onNotificationRemoved(StatusBarNotification sbn) {
    super.onNotificationRemoved(sbn);
  }
}

2.在AndroidManifest.xml中注册Service并声明相关权限

 <service
      android:name=".NotificationListener"
      android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
      <intent-filter>
        <action android:name="android.service.notification.NotificationListenerService" />
      </intent-filter>
    </service>

3.在系统“通知使用权”页面勾选app

完成上面两步设置,在app安装到手机上之后,还需要在“通知使用权”页面勾选app,这个路径在原生系统上是“设置->隐私->通知使用权”,不过在第三方系统上面路径不一致,而且比较难找,可以通过以下代码跳转过去。

private void jumpNotificationAccessSetting(Context context) {
    try {
      Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
      intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
      context.startActivity(intent);
    } catch(Exception e) {
        e.printStackTrace();
      }
    }

完成前面三步后,就可以在我们自己的app里面监听到系统发出的通知了。NotificationListenerService相关接口和StatusBarNotificationNotification的一些重要字段,大家可以参考附录1。

通知花样玩法

通过上面一节的分析,我们知道了如何拿到其他app发送的通知,以及了解了系统提供的一些操控通知的方法。基于这些能力,我们能在通知上玩出什么花样呢?目前市面上已经有一些app,比如通知过滤、通知语音播报、抢红包、微信消息防撤回等。下面我们来分析下,这些功能具体是如何实现的。

1.通知过滤

通知过滤是指自动移除符合一定条件的通知,比如内容里面包含某些关键词的、某些app发送的、某个时间段发送的、手机特定状态下(熄屏/亮屏,充电/电池)发送的通知。

下图是“通知滤盒”app里面过滤爱奇艺里面内容包含“头条”的通知,它的过滤条件可以是不同文本内容的组合,包含或者不包含。

下图是在“一知”app里面自动过滤广告营销类型的通知和自动收纳内容资讯的通知。

处理流程

通过NotificationListenerService#onNotificationPosted()接口获取到通知实例后,我们抽取出通知内容、包名、发送时间等关键信息,然后把这些信息输入到一系列过滤器中,满足过滤条件的通知就调用cancelNotification移除或者snoozeNotification冻结。

技术点

1.获取标准通知的文本内容

目前大部分应用发送的通知都是标准通知,获取文本内容比较容易。

@Override
public void getContent(StatusBarNotification sbn) {
    Notification notification = sbn.getNotification();
    Bundle extras = notification.extras;
    if (extras != null) {
        // 获取通知标题
        String title = extras.getString(Notification.EXTRA_TITLE, "");
        // 获取通知内容
        String content = extras.getString(Notification.EXTRA_TEXT, "");
    }
}

2.获取非标准通知的文本内容

非标准通知是指通过setCustomContentView或者setCustomBigContentView 方法实现的通知,通过常规的内容获取方法无法获取到文本信息,我们可以遍历view的方法来获取。

主要流程有以下三步:

  1. 获取非标准通知的contentView和bigContentView;
  2. 调用RemoteViews#apply方法,把RemoteViews转成View;
  3. 遍历View,获取TextView里面的文本内容。
  //获取notification的view
  public static View getContentView(Context context, Notification notification) {
    RemoteViews contentView = null;
    //获取contentView
    if (notification.contentView != null) {
      contentView = notification.contentView;
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
      contentView = Notification.Builder.recoverBuilder(context, notification).createContentView();
    }

    //RemoteViews转成view
    View view = null;
    try {
      view = contentView == null ? null : contentView.apply(context, null);
    } catch (Throwable e) {
    }
    return view;
  }


  //获取view里面的文本
  public static String getContent(View view) {
    StringBuilder stringBuilder = new StringBuilder();
    traversalView(view, stringBuilder);
    return stringBuilder.toString();
  }

  //遍历View,获取TextView里面的文本内容
  private static void traversalView(View view, StringBuilder stringBuilder) {
    if (view instanceof ViewGroup) {
      ViewGroup viewGroup = (ViewGroup) view;
      int count = viewGroup.getChildCount();

      for (int i = 0; i < count; ++i) {
        View childView = viewGroup.getChildAt(i);
        traversalView(childView, stringBuilder);
      }
    } else {
      if (view instanceof TextView) {
        TextView tv = (TextView) view;
        CharSequence text = tv.getText();
        stringBuilder.append(text);
        stringBuilder.append(";");
      }
    }
  }

3.移除常驻通知

常驻通知是Notification#isClearable()为false的通知,即使我们点击了“清除全部”按钮后,也是清除不了的,用户只能通过手动关闭该应用的通知或者通知对应的删除按钮来清除。

常见的有音乐类app的通知,更多的是一些为了增加应用入口或者为了进程保活的通知,这类常驻通知我们实际上是最想清除的。

在Android8.0以后系统开放了接口snoozeNotification(String key, long durationMs),它实际上是冻结通知的方法,key是通知的唯一标志,durationMs是冻结时长,一般设置一个很大的值,就可以实现清除常驻通知的目的,手机重启之后该冻结操作会失效。

2.微信消息防撤回

微信上的消息撤回功能,可谓是让人又爱又恨,爱是因为发错消息能及时撤回,恨则是因为别人也能撤回。相信大家每次看到好友撤回消息的时候,一定会很好奇他/她到底撤回了啥?

想实现微信消息防撤回本身是很难的,之前都是通过root之后装xposed框架,然后hook微信内部方法来实现的,现在通过通知监听服务也能实现。

下图是某个第三方app实现的微信防撤回功能界面,实际上是把可能撤回的消息都给你列了出来,你自己去辨别哪条是撤回消息。

实现方案

实现该功能的前提是微信的通知权限是打开的。当我们收到微信的消息后,提取出key、名字、时间戳等信息,存入数据库,当用户点击撤回消息的时候,查询2分钟以内的消息记录(撤回有效时间2分钟),然后用户根据这个列表判断哪条是撤回消息(无法精准定位撤回消息)。大致流程如下:

3.通知增能

通知增能主要是给通知扩展一些额外的能力,比如悬浮通知、弹幕通知、语音播报、自定义铃声、震动、自动跳转等。大家可以想想这些功能可以在哪些场景下使用?

弹幕通知:打游戏、看视频等全屏状态下方便查看通知;

自定义铃声:给微信、QQ、Soul等IM软件的不同好友和群设置不同的铃声;

语音播报:在驾驶、散步时不遗漏重要通知;

下图是一些第三方app提供的通知扩展能力。

             悬浮通知                                        自定义铃声                                              语音播报

技术方案

我们来揭秘下上面的这些功能是怎么实现的。第一步是定义匹配规则,比如关键词包含或者正则表达式匹配,然后是规则对应的执行动作,这些动作最后都是调用系统提供的能力。

4.抢红包

微信抢红包功能第一步是通过NotificationListenerService监听到红包通知,然后跳转到红包页面,最后通过辅助服务AccessibilityService来模拟点击动作,实现抢红包功能。技术难点主要是红包通知的判断和模拟点击的实现,模拟点击涉及到辅助服务相关技术,这里不展开讲了。

红包通知判断

主要是判断通知内容里面是不是包含“[微信红包]”。

 public void processRedPacketNotification(StatusBarNotification sbn) {
    PendingIntent pendingIntent = null;
    Notification notification = sbn.getNotification();
    Bundle extras = notification.extras;
    // 获取通知内容
    String content = extras.getString(Notification.EXTRA_TEXT, "");
    if (!TextUtils.isEmpty(content) && content.contains("[微信红包]")) {
      pendingIntent = notification.contentIntent;
    }

    // 发送pendingIntent打开微信
    try {
      if (pendingIntent != null) {
        pendingIntent.send();
      }
    } catch (PendingIntent.CanceledException e) {
      e.printStackTrace();
    }
  }

分析下源码

既然都讲这么多了,我们要不顺便分析一下通知发送源码吧。通知模块涉及到的主要类有Notification、NotificationChannel、NotificationManager、NotificationManagerService、NotificationListenerService等,这几个类的关系如下关系如下:

notification.png

  1. NotificationManagerService是整个模块的核心,它在系统启动时就会被启动并且运行在后台,负责系统中所有通知的收发、处理、展示、移除等逻辑;
  2. NotificaionManager是通知的管理类,负责发送通知、清除通知等,通过Binder方式调用NotificationManagerService;
  3. Notification是通知的实体类,里面定义通知的标题、内容、图标、跳转信息等; 调用过程从应用开始,通过NotificationManager.notify()来发出通知,NotificationManager通过binder机制把通知发送到NotificationManagerService中,NotificationManagerService再把通知分发给NotificationListeners中设置的监听器中,包括SystemUI、系统桌面、运动健康app和其他第三方注册的app中。

通知发送流程(基于Android10源码)如下,从发送通知->NotificationManagerService处理->分发到通知监听服务。

步骤分析

下面的分析会基于上面的流程图按步骤拆解。

1、NotificationChannel和Notification

    public void sendNotification(View view) {
        String id = "channel_1";
        String des = "des_1";
        NotificationChannel channel = new NotificationChannel(id, des, NotificationManager.IMPORTANCE_MIN);
        notificationManager.createNotificationChannel(channel);
        Notification notification = new Notification.Builder(MainActivity.this, id)
                .setContentTitle("你好")
                .setContentText("您有一条新通知")
                .setSmallIcon(R.drawable.icon)
                .setStyle(new Notification.MediaStyle())
                .setAutoCancel(false)
                .build();
        notificationManager.notify(1, notification);
    }

通过NotificationManager创建一个新的或者获取一个已经创建的通知渠道,然后通过Builder新建一个 Notification 对象,最后通过 NotificationManager#notify() 方法将 Notification 发送出去。

2、NotificationManager#notifyAsUser

    public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
    {
      	//获取系统通知服务,通过Binder方式调用
        INotificationManager service = getService();
        String pkg = mContext.getPackageName();
        try {
          // 做一些声音、图标、contentView的优化和校验工作。
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,fixNotification(notification), user.getIdentifier());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

notifyAsUser方法里会调用fixNotification(Notification notification)对通知进行预处理,比如通知小图标处理,图片资源裁剪,低内存兼容等,然后直接调用NotificationManagerService#enqueueNotificationWithTag(),这里通过binder调用进入system_server进程。

3、NotificationManagerService#enqueueNotificationInternal

void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid,
            final int callingPid, final String tag, final int id, final Notification notification,
            int incomingUserId) {
        // 检查调用uid是否有权限发送消息
        final int notificationUid = resolveNotificationUid(opPkg, pkg, callingUid, userId);
				
    	//检查分类合法性
        checkRestrictedCategories(notification);

        // 继续修正通知,主要是对fullScreenIntent处理
	fixNotification(notification, pkg, userId);
       
        ...
          
         // 获取通知channel信息,校验是否规范
        final NotificationChannel channel = mPreferencesHelper.getNotificationChannel(pkg,
                notificationUid, channelId, false /* includeDeleted */);
        if (channel == null) {
            boolean appNotificationsOff = mPreferencesHelper.getImportance(pkg, notificationUid)
                    == NotificationManager.IMPORTANCE_NONE;

            if (!appNotificationsOff) {
                doChannelWarningToast("Developer warning for package "" + pkg + ""\n" +
                        "Failed to post notification on channel "" + channelId + ""\n" +
                        "See log for more details");
            }
            return;
        }

    	//构造StatusBarNotification对象,用来分发给监听服务,包括SystemUI等
        final StatusBarNotification n = new StatusBarNotification(
                pkg, opPkg, id, tag, notificationUid, callingPid, notification,
                user, null, System.currentTimeMillis());
    
    	//构造NotificationRecord对象,在framework层使用
        final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
     ...

    	//检查应用发送通知的速率、通知总数(单应用最多25)等,决定能否发送
        if (!checkDisqualifyingFeatures(userId, notificationUid, id, tag, r,
                r.sbn.getOverrideGroupKey() != null)) {
            return;
        }

        // 给pendingIntents加白名单,比如省电模式、后台启动activity等白名单。
        if (notification.allPendingIntents != null) {
            final int intentCount = notification.allPendingIntents.size();
            if (intentCount > 0) {
                final ActivityManagerInternal am = LocalServices
                        .getService(ActivityManagerInternal.class);
                final long duration = LocalServices.getService(
                        DeviceIdleController.LocalService.class).getNotificationWhitelistDuration();
                for (int i = 0; i < intentCount; i++) {
                    PendingIntent pendingIntent = notification.allPendingIntents.valueAt(i);
                    if (pendingIntent != null) {
                        am.setPendingIntentWhitelistDuration(pendingIntent.getTarget(),
                                WHITELIST_TOKEN, duration);
                        am.setPendingIntentAllowBgActivityStarts(pendingIntent.getTarget(),
                                WHITELIST_TOKEN, (FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER
                                        | FLAG_SERVICE_SENDER));
                    }
                }
            }
        }

        mHandler.post(new EnqueueNotificationRunnable(userId, r));
    }

这里的代码运行在system_server进程,主要有几个关键点:

  1. 获取通知channel信息,Android 8.0之后不设置channel的通知是无法发送的;
  2. 除了系统的通知和已注册了监听器的应用外,其他app的通知都会限制通知数上限和通知频率上限;
  3. 将 notification 的 PendingIntent 加入到白名单,比如省电模式、后台启动activity等白名单;
  4. 把notification 进一步封装为 StatusBarNotificationNotificationRecord,最后封装到一个异步线程 EnqueueNotificationRunnable 中。
  5. StatusBarNotification主要面向客户端,仅包含用户需要知道的信息,如通知包名、id、key等信息,最后回调给监听者的就是这个对象。 NotificationRecord主要面向框架层,除了持有StatusBarNotification实例外,还封装了各种通知相关的信息,如channel、sound(通知铃声)、vibration(震动效果)等等,这些信息在服务端处理通知的时候需要用到。

4、EnqueueNotificationRunnable

protected class EnqueueNotificationRunnable implements Runnable {
        private final NotificationRecord r;
        private final int userId;

        EnqueueNotificationRunnable(int userId, NotificationRecord r) {
            this.userId = userId;
            this.r = r;
        };

        @Override
        public void run() {
            synchronized (mNotificationLock) {
              	//把通知加到队列中,ArrayList<NotificationRecord> mEnqueuedNotifications = new ArrayList<>()
                mEnqueuedNotifications.add(r);
                 //定时取消功能
                scheduleTimeoutLocked(r);

                final StatusBarNotification n = r.sbn;
               
              //根据key来查找NotificationRecord,有的话保持相同的排序
                NotificationRecord old = mNotificationsByKey.get(n.getKey());
                if (old != null) {
                    //复制排序信息
                    r.copyRankingInformation(old);
                }

               ......
                
                //处理NotificationGroup的信息
                handleGroupedNotificationLocked(r, old, callingUid, callingPid);

                 .....

                if (mAssistants.isEnabled()) {
                    //通知助手(NotificationAssistants),处理通知
                    mAssistants.onNotificationEnqueued(r);
                    mHandler.postDelayed(new PostNotificationRunnable(r.getKey()),
                            DELAY_FOR_ASSISTANT_TIME);
                } else {
                    mHandler.post(new PostNotificationRunnable(r.getKey()));
                }
            }
        }
    }

主要关注点:

  1. 把NotificationRecord加到队列中;
  2. 定时取消功能,构造通知的时候可以通过setTimeout方法设置;
  3. 更新NotificationGroup信息;
  4. mHandler 是WorkerHandler类的一个实例,在 NotificationManagerService#onStart方法中被创建,所以 EnqueueNotificationRunnable 的run方法会运行在system_server的主线程。

5、PostNotificationRunnable

protected class PostNotificationRunnable implements Runnable {
        private final String key;

        PostNotificationRunnable(String key) {
            this.key = key;
        }

        @Override
        public void run() {
            synchronized (mNotificationLock) {
                try {
                    NotificationRecord r = null;
                    int N = mEnqueuedNotifications.size();
                    for (int i = 0; i < N; i++) {
                        final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                        if (Objects.equals(key, enqueued.getKey())) {
                            r = enqueued;
                            break;
                        }
                    }

                    r.setHidden(isPackageSuspendedLocked(r));
                    NotificationRecord old = mNotificationsByKey.get(key);
                    final StatusBarNotification n = r.sbn;
                    final Notification notification = n.getNotification();
                 
                  //根据key查找Record,有就更新,没有就新增
                    int index = indexOfNotificationLocked(n.getKey());
                    if (index < 0) {
                        mNotificationList.add(r);
                        mUsageStats.registerPostedByApp(r);
                        r.setInterruptive(isVisuallyInterruptive(null, r));
                    } else {
                        old = mNotificationList.get(index);
                        mNotificationList.set(index, r);
                        mUsageStats.registerUpdatedByApp(r, old);
                        // 确保没有丢失前台服务的flag
                        notification.flags |=
                                old.getNotification().flags & FLAG_FOREGROUND_SERVICE;
                        r.isUpdate = true;
                        r.setTextChanged(isVisuallyInterruptive(old, r));
                    }

                    mNotificationsByKey.put(n.getKey(), r);

                    // 前台服务设置flag
                    if ((notification.flags & FLAG_FOREGROUND_SERVICE) != 0) {
                        notification.flags |= Notification.FLAG_ONGOING_EVENT
                                | Notification.FLAG_NO_CLEAR;
                    }

                    applyZenModeLocked(r);
                   //对mNotificationList进行排序
                    mRankingHelper.sort(mNotificationList);
										
                    if (notification.getSmallIcon() != null) {
                      //发送通知
                        StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
                        mListeners.notifyPostedLocked(r, old);
                        if (oldSbn == null || !Objects.equals(oldSbn.getGroup(), n.getGroup())) {
                          //发送group通知
                            mHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    mGroupHelper.onNotificationPosted(
                                            n, hasAutoGroupSummaryLocked(n));
                                }
                            });
                        }
                    } else {
                        //没有小图标,移除通知
                        if (old != null && !old.isCanceled) {
                            mListeners.notifyRemovedLocked(r,
                                    NotificationListenerService.REASON_ERROR, null);
                            mHandler.post(new Runnable() {
                                @Override
                                public void run() {
                                    mGroupHelper.onNotificationRemoved(n);
                                }
                            });
                        }
               
                    }

                    if (!r.isHidden()) {
                      	//处理铃声、震动等
                        buzzBeepBlinkLocked(r);
                    }
                    maybeRecordInterruptionLocked(r);
                } finally {
                  //最后移除队列中的NotificationRecord
                    int N = mEnqueuedNotifications.size();
                    for (int i = 0; i < N; i++) {
                        final NotificationRecord enqueued = mEnqueuedNotifications.get(i);
                        if (Objects.equals(key, enqueued.getKey())) {
                            mEnqueuedNotifications.remove(i);
                            break;
                        }
                    }
                }
            }
        }
    }

主要做了

  1. 判断通知新增还是刷新;
  2. 刷新NotificationGroup;
  3. 铃声、震动处理;
  4. 调用NotificationListeners#notifyPostedLocked方法。

6、NotificationListeners

 private void notifyPostedLocked(NotificationRecord r, NotificationRecord old,
                boolean notifyAllListeners) {
            // Lazily initialized snapshots of the notification.
            StatusBarNotification sbn = r.sbn;
            StatusBarNotification oldSbn = (old != null) ? old.sbn : null;
            TrimCache trimCache = new TrimCache(sbn);

            for (final ManagedServiceInfo info : getServices()) {
                boolean sbnVisible = isVisibleToListener(sbn, info);
                boolean oldSbnVisible = oldSbn != null ? isVisibleToListener(oldSbn, info) : false;
                ......

                final NotificationRankingUpdate update = makeRankingUpdateLocked(info);

                if (oldSbnVisible && !sbnVisible) {
                    final StatusBarNotification oldSbnLightClone = oldSbn.cloneLight();
                    mHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            //老通知可见,新通知不可见,移除通知
                            notifyRemoved(
                                    info, oldSbnLightClone, update, null, REASON_USER_STOPPED);
                        }
                    });
                    continue;
                }

                final StatusBarNotification sbnToPost = trimCache.ForListener(info);
              	//发送通知
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        notifyPosted(info, sbnToPost, update);
                    }
                });
            }
        }


private void notifyPosted(final ManagedServiceInfo info,
                final StatusBarNotification sbn, NotificationRankingUpdate rankingUpdate) {
            final INotificationListener listener = (INotificationListener) info.service;
            StatusBarNotificationHolder sbnHolder = new StatusBarNotificationHolder(sbn);
            try {
                listener.onNotificationPosted(sbnHolder, rankingUpdate);
            } catch (RemoteException ex) {
                Log.e(TAG, "unable to notify listener (posted): " + listener, ex);
            }
        }

上面的流程主要是把通知发送给各监听者:

  1. 寻找匹配的ManagedServiceInfo,它里面封装了注册的通知监听服务信息;
  2. 通知监听器移除不在显示的Notification;
  3. 向监听器发送新通知的信息;
  4. onNotificationPosted传入的参数是sbnHolder而不是sbn对象,大家可以看到有个get()方法,返回的是真正的sbn对象。app收到sbnHolder后需要再次调用binder调用才能获得sbn对象。为啥呢?看sbnHolder的注释:

Wrapper for a StatusBarNotification object that allows transfer across a oneway binder without sending large amounts of data over a oneway transaction.

大体意思就是对sbn对象进行封装,通过一次one way的binder调用,避免传输大量数据。

7、NotificationListenerService

  public void onNotificationPosted(IStatusBarNotificationHolder sbnHolder,
                NotificationRankingUpdate update) {
            StatusBarNotification sbn;
            try {
                //取出真正的sbn
                sbn = sbnHolder.get(); 
            } catch (RemoteException e) {
                Log.w(TAG, "onNotificationPosted: Error receiving StatusBarNotification", e);
                return;
            }
    
	  ......
              
            synchronized (mLock) {
                applyUpdateLocked(update);
                if (sbn != null) {
                    SomeArgs args = SomeArgs.obtain();
                    args.arg1 = sbn;
                    args.arg2 = mRankingMap;
                    //交由handler处理
           mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_POSTED,
                            args).sendToTarget();
                } else {
           mHandler.obtainMessage(MyHandler.MSG_ON_NOTIFICATION_RANKING_UPDATE,
                            mRankingMap).sendToTarget();
                }
            }
 
        }
 
     
        public void handleMessage(Message msg) {
            if (!isConnected) {
                return;
            }
            switch (msg.what) {
                case MSG_ON_NOTIFICATION_POSTED: {
                    SomeArgs args = (SomeArgs) msg.obj;
                    StatusBarNotification sbn = (StatusBarNotification) args.arg1;
                    RankingMap rankingMap = (RankingMap) args.arg2;
                    args.recycle();
                    onNotificationPosted(sbn, rankingMap); 
                } 
                break;

这里先通过binder方式从system_server调回到app进程,把sbnHolder实例传过来了,sbnHolder是IStatusBarNotificationHolder.Stub对象。 然后又通过sbnHolder.get()取出真正的StatusBarNotification实例,从RemoteException的异常类型就可以判断出它又是一次远程调用。拿到StatusBarNotification实例后,使用mHandler抛给子类的onNotificationPosted处理,至此各监听器就能收到新通知了。

总结

通过上面的分析我们揭开了Android通知花样玩法背后的秘密,知道了第三方app如何监听通知、取消通知以及从源码层面分析了从app发送通知->NotificationManagerService处理通知->通知分发到NotificationListenerService的client的整个流程。

当然Android通知管理还可以有更多有趣玩法等待我们去探索!

附录

附录1

1、NotificationListenerService

NotificationListenerService主要提供了监听系统通知发送(onNotificationPosted)和取消(cancelNotification)的能力,而且开放了一些操作通知的接口。

方法作用
getActiveNotifications()获取当前通知栏所有通知组
cancelAllNotifications()删除系统中所有可被清除的通知(不能清除常驻通知)
cancelNotification(String key)删除某一个通知
snoozeNotification(String key, long durationMs)休眠通知,可清除通知栏上常驻通知
onNotificationPosted(StatusBarNotification sbn)通知发送时回调
onNotificationRemoved(StatusBarNotification sbn)通知移除时回调
onNotificationRankingUpdate(RankingMap rankingMap)通知排序发生变化时回调

2、StatusBarNotification

StatusBarNotification是我们在NotificationListenerService#onNotificationPosted()接口里面获取到的通知实例,它主要有以下重要方法。

方法作用
getId()通知的id
getTag()通知的Tag,如果没有设置返回null
getKey()通知的key,唯一标志。可以用来指定删除某条通知
getPostTime()通知发送的时间
getPackageName()通知对应的包名
isClearable()通知是否可被清除,FLAG_ONGOING_EVENT、FLAG_NO_CLEAR
getNotification()获取通知对象

hi, 我是快手电商的小强~

快手电商无线技术团队正在招贤纳士🎉🎉🎉! 我们是公司的核心业务线, 这里云集了各路高手, 也充满了机会与挑战. 伴随着业务的高速发展, 团队也在快速扩张. 欢迎各位高手加入我们, 一起创造世界级的电商产品~

热招岗位: Android/iOS 高级开发, Android/iOS 专家, Java 架构师, 产品经理(电商背景), 测试开发... 大量 HC 等你来呦~

内部推荐请发简历至 >>>我们的邮箱: hr.ec@kuaishou.com <<<, 备注我的花名成功率更高哦~ 😘