본문 바로가기
깡샘 코틀린

18-1 스마트폰 정보 구하기

by 농농씨 2023. 7. 12.

전화 상태 변화 감지하기 - PhoneStateListener

앱에서 스마트폰의 상태를 파악해야 할 때가 있다. 예를 들면 전화가 걸려 오는 순간을 감지하거나 서비스 상태가 변경되는 순간을 감지하고 싶은 경우이다. 이처럼 스마트폰의 상태를 파악하는 방법은 PhoneStateListener를 이용하는 방법과 TelephonyCallback을 이용하는 방법이 있다.

PhoneStateListener를 이용하는 방법은 안드로이드 초기 버전부터 제공되던 방식인데 안드로이드 12(API 레벨 31)에서 deprecated 되었고, 대신 TelephonyCallback이 추가되었다. 그러나 워낙 오랫동안 PhoneStateListener를 사용해왔고 안드로이드 12 하위 버전을 대상으로는 계속 사용해야 하므로 이 책에서는 PhoneStateListener와 TelephonyCallback을 모두 살펴보자.

 

먼저 PhoneStateListener를 이용하려면 1️⃣PhoneStateListener를 상속받은 클래스를 작성하고 2️⃣그 클래스의 객체를 TelephonyManager에 등록해야 한다. 그러면 스마트폰의 전화 관련 상태가 바뀔 때마다 PhoneStateListener의 다음과 같은 함수가 자동으로 호출된다.

  • onCallforwardingIndicatorChanged(boolean cfi): 통화 전달 상태 변경
  • onCallStateChanged(int state, String icomingNumber): 통화 상태 변경
  • onCellLocationChanged(CellLocation location): 폰의 기지국 위치 변경
  • onDataActivity(int direction): 데이터 송수신 활동
  • onDataConnectionStateChanged(int state, int networkType): 데이터 연결 상태 변경
  • onMessageWaitingIndicatorChanged(boolean mwi): 메시지 대기 상태 변경
  • onServicestateChanged(serviceState serviceState): 단말기의 서비스 상태 변경
  • onSignalStrengthsChanged(signalStrength signalStrength): 신호 세기 변경

이 중에서 필요한 함수만 재정의해 놓으면 앱에서 상태 변화를 감지할 수 있다.

// 상태 변화 감지
val phoneStateListener = object : PhoneStateListener() {
// PhoneStateListener를 상속받은 클래스를 작성함
    override fun onServiceStatechanged(serviceState: ServiceState?) {
    // 단말기의 서비스 상태 변경을 감지하는 함수를 재정의함
        super.onServiceStateChanged(serviceState)
        (... 생략 ...)
    }
}

그런 다음 getSystemService() 함수로 TeleponyManager 객체를 얻고 이 객체의 listen() 함수에 PhoneStateListener객체를 등록한다.

// 전화 매니저 얻기
val manager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
// TelephonyManager 객체 얻고
manager.listen(phoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE)
// 이 객체의 listen 함수에 PhoneStateListener 객체 등록함

listen() 함수의 두 번째 매개변수에는 감지할 상태를 지정해야 하는데 이때 PhoneStateListener 뒤에 다음과 같은 상수를 사용한다.

  • LISTEN_CALL_FORWARDIG_INDICATOR: 통화 전달 지시자
  • LISTEN_CALL_STATE: 통화 상태
  • LISTEN_CELL_LOCATION: 기지국 위치
  • LISTEN_DATA_ACTIVITY: 데이터 송수신 활동
  • LISTEN_DATA_CONNECTION_STATE: 데이터 연결 상태
  • LISTEN_MESSAGE_WAITING_INDICATOR: 메시지 대기 지시자
  • LISTEN_SERVICE_STATE: 단말기의 서비스 상태
  • LISTEN_SIGNAL_STRENGTHS: 신호 세기

만약 여러 개의 상태를 함께 감지하려면 상수를 or 연산자로 나열하면 된다

// 여러 상태 감지
manager.listen(phoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE or
    PhoneStateListner.LISTEN_CALL_STATE)
    // manager.listen 함수의 두번째 매개변수에 단말기의 서비스 상태와 통화 상태를 감지하도록 상수를 or 연산자로 나열함

 

그리고 상태 변화 감지를 해제할 때는 listen() 함수에 LISTEN_NONE 상수를 지정한다.

// 상태 감지 해제
manager.listen(phoneStateListener PhoneStateListener.LISTEN_NONE)
// 상태 변화 감지 해제 위해 listen() 함수에 LISTEN_NONE 상수 지정함

 

앞에서 나열한 스마트폰의 통화 관련 상태가 바뀔 때 호출되는 함수 몇 가지를 살펴보자. 먼저 onServiceStateChanged() 함수는 비행 모드나 긴급 통화 등 스마트폰의 서비스 상태가 바뀌는 순간에 호출된다.

// 상태 감지 해제
override fun onServiceStateChanged(serviceState: ServiceState?) {
    when (serviceState?.state) {
    // 매개변수의 state 값으로 아래의 상수들이 각 상황으로의 변화가 감지될 때 전달됨
        Service.State.STATE_EMERGENCY_ONLY -> Log.d("kkang", "EMERGENCY_ONLY....")
        Service.State.STATE_OUT_OF_SERVICE -> Log.d("kkang", "OUT_OF_SERVICE....")
        Service.State.STATE_POSER_OFF -> Log.d("kkang", "POWER_OFF....")
        Service.State.STATE_IN_SERVICE -> Log.d("kkang", "IN_SERVICE....")
    }
}

스마트폰의 서비스 상태가 바뀌면 onServiceStateChanged() 함수가 호출되면서 매개변수인 ServiceState 객체의 state 값으로 전달된다. 이때 서비스 상태는 ServiceState 뒤에 다음과 같은 상수로 전달된다.

  • STATE_IN_SERVICE: 서비스 가능 상태
  • STATE_EMERGENCY_ONLY: 긴급 통화만 가능한 상태
  • STATE_OUT_OF_SERVICE: 서비스 불가 상태
  • STATE_POWER_OFF: 비행 모드 등 전화 기능을 꺼놓은 상태

 

이번에는 전화가 걸려오는 상태를 감지하는 onCallStateChanged() 함수를 알아보자.

// 전화가 걸려오는 상태 감지
override fun onCallStateChanged(state: Int, phoneNumber: String?) {
    when (state) {
        TelephonyManager.CALL_STATE_IDLE -> Log.d("kkang", "IDLE..")
        TelephonyManager.CALL_STATE_RINGING -> Log.d("kkang", "RINGING..")
        TelephonyManager.CALL_STATE_OFFHOOK -> Log.d("kkang", "OFFHOOK..")
    }
}

onCallStateChanged() 함수의 첫 번째 매개변수로 전화가 걸려오는 상태를 구별할 수 있으며 TelephonyManager 뒤에 다음과 같은 상수가 전달된다.

  • CALL_STATE_IDLE: 통화 대기 상태
  • CALL_STATE_RINGING: 벨이 울리는 상태
  • CALL_STATE_OFFHOOK: 통화 중인 상태

 

전화 상태 변화 감지하기 - TelephonyCallback

안드로이드 12 버전부터는 스마트폰의 각종 상태 변화를 감지할 때 TelephonyCallback을 이용한다. TelephonyCallback을 구현한 객체를 TelephonyManager에 등록하면 상태 변화가 발생했을 때 TelephonyCallback의 함수가 자동으로 호출된다.

// TelephonyCallback을 이용한 전화 상태 변화 감지
telephonyManager = getSystemService(Context.TELEPONY_SERVICE) as TelephonyManager
// TelephonyCallback을 구현한 객체
if (Build.VERISON.SDK_INT >= Build.VERSION_CODES.S) {
    telephonyManager.registerTelephonyCallback(
    // TelephonyCallback을 구현한 객체를 registerTelephonyCallback()함수로 TelephonyManager에 등록함
        mainExecutor,
        object : TelephonyCallback(), telephonyCallback.CallStateListener {
        // TelephonyCallback을 상속받는 동시에, 전화상태변화를 감지하는 인터페이스를 구현함
            override fun onCallStateChanged(state: Int) {
                when (state) {
                     TelephonyManager.CALL_STAE_IDLE -> {
                         Log.d("kkang", "IDLE")
                     }
                     TelephonyManager.CALL_STAE_OFFHOOK -> {
                         Log.d("kkang", "OFFHOOK")
                     }
                     TelephonyManager.CALL_STAE_RINGING -> {
                         Log.d("kkang", "RINGING")
                     }
                }
            }
        })
}

TelephonyCallback을 TelephonyManager에 등록할 때는 registerTelephonyCallback() 함수를 이용한다. 이 함수에 등록하는 객체는 1️⃣TelephonyCallback을 상속받아야 하며, 2️⃣감지하려는 상태에 따라 API가 제공하는 인터페이스를 구현해야 한다. 위 코드에서는 전화가 걸려오는 상황을 감지하는 TelephonyCallback.CallStateListener 인터페이스를 구현했다. 위 코드가 정상적으로 전화 상태를 감지하려면 다음의 3️⃣퍼미션을 등록해야 한다.

// 스마트폰 상태 퍼미션
<uses-permission android:name="android.permission.READ_PHONE_STATE" />

TelephonyCallback.CallStateListener 이외에 각종 상태 변화를 감지하는 인터페이스가 준비되어 있다. 대표적으로 다음과 같다.

  • TelephonyCallback.CellLocationListener: 셀(cell) 위치 변화 감지
  • TelephonyCallback.ServiceStateListener: 서비스 상태 변화 감지
  • TelephonyCallback.SignalStrengthsListener: 신호 세기 변화 감지
  • TelephonyCallback.DataActivityListener: 데이터 송수신 상태 변화 감지
  • TelephonyCallback.DataConnectionStateListener: 데이터 접속 상태 변화 감지
  • TelephonyCallback.CallStateListener: 전화 상태 변화 감지

 

네트워크 제공 국가, 사업자, 전화번호 얻기 - TelephonyManager

TelephonyManager는 네트웤 제공 국가, 사업자, 전화번호 등을 반환하는 다음 함수도 제공한다.

  • getNetworkCountryIso(): 네트워크 제공 국가
  • getNetworkOperatorName(): 네트워크 제공 사업자
  • getLine1Number(): 스마트폰의 전화번호(개정판 삭제)

먼저 getLine1Number() 함수를 이용해(개정판 삭제) 사용자 스마트폰의 전화번호를 추출하려면 다음과 같은 퍼미션이 필요하다.

// 사용자 전화번호를 읽는 퍼미션
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />

사용자 스마트폰의 전화번호를 추출하는 방법은 API Level 33 이전까지는 TelephonyManager의 getLine1Number() 함수를 이용했지만 API Level 33부터 이 함수는 deprecated 되었다. 33 버전부터는 SubscriptionManager의 getPhoneNumber()를 권장한다.

SubscriptionManager는 현재 활성화된 SIM에 대한 정보와 현재 네트워크가 로밍 중인지 등을 확인할 수 있는 방법을 제공하는 시스템 서비스이다. 다음 코드는 전화번호를 추출하는 SubscriptionManager의 getPhoneNumber() 함수이다.

// getPhoneNumber() 함수로 전화번호 추출하기
var phoneNumber: String = "unknown"
// 전화번호 저장할 객체 생성

if (Build.VERSION.SDK_INT >= Build.VERISON_CODES.TIRAMISU) { // 최소 SDK 버전 설정
    val subscriptionManager =
        getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE) as SubscriptionManager
        // 현재 활성화된 SIM에 대한 정보와 현재 네트워크가 로밍 중인지 등을 확인할 수 있는 방법을 제공하는 시스템 서비스
        // 를 구현?한?객체?
    for (sjbscriptionInfo: SubscriptionInfo in subscriptionManager.ativeSubscription InfoList) {
        val activeSubscriptionId: Int = subscriptionInfo.subscriptionId
        phoneNumber = subscriptionManager.getPhoneNumber(activeSubscriptionId)
        // subscriptionManager의 getPhoneNumber() 함수로 전화번호 추출하기
    }
} else { // 버전 낮으면 deprecated된 기존의 line1Number 함수 이용하여 전화번호 추출하기
    // deprecat4ed.........
    phoneNumber = telephonyManager.line1Number
}

 

networkCountryIso 속성은 네트워크 제공 국가 정보이므로 국내라면 'kr'이고, networkOperatorName 속성은 네트워크 사업자명이다.

// 네트워크 국가, 사업자, 전화번호 얻기.
val countryIso - telephonyManager.networkCountryIso
val operatorName = telephonyManager.networkOperatorName
val phoneNumber = telephonyManager.line1Number

 

네트워크 접속 정보 - ConnectivityManager

앞에서 살펴본 TelephonyManager는 스마트폰의 전화 정보가 필요할 때 이용한다. 그런데 서버와 데이터 통신을 하려면 네트워크 접속 정보가 필요하다. 현재 스마트폰이 네트워크가 가능한지, 가능하다면 이동 통신망에 접속되어 있는지 와이파이에 접속되어있는지 등을 파악해야 한다. 이처럼 네트워크 접속 정보를 파악할 때는 ConnectivityManager를 이용한다.

ConnectivityManager를 이용하려면 먼저 다음 퍼미션을 선언해야 한다.

// 네트워크 상태 접근 퍼미션
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

 

getActiveNetwork() 함수를 이용하는 방법

스마트폰에서 네트워크가 가능한지, 가능하다면 어떤 네트워크에 접속되었는지 알아볼 때는 ConnectivityManager의 getActiveNetwork() 함수로 Network 객체를 얻어서 이용한다. 그런데 이 함수는 API 레벨 23부터 제공한다. 만약 23 하위 버전에서도 실행되는 앱을 개발한다면 ConnectivityManager의 getActiveNetworkInfo() 함수를 이용해 NetworkInfo 객체를 얻어야 한다.

// 네트워크 접속 가능 여부
private fun isNetworkAvailable(): Boolean {
    val connectivityManager = getSystemSerivce(Context.CONNECTIVITY_SERVICE)
        as connectivityManager
        // 객체 생성
    if (Build.VERISON.SDK_INT >= Build.VERSION_CODES.M) { // 최소 SDK 버전 설정
        val nw = connectivitymanager.activeNetwork ?: return false
        // 네트워크 가능한지 알아보기 위해 ConnectivityManager의 getActiveNetwork() 함수로 객체 얻음
        val actNw = connectivityManager.getNetworkCapabilities(nw) ?: return false
        return when {
            actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
            // hasTransport() 함수로 와이파이인지 이동통신망인지 알아냄
            // 와이파이 통해 네트워크 연결됐을 경우
                Log.d("kkang", "wifi available")
                true
            }
            actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
            // 데이터 네트워크일 경우
                Log.d("kkang", "cellular available")
                true
            }
            else -> false
        }
    } else { // 만약 API 레벨 23 하위 버전이라면 getActiveNetworkInfo() 함수 이용
        return connedctivitymanager.activeNetworkInfo?.isConnected ?: false
    }
}

activititynetwork 정보가 null이면 현재 스마트폰은 네트워크에 접속할 수 없다는 이야기이다. activityNetwork로 얻은 Network 객체를 다시 getNetworkCapabilities() 함수의 매개변수로 지정하면 현재 접속된 네트워크 정보를 얻을 수 있다. 이 함수의 반환값은 Networkapabilities 객체이며 hasTransport() 함수를 이용해 와이파이에 접속된 상태인지 아니면 이동 통신망에 접속된 상태인지를 알 수 있다.

 

requestNetwork() 함수를 이용하는 방법

네트워크 접속 정보를 파악할 때 getActiveNetwork() 함수 말고 connetivityManager 클래스의 requestNetwork() 함수를 이용할 수도 있다. 다만 이 함수를 이용하려면 다음 퍼미션을 선언해야 한다.

// CHANGE_NETWORK_STATE 퍼미션
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />

 

먼저 requestNetwork() 함수는 다음처럼 정의되어 있다.

public void requestNetwork(NetworkRequest request, ConnectivityManager.NetworkCallback 
networkCallback)

첫 번째 매개변수에는 네트워크 타입을 나타내는 NetworkRequest 객체를 지정한다. NetworkRequest의 addCapability()와 addTransportType() 함수를 이용하면 네트워크 타입을 지정할 수 있다.

// 네트워크 타입 지정
val networkReq: NetworkRequest = NetworkRequest.Builder()
// 네트워크 타입 나타내는 networkRequest 객체 생성
    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    // 네트워크 타입 지정하는 함수, 일반적인 데이터 통신 나타내는 상수 전달함
    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
    // 네트워크 타입 지정하는 함수, 이동통신망 의미하는 상수 전달
    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
    // 와이파이 의미하는 상수 전달
    .build()

appCapability() 함수에 전달한 상수는 일반적인 데이터 통신을 의미하며 addTransportType() 함수에 전달한 두 상수는 각각 이동 통신망(TRANSPORT_CELLULAR)와 와이파이(TRANSPORT_WIFI)를 의미한다.

이렇게 NetworkRequest 객체에 네트워크 타입을 설정했다면 이 객체를 requestNetwork() 함수의 두 번째 매개변수(?)로 지정한다. 그러면 콜백 함수가 자동으로 호출되면서 해당 네트워크를 이용할 수 있는지를 판별한다.

(첫번째 매개변수에 네트워크 타입을 나타내는 객체를 지정하고, 그 객체를 또 두번째 매개변수로 지정한다고? 뭔말이지?)

// 네트워크 접속 가능 여부
conManager.requestNetwork(networkReq, object : ConnectivityManager.NetworkCallback() {
    override fun onAvailable(network: Network) {
    // 이 함수 호출되면 네트워크 가능하다는 의미
        super.onAvailable(network)
        Log.d("kkang", "NetworkCallback...onAvailable....")
    }
    override fun onUnavailable() {
    // 이 함수 호출되면 네트워크 불가능하다는 의미
        super.onUnavailable()
        Log.d("kkang", "NetworkCallback...onUnavailable....")
    }
})

만약 onAvailable() 함수가 호출되면 지정한 타입의 네트워크가 가능하다는 의미이고, onUnavailable() 함수가 호출되면 불가능하다는 의미이다. 참고로 requestNetwork() 함수는 API 레벨 21에서 추가되었으므로 minSdkVersion을 21보다 낮춰서 개발한다면 @RequiresApi(Build.VERSION_CODES.LOLLIPOP)과 같은 호환성 코드를 추가해 줘야 한다.