我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Android无障碍监听通知的实战过程

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Android无障碍监听通知的实战过程

监听通知

Android 中的 AccessibilityService 可以监听通知信息的变化,首先需要创建一个无障碍服务,这个教程可以自行百度。在无障碍服务的配置文件中,需要以下配置:

<accessibility-service
	...
	android:accessibilityEventTypes="其他内容|typeNotificationStateChanged"
	android:canRetrieveWindowContent="true" />

然后在 AccessibilityService 的 onAccessibilityEvent 方法中监听消息:

override fun onAccessibilityEvent(event: AccessibilityEvent?) {
    when (event.eventType) {
        AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED -> {
            Log.d(Tag, "Notification: $event")
        }
    }
}

当有新的通知或 Toast 出现时,在这个方法中就会收到 AccessibilityEvent 。

另一种方案是通过 NotificationListenerService 进行监听,这里不做详细介绍了。两种方案的应用场景不同,推荐使用 NotificationListenerService 而不是无障碍服务。stackoverflow 上一个比较好的回答:

It depends on WHY you want to read it. The general answer would be Notification Listener. Accessibility Services are for unique accessibility services. A user has to enable an accessibility service from within the Accessibility Service menu (where TalkBack and Switch Access are). Their ability to read notifications is a secondary ability, to help them achieve the goal of creating assistive technologies (alternative ways for people to interact with mobile devices).

Whereas, Notification Listeners, this is their primary goal. They exist as part of the context of an app and as such don't need to be specifically turned on from the accessibility menu.

Basically, unless you are in fact building an accessibility service, you should not use this approach, and go with the generic Notification Listener.

无障碍服务监听通知逻辑

从用法中可以看出一个关键信息 -- TYPE_NOTIFICATION_STATE_CHANGED ,通过这个事件类型入手,发现它用于两个类中:

  • ToastPresenter:用于在应用程序进程中展示系统 UI 样式的 Toast 。
  • NotificationManagerService:通知管理服务。

ToastPresenter

ToastPresenter 的 trySendAccessibilityEvent 方法中,构建了一个 TYPE_NOTIFICATION_STATE_CHANGED 类型的消息:

public void trySendAccessibilityEvent(View view, String packageName) {
    if (!mAccessibilityManager.isEnabled()) {
        return;
    }
    AccessibilityEvent event = AccessibilityEvent.obtain(
            AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
    event.setClassName(Toast.class.getName());
    event.setPackageName(packageName);
    view.dispatchPopulateAccessibilityEvent(event);
    mAccessibilityManager.sendAccessibilityEvent(event);
}

这个方法的调用在 ToastPresenter 中的 show 方法中:

public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
        int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
        @Nullable ITransientNotificationCallback callback) {
    // ... 
    trySendAccessibilityEvent(mView, mPackageName); 
    // ...
}

而这个方法的调用就是在 Toast 中的 TN 类中的 handleShow 方法。

Toast.makeText(this, "", Toast.LENGTH_SHORT).show()

在 Toast 的 show 方法中,获取了一个 INotificationManager ,这个是 NotificationManagerService 在客户端暴露的 Binder 对象,通过这个 Binder 对象的方法可以调用 NMS 中的逻辑。

也就是说,Toast 的 show 方法调用了 NMS :

public void show() {
    // ...
    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    final int displayId = mContext.getDisplayId();

    try {
        if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
            if (mNextView != null) {
                // It's a custom toast
                service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
            } else {
                // It's a text toast
                ITransientNotificationCallback callback = new CallbackBinder(mCallbacks, mHandler);
                service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);
            }
        } else {
            service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
        }
    } catch (RemoteException e) {
        // Empty
    }
}

这里是 enqueueToast 方法中,最后调用:

private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,
				@Nullable ITransientNotification callback, int duration, int displayId,
				@Nullable ITransientNotificationCallback textCallback) {
  	// ...
		record = getToastRecord(callingUid, callingPid, pkg, token, text, callback, duration, windowToken, displayId, textCallback);
  	// ...
}

getToastRecord 中根据 callback 是否为空产生了不同的 Toast :

private ToastRecord getToastRecord(int uid, int pid, String packageName, IBinder token,
        @Nullable CharSequence text, @Nullable ITransientNotification callback, int duration,
        Binder windowToken, int displayId,
        @Nullable ITransientNotificationCallback textCallback) {
    if (callback == null) {
        return new TextToastRecord(this, mStatusBar, uid, pid, packageName, token, text,duration, windowToken, displayId, textCallback);
    } else {
        return new CustomToastRecord(this, uid, pid, packageName, token, callback, duration, windowToken, displayId);
    }
}

两者的区别是展示对象的不同:

  • TextToastRecord 因为 ITransientNotification 为空,所以它是通过 mStatusBar 进行展示的:

        @Override
        public boolean show() {
            if (DBG) {
                Slog.d(TAG, "Show pkg=" + pkg + " text=" + text);
            }
            if (mStatusBar == null) {
                Slog.w(TAG, "StatusBar not available to show text toast for package " + pkg);
                return false;
            }
            mStatusBar.showToast(uid, pkg, token, text, windowToken, getDuration(), mCallback);
            return true;
        }
    
  • CustomToastRecord 调用 ITransientNotification 的 show 方法:

        @Override
        public boolean show() {
            if (DBG) {
                Slog.d(TAG, "Show pkg=" + pkg + " callback=" + callback);
            }
            try {
                callback.show(windowToken);
                return true;
            } catch (RemoteException e) {
                Slog.w(TAG, "Object died trying to show custom toast " + token + " in package "
                        + pkg);
                mNotificationManager.keepProcessAliveForToastIfNeeded(pid);
                return false;
            }
        }
    

    这个 callback 最在 Toast.show() 时传进去的 TN :

    TN tn = mTN;
    service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
    

    也就是调用到了 TN 的 show 方法:

            @Override
            @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
            public void show(IBinder windowToken) {
                if (localLOGV) Log.v(TAG, "SHOW: " + this);
                mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
            }
    
    

TN 的 show 方法中通过 mHandler 来传递了一个类型是 SHOW 的消息:

            mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, mToken);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };

而这个 Handler 在处理 SHOW 时,会调用 handleShow(token) 这个方法里面也就是会触发 ToastPresenter 的 show 方法的地方:

public void handleShow(IBinder windowToken) {
    // If a cancel/hide is pending - no need to show - at this point
    // the window token is already invalid and no need to do any work.
    if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
        return;
    }
    if (mView != mNextView) {
        // remove the old view if necessary
        handleHide();
        mView = mNextView;
      	// 【here】
        mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY, mHorizontalMargin, mVerticalMargin, new CallbackBinder(getCallbacks(), mHandler));
    }
}

本章节最开始介绍到了 ToastPresenter 的 show 方法中会调用 trySendAccessibilityEvent 方法,也就是从这个方法发送类型是 TYPE_NOTIFICATION_STATE_CHANGED 的无障碍消息给无障碍服务的。

NotificationManagerService

在通知流程中,是通过 NMS 中的 sendAccessibilityEvent 方法来向无障碍发送消息的:

void sendAccessibilityEvent(Notification notification, CharSequence packageName) {
    if (!mAccessibilityManager.isEnabled()) {
        return;
    }

    AccessibilityEvent event =
        AccessibilityEvent.obtain(AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
    event.setPackageName(packageName);
    event.setClassName(Notification.class.getName());
    event.setParcelableData(notification);
    CharSequence tickerText = notification.tickerText;
    if (!TextUtils.isEmpty(tickerText)) {
        event.getText().add(tickerText);
    }

    mAccessibilityManager.sendAccessibilityEvent(event);
}

这个方法的调用有两处,均在 NMS 的 buzzBeepBlinkLocked 方法中,buzzBeepBlinkLocked 方法是用来处理通知是否应该发出铃声、震动或闪烁 LED 的。省略无关逻辑:

int buzzBeepBlinkLocked(NotificationRecord record) {
    // ...
    if (!record.isUpdate && record.getImportance() > IMPORTANCE_MIN && !suppressedByDnd) {
        sendAccessibilityEvent(notification, record.getSbn().getPackageName());
        sentAccessibilityEvent = true;
    }

    if (aboveThreshold && isNotificationForCurrentUser(record)) {
        if (mSystemReady && mAudioManager != null) {
            // ...
            if (hasAudibleAlert && !shouldMuteNotificationLocked(record)) {
                if (!sentAccessibilityEvent) {
                    sendAccessibilityEvent(notification, record.getSbn().getPackageName());
                    sentAccessibilityEvent = true;
                }
                // ...
            } else if ((record.getFlags() & Notification.FLAG_INSISTENT) != 0) {
                hasValidSound = false;
            }
        }
    }
    // ...
}

buzzBeepBlinkLocked 的调用路径有两个:

  • handleRankingReconsideration 方法中 RankingHandlerWorker (这是一个 Handler)调用 handleMessage 处理 MESSAGE_RECONSIDER_RANKING 类型的消息:

    @Override
    public void handleMessage(Message msg) {
    		switch (msg.what) {
    				case MESSAGE_RECONSIDER_RANKING:
    						handleRankingReconsideration(msg);
    						break;
    				case MESSAGE_RANKING_SORT:
    						handleRankingSort();
    						break;
    				}
    }
    

    handleRankingReconsideration 方法中调用了 buzzBeepBlinkLocked :

    private void handleRankingReconsideration(Message message) {
        // ...
        synchronized (mNotificationLock) {
            // ...
            if (interceptBefore && !record.isIntercepted()
                    && record.isNewEnoughForAlerting(System.currentTimeMillis())) {
                buzzBeepBlinkLocked(record);
            }
        }
        if (changed) {
            mHandler.scheduleSendRankingUpdate();
        }
    }
    
  • PostNotificationRunnable 的 run 方法。

PostNotificationRunnable

这个东西是用来发送通知并进行处理的,例如提示和重排序等。

PostNotificationRunnable 的构建和 post 在 EnqueueNotificationRunnable 中。在 EnqueueNotificationRunnable 的 run 最后,进行了 post:

public void run() {
		// ...
    // tell the assistant service about the notification
    if (mAssistants.isEnabled()) {
        mAssistants.onNotificationEnqueuedLocked(r);
        mHandler.postDelayed(new PostNotificationRunnable(r.getKey()), DELAY_FOR_ASSISTANT_TIME);
    } else {
        mHandler.post(new PostNotificationRunnable(r.getKey()));
    }
}

EnqueueNotificationRunnable 在 enqueueNotificationInternal 方法中使用,enqueueNotificationInternal 方法是 INotificationManager 接口中定义的方法,它的实现在 NotificationManager 中:

    public void notifyAsPackage(@NonNull String targetPackage, @Nullable String tag, int id,
            @NonNull Notification notification) {
        INotificationManager service = getService();
        String sender = mContext.getPackageName();

        try {
            if (localLOGV) Log.v(TAG, sender + ": notify(" + id + ", " + notification + ")");
            service.enqueueNotificationWithTag(targetPackage, sender, tag, id,
                    fixNotification(notification), mContext.getUser().getIdentifier());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @UnsupportedAppUsage
    public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
    {
        INotificationManager service = getService();
        String pkg = mContext.getPackageName();

        try {
            if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    fixNotification(notification), user.getIdentifier());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

一般发送一个通知都是通过 NotificationManager 或 NotificationManagerCompat 来发送的,例如:

NotificationManagerCompat.from(this).notify(1, builder.build());

NotificationManagerCompat 中的 notify 方法本质上调用的是 NotificationManager:

// NotificationManagerCompat
public void notify(int id, @NonNull Notification notification) {
    notify(null, id, notification);
}

public void notify(@Nullable String tag, int id, @NonNull Notification notification) {
    if (useSideChannelForNotification(notification)) {
        pushSideChannelQueue(new NotifyTask(mContext.getPackageName(), id, tag, notification));
        // Cancel this notification in notification manager if it just transitioned to being side channelled.
        mNotificationManager.cancel(tag, id);
    } else {
        mNotificationManager.notify(tag, id, notification);
    }
}

mNotificationManager.notify(tag, id, notification) 中的实现:

public void notify(String tag, int id, Notification notification) {
    notifyAsUser(tag, id, notification, mContext.getUser());
}

public void cancel(@Nullable String tag, int id) {
    cancelAsUser(tag, id, mContext.getUser());
}

串起来了,最终就是通过 NotificationManager 的 notify 相关方法发送通知,然后触发了通知是否要触发铃声/震动/LED 闪烁的逻辑,并且在这个逻辑中,发送出了无障碍消息。

总结

到此这篇关于Android无障碍监听通知的文章就介绍到这了,更多相关Android无障碍监听通知内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Android无障碍监听通知的实战过程

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Android无障碍监听通知怎么实现

本篇内容主要讲解“Android无障碍监听通知怎么实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Android无障碍监听通知怎么实现”吧!监听通知Android 中的 Accessibili
2023-07-02

Android Service的跨进程通信实战&amp;Service/AIDL远程调用过程解析(Android Q)

Service的跨进程通信实战设想这么一个场景,我们有2个APP(或者2个进程,均可),其中一个APP需要提供一个Person相关的服务(该服务有一个名为eat的接口),我们叫它PersonServer;另一个APP需要访问PersonSe
2022-06-06

Android监听手机电话状态与发送邮件通知来电号码的方法(基于PhoneStateListene实现)

本文实例讲述了Android监听手机电话状态与发送邮件通知来电号码的方法。分享给大家供大家参考,具体如下: 在android中可以用PhoneStateListener来聆听手机电话状态(比如待机、通话中、响铃等)。本例是通过它来监听手机电
2022-06-06

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录