Giới thiệu
Khi ứng dụng crash (uncaught exception), thông báo đang hiển thị có thể vẫn còn lại. Giải pháp: cài đặt Thread.UncaughtExceptionHandler tùy chỉnh để hủy thông báo trước khi app chết.
Code mẫu
1. CrashHandler.java
package com.example.crashnotification;
import android.app.NotificationManager;
import android.content.Context;
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final int NOTIFICATION_ID = 12345; // ID cố định của thông báo cần xóa
private final Thread.UncaughtExceptionHandler defaultHandler;
private final NotificationManager notificationManager;
public CrashHandler(Context context) {
this.defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
try {
if (notificationManager != null) {
// Xóa thông báo cụ thể
notificationManager.cancel(NOTIFICATION_ID);
// Hoặc xóa tất cả: notificationManager.cancelAll();
}
} catch (Throwable ignored) {
// Tránh crash trong crash handler
} finally {
// Gọi handler mặc định để hệ thống xử lý crash (hiện dialog, ghi log, restart)
if (defaultHandler != null) {
defaultHandler.uncaughtException(thread, throwable);
}
}
}
}
2. MainActivity.java - Đăng ký handler
package com.example.crashnotification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
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.View;
public class MainActivity extends AppCompatActivity {
private static final String CHANNEL_ID = "crash_channel";
private static final int NOTIFICATION_ID = 12345; // Phải khớp với CrashHandler
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ==== ĐĂNG KÝ CRASH HANDLER SỚM NHẤT CÓ THỂ ====
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(this));
findViewById(R.id.btnCreateNotification).setOnClickListener(v -> createNotification());
findViewById(R.id.btnCrash).setOnClickListener(v -> {
// Giả lập crash
throw new RuntimeException("Test crash để xóa notification");
});
}
private void createNotification() {
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra("from_notification", true);
PendingIntent pendingIntent = PendingIntent.getActivity(
this, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
NotificationManager manager = getSystemService(NotificationManager.class);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("Thông báo sẽ bị xóa khi crash")
.setContentText("Bấm nút Crash để test")
.setContentIntent(pendingIntent)
.setAutoCancel(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID, "Crash Test", NotificationManager.IMPORTANCE_DEFAULT);
manager.createNotificationChannel(channel);
}
manager.notify(NOTIFICATION_ID, builder.build()); // Dùng ID cố định
}
}
3. 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_centerInParent="true"
android:layout_marginBottom="20dp"
android:text="Tạo thông báo (ID cố định)" />
<Button
android:id="@+id/btnCrash"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/btnCreateNotification"
android:text="Gây Crash (Test)" />
</RelativeLayout>
Lưu ý quan trọng
- ID cố định: Phải dùng cùng ID (12345) khi notify() và cancel().
- Đăng ký sớm: Gọi
Thread.setDefaultUncaughtExceptionHandler()ngay trongonCreate()của Application class hoặc Activity đầu tiên. - Không làm việc nặng trong handler: Chỉ cancel notification, log, rồi gọi default handler. Tránh network, DB, UI.
- Application class tốt hơn: Nên đăng ký trong
MyApplication extends Application→ onCreate() để bao phủ toàn app.
Cách tốt hơn cho Production: ProcessDeath vs Crash
Crash handler chỉ hoạt động khi Java crash (UncaughtException). Nếu process bị kill bởi hệ thống (OOM, user swipe recent), handler **không chạy**.
Giải pháp bền vững: Foreground Service + Ongoing Notification
// Trong Service
Notification ongoing = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("App đang chạy")
.setContentText("Nhấn để mở")
.setSmallIcon(R.drawable.ic_launcher)
.setOngoing(true) // Không thể vuốt xóa
.build();
startForeground(1, ongoing); // ID = 1
// Khi service stop (app đóng đúng cách):
stopForeground(true); // true = xóa notification
Cách này đảm bảo thông báo biến mất khi app dừng (crash, kill, swipe) vì service bị hủy → notification ongoing bị xóa.