Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Android

Cách phát hiện thông báo mới trên Android (NotificationListenerService)

Giới thiệu

Sử dụng NotificationListenerService để theo dõi tất cả thông báo được đăng/bỏ bởi các ứng dụng khác. Cần quyền truy cập thông báo (Notification Access) từ người dùng.

Yêu cầu & Quy trình cấp quyền

  1. App khai báo service với permission BIND_NOTIFICATION_LISTENER_SERVICE.
  2. Người dùng vào: Cài đặt → Ứng dụng → Quyền truy cập đặc biệt → Truy cập thông báo → Bật cho app.
  3. Hoặc dẫn dắt user qua Intent: android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS.

Kiến trúc code

1. MyListener Interface (Callback)

package com.example.notificationdetector;

public interface NotificationCallback {
    void onNotificationPosted(String packageName, String title, String text);
    void onNotificationRemoved(String packageName);
}

2. NotificationListenerService

package com.example.notificationdetector;

import android.content.Context;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;

public class NotificationDetectorService extends NotificationListenerService {

    private static final String TAG = "NotifDetector";
    private static NotificationCallback callback;

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
        String pkg = sbn.getPackageName();
        String title = "", text = "";
        
        if (sbn.getNotification().extras != null) {
            title = sbn.getNotification().extras.getString("android.title", "");
            text = sbn.getNotification().extras.getString("android.text", "");
        }
        
        Log.d(TAG, "Posted: " + pkg + " | " + title + " | " + text);
        
        if (callback != null) {
            callback.onNotificationPosted(pkg, title, text);
        }
    }

    @Override
    public void onNotificationRemoved(StatusBarNotification sbn) {
        String pkg = sbn.getPackageName();
        Log.d(TAG, "Removed: " + pkg);
        
        if (callback != null) {
            callback.onNotificationRemoved(pkg);
        }
    }

    public static void setCallback(NotificationCallback cb) {
        callback = cb;
    }
}

3. MainActivity.java

package com.example.notificationdetector;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements NotificationCallback {

    private TextView txtLog;
    private static final String CHANNEL_ID = "test_channel";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        txtLog = findViewById(R.id.txtLog);
        NotificationDetectorService.setCallback(this);

        findViewById(R.id.btnCreateNotification).setOnClickListener(v -> createTestNotification());
    }

    private void createTestNotification() {
        NotificationManager manager = getSystemService(NotificationManager.class);
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "default")
                .setSmallIcon(R.drawable.ic_launcher_foreground)
                .setContentTitle("Test Detector")
                .setContentText("Kiểm tra NotificationListenerService")
                .setAutoCancel(true);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID, "Test", NotificationManager.IMPORTANCE_DEFAULT);
            manager.createNotificationChannel(channel);
            builder.setChannelId(CHANNEL_ID);
        }
        manager.notify((int) System.currentTimeMillis(), builder.build());
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.action_settings) {
            startActivity(new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"));
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onNotificationPosted(String pkg, String title, String text) {
        runOnUiThread(() -> txtLog.append("\n[POST] " + pkg + ": " + title + " - " + text));
    }

    @Override
    public void onNotificationRemoved(String pkg) {
        runOnUiThread(() -> txtLog.append("\n[REMOVE] " + pkg));
    }
}

4. activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btnCreateNotification"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_alignParentEnd="true"
        android:layout_alignParentTop="true"
        android:text="Tạo thông báo test" />

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/btnCreateNotification">

        <TextView
            android:id="@+id/txtLog"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Notification Listener Log"
            android:textAppearance="?android:attr/textAppearanceMedium" />

    </ScrollView>
</RelativeLayout>

5. res/menu/menu_main.xml

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:app="https://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/action_settings"
        android:title="Cấp quyền Truy cập thông báo"
        app:showAsAction="never" />
</menu>

6. AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
    package="com.example.notificationdetector">

    <application ...>
        <activity android:name=".MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".NotificationDetectorService"
            android:label="@string/app_name"
            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
            <intent-filter>
                <action android:name="android.service.notification.NotificationListenerService" />
            </intent-filter>
        </service>
    </application>
</manifest>

Ứng dụng thực tế

  • App tự động trả lời tin nhắn (Auto-reply SMS/Chat).
  • App quản lý thông báo: gom nhóm, lọc rác, ưu tiên.
  • App phụ huynh: theo dõi thông báo con cái.
  • App accessibility: đọc thông báo to cho người khiếm thị.
⚠️ Chính sách Google Play: Quyền Notification Access được coi là quyền nhạy cảm cao. Cần lý do chính đáng, minh bạch cho người dùng, và tuân thủ Play Console Declaration.