文章目录
背景介绍
Android在4.3的版本中(即API 18)加入了NotificationListenerService,根据SDK的描述(AndroidDeveloper)可以知道,当系统收到新的通知或者通知被删除时,会触发NotificationListenerService的回调方法。同时在Android 4.4 中新增了Notification.extras 字段,也就是说可以使用NotificationListenerService获取系统通知具体信息,这在以前是需要用反射来实现的
主要方法
NotificationListenerService主要方法(成员变量):
getActiveNotifications() :返回当前系统所有通知到StatusBarNotification[];
onNotificationPosted(StatusBarNotification sbn) :当系统收到新的通知后出发回调;
onNotificationRemoved(StatusBarNotification sbn) :当系统通知被删掉后出发回调;
技术细节
话不多说,直接实操,首先新建个Android项目,现在新版Android Studio已经默认使用Kotlin语言,但我对Kotlin不熟悉,现在先用Java,后面有空再换成Kotlin实现
选择了Basic Views Activity
android studio默认使用gradle构建工具,若嫌弃官网下载太慢,可更换腾讯镜像https://mirrors.cloud.tencent.com/gradle/
好多年没写Android代码了,来看看有什么变化。AndroidManifest.xml基本没什么变化
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NotificationMoitor"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.NotificationMoitor">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
再看看MainActivity.java,代码如下
package com.ths.notificationmoitor;
import android.os.Bundle;
import com.google.android.material.snackbar.Snackbar;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import androidx.core.view.WindowCompat;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import com.ths.notificationmoitor.databinding.ActivityMainBinding;
import android.view.Menu;
import android.view.MenuItem;
public class MainActivity extends AppCompatActivity {
private AppBarConfiguration appBarConfiguration;
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbar);
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build();
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
binding.fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAnchorView(R.id.fab)
.setAction("Action", null).show();
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onSupportNavigateUp() {
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
return NavigationUI.navigateUp(navController, appBarConfiguration)
|| super.onSupportNavigateUp();
}
}
这个变化有点大了,androidx替代了原来的support v4,v7扩展包,ActivityMainBinding是新引入的视图绑定,早期版本是 setContentView(R.layout.activity_main);项目新建后默认有两个fragment(FirstFragment,SecondFragment),在MainActivity中未找到哪里设置默认显示FirstFragment,经过一番分析,发现在res目录下navigation/nav_graph.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/FirstFragment">
<fragment
android:id="@+id/FirstFragment"
android:name="com.ths.notificationmoitor.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">
<action
android:id="@+id/action_FirstFragment_to_SecondFragment"
app:destination="@id/SecondFragment" />
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="com.ths.notificationmoitor.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">
<action
android:id="@+id/action_SecondFragment_to_FirstFragment"
app:destination="@id/FirstFragment" />
</fragment>
</navigation>
猜想这里是定义了fragment,然后指定第一个显示的fragment,于是新建fragment_main.xml布局文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:background="@color/white"
android:layout_height="match_parent"
android:padding="16dp">
<androidx.appcompat.widget.AppCompatButton
android:text="启动服务"
android:id="@+id/button_start_service"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</androidx.appcompat.widget.AppCompatButton>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
接着再新建MainFragment.java文件,内容如下
package com.ths.notificationmoitor;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.ths.notificationmoitor.databinding.FragmentMainBinding;
public class MainFragment extends Fragment {
private FragmentMainBinding binding;
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState
) {
binding = FragmentMainBinding.inflate(inflater, container, false);
return binding.getRoot();
}
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.buttonStartService.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
接着修改刚才找到的 nav_graph.xml文件,新增
<fragment
android:id="@+id/MainFragment"
android:name="com.ths.notificationmoitor.MainFragment"
android:label="Main Fragment"
tools:layout="@layout/fragment_main" />
顶部修改默认显示项
app:startDestination="@id/MainFragment"
修改完毕,运行验证
验证成功。
新建NotificationService.java文件,继承NotificationListenerService
package com.ths.notificationmoitor;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
public class NotificationService extends NotificationListenerService {
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
super.onNotificationPosted(sbn);
//通知来源包名
String notificationPkg = sbn.getPackageName();
Bundle extras = sbn.getNotification().extras;
// 获取接收消息的抬头
String notificationTitle = extras.getString(Notification.EXTRA_TITLE);
// 获取接收消息的内容
String notificationText = extras.getString(Notification.EXTRA_TEXT);
Toast.makeText(this, notificationPkg+notificationTitle+notificationText, Toast.LENGTH_LONG).show();
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
super.onNotificationRemoved(sbn);
}
@Override
public StatusBarNotification[] getActiveNotifications() {
return super.getActiveNotifications();
}
}
修改AndroidManifest.xml,与Acitivity同级配置
<service android:name=".NotificationService" />
启动service
binding.buttonStartService.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getActivity(),NotificationService.class);
requireActivity().startService(intent);
}
});
点击启动按钮,启动服务
下面我们来模拟发送通知,看能不能监听到,我们再新建个服务来模拟发送通知,代码如下
package com.ths.notificationmoitor;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import java.text.SimpleDateFormat;
public class SendNotificationService extends Service {
private static NotificationManager mNotiManager;
private static Notification mNotification;
private int noticeId = 0;
private final Handler mHandler = new Handler();
@SuppressLint("SimpleDateFormat")
private final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
startSendNotice();
return super.onStartCommand(intent, flags, startId);
}
private final String[] smsTemplate = new String[]{"出来玩啊,有妹纸。", "哥,手头紧,借点钱。","哥,洗脚去呀。","哥,老地方,来喝酒。","哥,出来唱歌。"};
private void createNotification() {
String template = smsTemplate[(int) ((Math.random() * 9 + 1) * 10) % smsTemplate.length];
String mockSms = template + simpleDateFormat.format(System.currentTimeMillis());
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getPackageName());
builder.setWhen(System.currentTimeMillis());
builder.setSmallIcon(android.R.drawable.stat_notify_chat);
builder.setPriority(Notification.PRIORITY_MAX);
builder.setContentTitle("张三");
builder.setContentText(mockSms);
mNotification = builder.getNotification();
mNotification.flags = Notification.FLAG_AUTO_CANCEL;
mNotification.defaults = Notification.DEFAULT_SOUND;
try {
mNotiManager.notify(noticeId, mNotification);
} catch (Exception e) {
e.printStackTrace();
}
noticeId++;
}
Runnable noticeTask = new Runnable() {
@Override
public void run() {
createNotification();
mHandler.postDelayed(noticeTask, 1000 * 30);
}
};
public void startSendNotice() {
mNotiManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mNotiManager.createNotificationChannel(new NotificationChannel(getPackageName(), "通知", NotificationManager.IMPORTANCE_HIGH));//第三个参数,重要程度
}
mNotification = new Notification();
mHandler.post(noticeTask);
}
public void stopSendNotice() {
noticeId = 0;
mHandler.removeCallbacks(noticeTask);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
stopSendNotice();
super.onDestroy();
}
}
此时,已完成通知消息的模拟发送,要完成通知的监听,监听服务需要增加通知监听的权限配置
<service
android:name=".NotificationService"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
由于此权限较为敏感,需要要求用户手动点击才能开启,因此在主界面增加个按钮,引导用户点击开启,代码如下
binding.buttonOpenPermission.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!isEnabled()) {
startActivity(new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"));
} else {
Toast toast = Toast.makeText(requireActivity(), "监控器开关已打开", Toast.LENGTH_SHORT);
toast.show();
}
}
});
// 判断是否打开了通知监听权限
private boolean isEnabled() {
String pkgName = requireActivity().getPackageName();
final String flat = Settings.Secure.getString(requireActivity().getContentResolver(), "enabled_notification_listeners");
if (!TextUtils.isEmpty(flat)) {
final String[] names = flat.split(":");
for (String name : names) {
final ComponentName cn = ComponentName.unflattenFromString(name);
if (cn != null) {
if (TextUtils.equals(pkgName, cn.getPackageName())) {
return true;
}
}
}
}
return false;
}
运行效果如下
到这里,通知消息监听核心代码已完成,后面可根据自己需求做更完善的开发。