Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Android

วิธีทับ (Intercept) การแจ้งเตือน Status Bar ใน Android

ภาพรวม

NotificationListenerService เป็น System Service ที่อนุญาตให้แอปอ่าน, กรอง, และตอบสนองต่อ Notification ของแอปอื่นๆ ต้องขอสิทธิ์พิเศษจากผู้ใช้ผ่าน Settings

⚠️ คำเตือน: การใช้ NotificationListenerService ต้องการความไว้วางใจจากผู้ใช้ ใช้เพื่อวัตถุประสงค์ที่ชัดเจน (Parental Control, Auto-reply, Backup) และต้องปฏิบัติตาม Play Store Policy อยเข้มงวด

ตัวอย่าง: สร้าง Notification Listener Service

ขั้นตอนที่ 1: สร้างโปรเจกต์ใหม่

Android Studio → New Project → Empty Activity (Package: app.tutorialspoint.com.notifyme)

ขั้นตอนที่ 2: สร้าง Interface สำหรับ Callback (MyListener.java)

package app.tutorialspoint.com.notifyme;

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

ขั้นตอนที่ 3: NotificationService.java (extends NotificationListenerService)

package app.tutorialspoint.com.notifyme;

import android.content.Context;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import androidx.annotation.RequiresApi;
import android.os.Build;

public class NotificationService extends NotificationListenerService {
    private static final String TAG = "NotificationService";
    private static MyListener listener;

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "Service created");
    }

    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
        if (sbn.getNotification() == null) return;
        
        String packageName = sbn.getPackageName();
        String title = "";
        String text = "";
        
        // ดึงข้อมูลจาก Notification (API 16+)
        android.app.Notification notification = sbn.getNotification();
        Bundle extras = notification.extras;
        if (extras != null) {
            title = extras.getString(android.app.Notification.EXTRA_TITLE, "");
            text = extras.getString(android.app.Notification.EXTRA_TEXT, "");
        }
        
        Log.i(TAG, "Posted: " + packageName + " | " + title + " | " + text);
        
        if (listener != null) {
            listener.onNotificationPosted(packageName, title, text);
        }
    }

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

    public static void setListener(MyListener listener) {
        NotificationService.listener = listener;
    }
    
    @Override
    public void onListenerConnected() {
        super.onListenerConnected();
        Log.d(TAG, "Listener connected");
        // ดึง Notification ที่มีอยู่แล้ว (Optional)
        StatusBarNotification[] active = getActiveNotifications();
        for (StatusBarNotification sbn : active) {
            onNotificationPosted(sbn);
        }
    }

    @Override
    public void onListenerDisconnected() {
        super.onListenerDisconnected();
        Log.d(TAG, "Listener disconnected");
        listener = null;
    }
}

ขั้นตอนที่ 4: Layout (res/layout/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="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:text="Create Test Notification" />

    <Button
        android:id="@+id/btnOpenSettings"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btnCreateNotification"
        android:layout_marginTop="8dp"
        android:text="Open Notification Access Settings" />

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/btnOpenSettings"
        android:layout_marginTop="16dp">

        <TextView
            android:id="@+id/textView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Notification Listener Service Example\nWaiting for notifications..."
            android:textAppearance="?android:attr/textAppearanceMedium" />
    </ScrollView>
</RelativeLayout>

ขั้นตอนที่ 5: MainActivity.java

package app.tutorialspoint.com.notifyme;

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

public class MainActivity extends AppCompatActivity implements MyListener {
    private TextView txtView;
    public static final String NOTIFICATION_CHANNEL_ID = "10001";
    private static final String DEFAULT_CHANNEL_ID = "default";

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

        txtView = findViewById(R.id.textView);
        
        // ลงทะเบียน Listener
        NotificationService.setListener(this);

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

        Button btnOpenSettings = findViewById(R.id.btnOpenSettings);
        btnOpenSettings.setOnClickListener(v -> openNotificationAccessSettings());
    }

    private void createTestNotification() {
        NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, DEFAULT_CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_launcher_foreground)
                .setContentTitle("Test Notification")
                .setContentText("Notification Listener Service Example")
                .setTicker("Test Ticker")
                .setAutoCancel(true)
                .setPriority(NotificationCompat.PRIORITY_DEFAULT);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(
                    NOTIFICATION_CHANNEL_ID, "Test Channel", NotificationManager.IMPORTANCE_DEFAULT);
            manager.createNotificationChannel(channel);
            builder.setChannelId(NOTIFICATION_CHANNEL_ID);
        }

        manager.notify((int) System.currentTimeMillis(), builder.build());
    }

    private void openNotificationAccessSettings() {
        Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS");
        startActivity(intent);
    }

    @Override
    public void onNotificationPosted(String packageName, String title, String text) {
        runOnUiThread(() -> {
            String log = "\n[POSTED] " + packageName + "\nTitle: " + title + "\nText: " + text;
            txtView.append(log);
            // Auto scroll
            // scrollView.fullScroll(View.FOCUS_DOWN); // ถ้ามี ScrollView
        });
    }

    @Override
    public void onNotificationRemoved(String packageName) {
        runOnUiThread(() -> {
            txtView.append("\n[REMOVED] " + packageName);
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        NotificationService.setListener(null); // ป้องกัน Memory Leak
    }
}

ขั้นตอนที่ 6: AndroidManifest.xml - สำคัญมาก

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
    package="app.tutorialspoint.com.notifyme">
    
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <!-- สำคัญ: Service ต้องมี Permission นี้ -->
        <service
            android:name=".NotificationService"
            android:label="@string/app_name"
            android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
            android:exported="false">
            <intent-filter>
                <action android:name="android.service.notification.NotificationListenerService" />
            </intent-filter>
        </service>
    </application>
</manifest>

ขั้นตอนการเปิดใช้งาน (สำคัญมาก)

  1. ติดตั้งแอปบนอุปกรณ์
  2. ไป **Settings → Apps → Special App Access → Notification access** (หรือ Settings → Notifications → Notification access)
  3. หาแอปของคุณ → Toggle **ON**
  4. ยืนยัน Dialog "Allow access to notifications?"
  5. กลับมาแอป → กด "Create Test Notification" → ดู Log ใน TextView

ข้อมูลที่เข้าถึงได้จาก StatusBarNotification

StatusBarNotification sbn;
// Package name
sbn.getPackageName();
// Notification ID
sbn.getId();
// Tag
sbn.getTag();
// UID
sbn.getUid();
// User Handle
sbn.getUser();
// Notification object
Notification notification = sbn.getNotification();
// Extras (Title, Text, BigText, etc.)
Bundle extras = notification.extras;
String title = extras.getString(Notification.EXTRA_TITLE);
String text = extras.getString(Notification.EXTRA_TEXT);
CharSequence bigText = extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
// Actions
notification.actions // Array of Action
// Post time
sbn.getPostTime();

การจัดการ Notification (API 18+)

// ลบ Notification ของแอปอื่น (ต้องมี Permission)
cancelNotification(sbn.getPackageName(), sbn.getTag(), sbn.getId());

// ลบทั้งหมดของ Package
cancelAllNotifications(packageName);

ข้อจำกัดและข้อควรระวัง

  • Permission: ต้องได้รับอนุญาตจาก User ทุกครั้งที่ติดตั้ง/อัปเดต
  • Privacy: Google Play ตรวจสอบเข้มงวด ต้องระบุเหตุผลใน Play Console
  • Performance: onNotificationPosted เรียกบน Main Thread ของ Service → ห้ามทำงานหนัก ใช้ Handler/Executor
  • Reliability: Service อาจถูก Kill โดยระบบ → ใช้ android:foregroundServiceType="dataSync" หรือ Start Foreground
  • Android 12+: ต้องการ Permission เพิ่มเติมสำหรับการเข้าถึงข้อมูลบางอย่าง

กรณีใช้งานจริง (Use Cases)

  • แอปควบคุมผู้ปกครอง (Parental Control)
  • แอปสำรองข้อความ (SMS Backup)
  • แอปตอบอัตโนมัติ (Auto-reply bot)
  • แอปจัดการ Notification (Grouper, Filter)
  • Smartwatch Companion App
ทางเลือกอื่น: หากต้องการแค่ดู Notification ของตัวเอง ไม่ต้องใช้ Listener Service ให้ใช้ NotificationManager.getActiveNotifications() (API 23+) แทน