Giới thiệu
Bài viết này hướng dẫn phát hiện trạng thái cuộc gọi (đang reo, nghe máy, kết thúc) bằng BroadcastReceiver lắng nghe action android.intent.action.PHONE_STATE. Yêu cầu quyền READ_PHONE_STATE (dangerous permission – cần xin runtime).
Các bước thực hiện
Bước 1: Tạo dự án mới
Mở Android Studio, chọn File → New Project và tạo dự án.
Bước 2: Thiết kế giao diện (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"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="App chạy ngầm phát hiện cuộc gọi"
android:textStyle="bold"
android:textColor="@color/colorPrimary" />
</RelativeLayout>
Bước 3: Code MainActivity – xin quyền (src/MainActivity.java)
import android.Manifest;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private static final int PERMISSION_REQUEST_READ_PHONE_STATE = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
requestPhoneStatePermission();
}
private void requestPhoneStatePermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_PHONE_STATE},
PERMISSION_REQUEST_READ_PHONE_STATE);
} else {
Toast.makeText(this, "Đã có quyền READ_PHONE_STATE", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_READ_PHONE_STATE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Quyền đã được cấp", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Quyền bị từ chối - không thể phát hiện cuộc gọi", Toast.LENGTH_SHORT).show();
}
}
}
}
Bước 4: Tạo BroadcastReceiver (src/CallDetector.java)
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.TelephonyManager;
import android.widget.Toast;
public class CallDetector extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
try {
String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE);
String incomingNumber = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER);
if (TelephonyManager.EXTRA_STATE_RINGING.equals(state)) {
// Đang reo
String msg = "Cuộc gọi đến từ: " + (incomingNumber != null ? incomingNumber : "Số ẩn");
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
// TODO: Xử lý logic tại đây (lưu log, hiển thị UI, chặn cuộc gọi...)
} else if (TelephonyManager.EXTRA_STATE_OFFHOOK.equals(state)) {
// Đang nghe máy
Toast.makeText(context, "Đang 통 hóa", Toast.LENGTH_SHORT).show();
} else if (TelephonyManager.EXTRA_STATE_IDLE.equals(state)) {
// Kết thúc/treo máy
Toast.makeText(context, "Cuộc gọi kết thúc", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
Bước 5: Đăng ký Receiver trong AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android"
package="app.com.sample">
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<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>
<!-- Đăng ký BroadcastReceiver cho PHONE_STATE -->
<receiver android:name=".CallDetector" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
</receiver>
</application>
</manifest>
Chạy ứng dụng
Cài đặt app, cấp quyền READ_PHONE_STATE. Gọi điện thoại đến thiết bị -> Toast hiển thị số điện thoại đang gọi đến. Receiver hoạt động ngay cả khi app đóng (do khai báo trong Manifest).
⚠️ Thay đổi quan trọng từ Android 10 (API 29)
- READ_PHONE_STATE không còn cho phép đọc số điện thoại gọi đến (EXTRA_INCOMING_NUMBER trả về null) trừ khi app là **Default Dialer** hoặc có quyền đặc biệt
READ_CALL_LOG - Android 11+ hạn chế BroadcastReceiver trong Manifest cho PHONE_STATE – nên đăng ký động (registerReceiver) khi app chạy foreground
- Để chặn cuộc gọi: dùng
CallScreeningService(API 26+) hoặcTelecomManager
Cách hiện đại (API 26+): CallScreeningService
// Manifest
<service android:name=".CallScreeningService"
android:permission="android.permission.BIND_SCREENING_SERVICE">
<intent-filter>
<action android:name="android.telecom.CallScreeningService" />
</intent-filter>
</service>
// Service
public class CallScreeningService extends CallScreeningService {
@Override
public void onScreenCall(Call.Details details) {
// Xử lý: chặn, cho phép, chuyển voicemail...
CallResponse.Builder response = new CallResponse.Builder();
if (shouldBlock(details.getHandle())) {
response.setRejectCall(true).setSilenceRinger(true);
}
respondToCall(details.getCallId(), response.build());
}
}
Lưu ý
- Toast trong BroadcastReceiver có thể không hiển thị nếu app bị kill – nên dùng Notification hoặc log
- Test trên thiết bị thật – emulator không mô phỏng cuộc gọi thật tốt
intent.getAction() trước khi xử lý