ภาพรวม
บทความนี้แสดงวิธีใช้ NotificationListenerService เพื่ออ่าน Notification ทุกข้อความที่เข้ามาบนอุปกรณ์ คล้ายบทความก่อนหน้า (ID: 1004061542) แต่เน้นที่การ "อ่านทั้งหมด" รวมถึง Notification ที่มีอยู่แล้วตอนเริ่ม Service
ความแตกต่างจากบทความก่อนหน้า
- ใช้
onListenerConnected()ดึง Notification Active ที่มีอยู่แล้ว - แสดงข้อมูลครบถ้วน: Title, Text, BigText, Time, Package, UID
- จัดการ Lifecycle ให้ถูกต้อง (Register/Unregister Listener)
โค้ดหลัก (เหมือนบทความก่อนหน้า แต่เพิ่มรายละเอียด)
NotificationService.java - เวอร์ชันครบถ้วน
package app.tutorialspoint.com.notifyme;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import androidx.annotation.RequiresApi;
public class NotificationService extends NotificationListenerService {
private static final String TAG = "NotifListenerService";
private static NotificationListener listener;
public interface NotificationListener {
void onNotificationReceived(NotificationData data);
void onNotificationRemoved(String packageName, int id);
void onListenerConnected();
}
public static class NotificationData {
public String packageName;
public String title;
public String text;
public String bigText;
public long postTime;
public int id;
public String tag;
public int uid;
@Override
public String toString() {
return "[" + packageName + "] " + title + ": " + text +
(bigText != null && !bigText.isEmpty() ? " | Big: " + bigText : "") +
" | Time: " + postTime;
}
}
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "Service created");
}
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
NotificationData data = extractNotificationData(sbn);
Log.i(TAG, "Posted: " + data.toString());
if (listener != null) {
listener.onNotificationReceived(data);
}
}
@Override
public void onNotificationRemoved(StatusBarNotification sbn) {
Log.i(TAG, "Removed: " + sbn.getPackageName() + " ID: " + sbn.getId());
if (listener != null) {
listener.onNotificationRemoved(sbn.getPackageName(), sbn.getId());
}
}
@Override
public void onListenerConnected() {
super.onListenerConnected();
Log.d(TAG, "Listener connected - fetching active notifications");
// ดึง Notification ที่ Active อยู่แล้ว (ก่อน Service เริ่มทำงาน)
StatusBarNotification[] activeNotifications = getActiveNotifications();
for (StatusBarNotification sbn : activeNotifications) {
NotificationData data = extractNotificationData(sbn);
Log.i(TAG, "Active: " + data.toString());
if (listener != null) {
listener.onNotificationReceived(data);
}
}
if (listener != null) {
listener.onListenerConnected();
}
}
@Override
public void onListenerDisconnected() {
super.onListenerDisconnected();
Log.d(TAG, "Listener disconnected");
listener = null;
}
private NotificationData extractNotificationData(StatusBarNotification sbn) {
NotificationData data = new NotificationData();
data.packageName = sbn.getPackageName();
data.id = sbn.getId();
data.tag = sbn.getTag();
data.uid = sbn.getUid();
data.postTime = sbn.getPostTime();
android.app.Notification notification = sbn.getNotification();
if (notification != null && notification.extras != null) {
Bundle extras = notification.extras;
data.title = extras.getString(android.app.Notification.EXTRA_TITLE, "");
data.text = extras.getString(android.app.Notification.EXTRA_TEXT, "");
data.bigText = extras.getString(android.app.Notification.EXTRA_BIG_TEXT, "");
// ข้อมูลเพิ่มเติม
// CharSequence[] lines = extras.getCharSequenceArray(Notification.EXTRA_LINES); // InboxStyle
// Bitmap picture = (Bitmap) extras.getParcelable(Notification.EXTRA_LARGE_ICON); // BigPicture
}
return data;
}
public static void setListener(NotificationListener listener) {
NotificationService.listener = listener;
}
}
MainActivity.java - แสดงผลแบบ Real-time
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.text.method.ScrollingMovementMethod;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity implements NotificationService.NotificationListener {
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);
txtView.setMovementMethod(new ScrollingMovementMethod()); // เลื่อนอ่านได้
NotificationService.setListener(this);
Button btnCreateNotification = findViewById(R.id.btnCreateNotification);
btnCreateNotification.setOnClickListener(v -> createTestNotification());
Button btnOpenSettings = findViewById(R.id.btnOpenSettings);
btnOpenSettings.setOnClickListener(v -> openNotificationAccessSettings());
Button btnClear = findViewById(R.id.btnClear);
btnClear.setOnClickListener(v -> txtView.setText("Cleared\n"));
}
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 from " + System.currentTimeMillis())
.setContentText("Short text")
.setStyle(new NotificationCompat.BigTextStyle()
.bigText("This is a very long big text that demonstrates reading big text content from notification extras. " +
"It can contain multiple lines and detailed information."))
.setTicker("Ticker text")
.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 onNotificationReceived(NotificationService.NotificationData data) {
runOnUiThread(() -> {
String log = "\n--- NEW ---\n" + data.toString();
txtView.append(log);
});
}
@Override
public void onNotificationRemoved(String packageName, int id) {
runOnUiThread(() -> {
txtView.append("\n--- REMOVED ---\n" + packageName + " ID: " + id);
});
}
@Override
public void onListenerConnected() {
runOnUiThread(() -> txtView.append("\n>>> Listener Connected - Reading active notifications..."));
}
@Override
protected void onDestroy() {
super.onDestroy();
NotificationService.setListener(null);
}
}
Layout เพิ่มปุ่ม Clear (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">
<LinearLayout
android:id="@+id/buttonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_alignParentTop="true">
<Button
android:id="@+id/btnCreateNotification"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Create Test Notification" />
<Button
android:id="@+id/btnOpenSettings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Open Notification Access Settings" />
<Button
android:id="@+id/btnClear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Clear Log"
android:backgroundTint="#FF9800" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/buttonContainer"
android:layout_marginTop="16dp">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Notification Listener Service - Reading All Notifications\nWaiting..."
android:textAppearance="?android:attr/textAppearanceMedium" />
</ScrollView>
</RelativeLayout>
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
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>
ขั้นตอนการทดสอบ
- ติดตั้งและรันแอป
- กด "Open Notification Access Settings" → เปิด Permission ให้แอป
- กลับมาแอป → จะเห็น "Listener Connected" และ Notification ที่ Active อยู่แล้ว
- กด "Create Test Notification" หลายครั้ง → ดู Log เพิ่มขึ้นทีละข้อความ
- สวายปิด Notification จาก Shade → ดู Event "REMOVED"
- ลองส่ง Notification จากแอปอื่น (Line, Facebook, SMS) → จะถูกจับได้ทั้งหมด
ข้อมูลที่อ่านได้จาก Notification Extras
| Key (Notification.EXTRA_*) | ชนิด | คำอธิบาย |
|---|---|---|
| EXTRA_TITLE | String | หัวข้อ Notification |
| EXTRA_TEXT | String | ข้อความหลัก |
| EXTRA_BIG_TEXT | String | ข้อความขยาย (BigTextStyle) |
| EXTRA_SUB_TEXT | String | ข้อความรอง |
| EXTRA_SUMMARY_TEXT | String | ข้อความสรุป (Inbox/BigPicture) |
| EXTRA_LINES | CharSequence[] | รายการบรรทัด (InboxStyle) |
| EXTRA_LARGE_ICON | Bitmap | Large Icon |
| EXTRA_PICTURE | Bitmap | Big Picture |
| EXTRA_PEOPLE | String[] | รายชื่อคน (MessagingStyle) |
| EXTRA_MESSAGES | Parcelable[] | ข้อความแชท (MessagingStyle) |
การจัดการข้อมูล Notification (Best Practices)
- Filter Package: เก็บเฉพาะ Package ที่สนใจ (Whitelist)
- Debounce: หลีกเลี่ยงการประมวลผลซ้ำ (Key: pkg + id + tag)
- Background Thread: ประมวลผลข้อมูลหนักใน Background Thread
- Storage: บันทึกลง Room Database พร้อม Timestamp
- Privacy: ไม่ Log ข้อมูลส่วนตัว, เข้ารหัสถ้าบันทึก
ข้อจำกัดสำคัญ
- ไม่สามารถอ่าน Notification ที่มาก่อน Service เริ่มทำงาน (ยกเว้น Active Notifications)
- ไม่สามารถอ่าน Notification ที่ User Dismiss แล้ว (ก่อน Listener Connected)
- บางแอปตั้ง Flag
FLAG_NO_CLEARหรือเป็น System Notification อาจเข้าถึงไม่ได้ - Android 12+ จำกัดการเข้าถึง EXTRA_MESSAGES, EXTRA_PEOPLE ต้องการ Permission เพิ่ม
Play Store Policy: แอปที่ใช้ NotificationListenerService ต้องระบุเป็น "Core Functionality" ใน Play Console และผ่านการตรวจสอบพิเศษ ห้ามใช้เพื่อเก็บข้อมูลขาย/โฆษณา