이 튜토리얼에서는 안드로이드 앱에서 수신 전화를 프로그래밍 방식으로 자동 응답하는 방법을 단계별로 살펴봅니다. 통화 자동 응답 기능은 주로 자동 응답기, 콜센터 앱, 운전 중 핸즈프리 통화 등의 시나리오에서 활용됩니다.
구현 원리
핵심 아이디어는 헤드셋 버튼 클릭 이벤트(KEYCODE_HEADSETHOOK)를 에뮬레이트하여 시스템이 마치 사용자가 유선 이어폰의 통화 버튼을 누른 것처럼 동작하게 만드는 것입니다. 제조사(예: HTC)에 따라 브로드캐스트 방식을 함께 사용해야 하는 경우도 있어, 두 가지 방식을 모두 처리하도록 구현합니다.
1단계 — 새 프로젝트 만들기
Android Studio를 열고 File → New Project 메뉴로 이동한 뒤, 새 프로젝트 생성에 필요한 모든 정보를 입력하여 프로젝트를 만듭니다.
2단계 — 레이아웃 작성 (res/layout/activity_main.xml)
메인 화면에는 간단한 TextView 하나만 배치합니다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="https://schemas.android.com/apk/res/android"
xmlns:tools="https://schemas.android.com/tools"
xmlns:app="https://schemas.android.com/apk/res-auto"
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:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>3단계 — MainActivity 구현 (src/MainActivity.java)
메인 액티비티에서는 API 레벨에 따라 분기 처리하며, 통화 응답을 담당할 AcceptCallActivity를 새 태스크로 시작합니다.
package com.app.sample;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void acceptCall() {
Context context = null;
if (Build.VERSION.SDK_INT >= 21) {
Intent answerCalintent = new Intent(context, AcceptCallActivity.class);
answerCalintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
answerCalintent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(answerCalintent);
}
else {
Intent intent = new Intent(context, AcceptCallActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}
}4단계 — 통화 응답 액티비티 구현 (src/AcceptCallActivity.java)
이 클래스가 실제 통화 응답의 핵심입니다. 주요 동작 흐름은 다음과 같습니다.
- 키가드 상태 확인: 화면이 잠겨 있으면
FLAG_DISMISS_KEYGUARD,FLAG_TURN_SCREEN_ON,FLAG_SHOW_WHEN_LOCKED플래그를 추가해 잠금 화면 위에서도 동작할 수 있게 합니다. - 통화 상태 수신자 등록:
ACTION_PHONE_STATE_CHANGED브로드캐스트를 수신해 통화가 연결되면 액티비티를 종료합니다. - 헤드셋 훅 이벤트 전송: 먼저
input keyevent KEYCODE_HEADSETHOOK명령을 실행하고, 실패 시ACTION_MEDIA_BUTTON브로드캐스트로 대체합니다. - HTC 기기 예외 처리: HTC 기기이면서 유선 헤드셋이 연결되지 않은 경우, 헤드셋 연결 상태 브로드캐스트를 먼저 전송해야 응답이 정상 동작합니다.
package com.app.sample;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.telephony.TelephonyManager;
import android.view.KeyEvent;
import android.view.WindowManager;
import java.io.IOException;
import java.util.logging.Logger;
public class AcceptCallActivity extends Activity {
private static Logger logger =
Logger.getLogger(String.valueOf(AcceptCallActivity.class));
private static final String MANUFACTURER_HTC = "HTC";
private KeyguardManager keyguardManager;
private AudioManager audioManager;
private CallStateReceiver callStateReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
}
@Override
protected void onResume() {
super.onResume();
registerCallStateReceiver();
updateWindowFlags();
acceptCall();
}
@Override
protected void onPause() {
super.onPause();
if (callStateReceiver != null) {
unregisterReceiver(callStateReceiver);
callStateReceiver = null;
}
}
private void registerCallStateReceiver() {
callStateReceiver = new CallStateReceiver();
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
registerReceiver(callStateReceiver, intentFilter);
}
private void updateWindowFlags() {
if (keyguardManager.inKeyguardRestrictedInputMode()) {
getWindow().addFlags( WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
} else {
getWindow().clearFlags( WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
}
}
private void acceptCall() {
boolean broadcastConnected = MANUFACTURER_HTC.equalsIgnoreCase(Build.MANUFACTURER) && !audioManager.isWiredHeadsetOn();
if (broadcastConnected) {
broadcastHeadsetConnected(false);
}
try {
try {
Runtime.getRuntime().exec("input keyevent " + Integer.toString(KeyEvent.KEYCODE_HEADSETHOOK));
}
catch (IOException e) {
String enforcedPerm = "android.permission.CALL_PRIVILEGED";
Intent btnDown = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra( Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_HEADSETHOOK));
Intent btnUp = new Intent(Intent.ACTION_MEDIA_BUTTON).putExtra( Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_HEADSETHOOK));
sendOrderedBroadcast(btnDown, enforcedPerm);
sendOrderedBroadcast(btnUp, enforcedPerm);
}
}
finally {
if (broadcastConnected) {
broadcastHeadsetConnected(false);
}
}
}
private void broadcastHeadsetConnected(boolean connected) {
Intent i = new Intent(Intent.ACTION_HEADSET_PLUG);
i.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
i.putExtra("state", connected ? 1 : 0);
i.putExtra("name", "mysms");
try {
sendOrderedBroadcast(i, null);
}
catch (Exception e) {
}
}
private class CallStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
finish();
}
}
}5단계 — 매니페스트 설정 (Manifests/AndroidManifest.xml)
매니페스트 파일에 메인 액티비티를 등록합니다. 실제 서비스에 적용할 때는 필요에 따라 READ_PHONE_STATE 같은 통화 관련 권한도 함께 선언해야 할 수 있습니다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="https://schemas.android.com/apk/res/android" package="com.app.sample" >
<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>
</application>
</manifest>앱 실행 및 테스트
이제 애플리케이션을 실행해 보겠습니다. 실제 안드로이드 스마트폰을 컴퓨터에 연결한 상태라고 가정합니다. Android Studio에서 프로젝트의 액티비티 파일 중 하나를 연 뒤 툴바의 Run 아이콘을 클릭하세요. 실행 옵션 목록에서 본인의 모바일 기기를 선택하면, 기기에 아래와 같은 기본 화면이 표시됩니다.

마무리 및 참고 사항
이 예제는 안드로이드 5.0(Lollipop, API 21) 이전 버전에서 널리 사용되던 방식입니다. 다만 최신 안드로이드 버전(API 26 이상)에서는 보안 정책 강화로 인해 TelecomManager.acceptRingingCall() 또는 InCallService API를 사용하는 것이 권장되며, 일부 기능은 시스템 권한 없이는 동작하지 않을 수 있습니다. 레거시 기기 지원이 필요한 프로젝트라면 이 예제를 참고하되, 대상 OS 버전에 맞는 접근 방식을 함께 검토하시기 바랍니다.