Если вы являетесь разработчиком Android и вам нужно определить, находится ли ваш телефон в состоянии ожидания, снята (состояние приема) или находится в состоянии вызова, это руководство было создано для вас. Целью здесь является показать вам, как реализовать обратные вызовы телефона в Android, позволяющие обнаруживать вызовы на ваш телефон.
О диспетчере телефонии
В этой теме наиболее важным классом, о котором мы поговорим, является TelephonyManager
. Этот класс использует прослушиватель PhoneStateListener
для прослушивания обновлений службы телефонии.
Будет создан экземпляр объекта типа TelephonyManager
, и он будет прослушивать обновления Context.TELEPHONY_SERVICE
. Для отслеживания обновлений состояний телефонии, таких как PhoneStateListener.LISTEN_DATA_CONNECTION_STATE
, PhoneStateListener.LISTEN_CELL_INFO
и других, будет создан класс PhoneCallback
, выходящий из PhoneStateListener
.
С помощью TelephonyManager
можно получить доступ к службам телефонии устройства, а с помощью метода TelephonyManager.listen(PhoneStateListener listener, int events)
можно отслеживать всю информацию, TelephonyManager.listen(PhoneStateListener listener, int events)
телефоном.
Класс PhoneStateListener
получает обратные вызовы, когда наблюдается какое-то событие TELEPHONY_SERVICE. В этом руководстве будет использоваться флаг PhoneStateListener.LISTEN_CALL_STATE
. Он будет отслеживать и прослушивать состояние вызова телефона.
Как можно прочитать в документации PhoneStateListener (см. Раздел «Дальнейшее чтение» ниже), существуют другие константы, которые можно установить в int events
аргумента int events
метода TelephonyManager.listen
. Например: LISTEN_SIGNAL_STRENGTHS
прослушивает изменения уровня сетевого сигнала (сотовый). Но в этом уроке будет использоваться флаг LISTEN_CALL_STATE
, который прослушивает изменения состояния вызова устройства.
Исходный код!
Чтобы показать, как работают эти обратные вызовы, будет создано приложение для Android. Он будет иметь основное Activity
, пользовательский класс для инкапсуляции обратного вызова PhoneStateListener
и класс для управления разрешениями времени выполнения, которые необходимо объявить в версии Android выше 6.0 (Marshmallow).
Прежде всего, необходимо объявить разрешения на доступ к состоянию телефона. Какие из них будут использоваться этим приложением? Чтобы узнать это, нужно взглянуть на класс PhoneStateListener
. Исходный код этого класса показан ниже (для API 25).
package android.telephony; import android.telephony.CellInfo; import android.telephony.CellLocation; import android.telephony.ServiceState; import android.telephony.SignalStrength; import java.util.List; public class PhoneStateListener { public static final int LISTEN_CALL_FORWARDING_INDICATOR = 8 ; public static final int LISTEN_CALL_STATE = 32 ; public static final int LISTEN_CELL_INFO = 1024 ; public static final int LISTEN_CELL_LOCATION = 16 ; public static final int LISTEN_DATA_ACTIVITY = 128 ; public static final int LISTEN_DATA_CONNECTION_STATE = 64 ; public static final int LISTEN_MESSAGE_WAITING_INDICATOR = 4 ; public static final int LISTEN_NONE = 0 ; public static final int LISTEN_SERVICE_STATE = 1 ; /** @deprecated */ @Deprecated public static final int LISTEN_SIGNAL_STRENGTH = 2 ; public static final int LISTEN_SIGNAL_STRENGTHS = 256 ; public PhoneStateListener () { throw new RuntimeException( "Stub!" ); } public void onServiceStateChanged (ServiceState serviceState) { throw new RuntimeException( "Stub!" ); } /** @deprecated */ @Deprecated public void onSignalStrengthChanged ( int asu) { throw new RuntimeException( "Stub!" ); } public void onMessageWaitingIndicatorChanged ( boolean mwi) { throw new RuntimeException( "Stub!" ); } public void onCallForwardingIndicatorChanged ( boolean cfi) { throw new RuntimeException( "Stub!" ); } public void onCellLocationChanged (CellLocation location) { throw new RuntimeException( "Stub!" ); } public void onCallStateChanged ( int state, String incomingNumber) { throw new RuntimeException( "Stub!" ); } public void onDataConnectionStateChanged ( int state) { throw new RuntimeException( "Stub!" ); } public void onDataConnectionStateChanged ( int state, int networkType) { throw new RuntimeException( "Stub!" ); } public void onDataActivity ( int direction) { throw new RuntimeException( "Stub!" ); } public void onSignalStrengthsChanged (SignalStrength signalStrength) { throw new RuntimeException( "Stub!" ); } public void onCellInfoChanged (List<CellInfo> cellInfo) { throw new RuntimeException( "Stub!" ); } }
Создавая собственный класс, расширяющий PhoneStateListener
, некоторые методы могут быть переопределены. Например:
-
public void onCallStateChanged(int state, String incomingNumber)
-> Требуется разрешение READ_PHONE_STATE. -
public void onCellLocationChanged(CellLocation location)
-> Требуется разрешение ACCESS_COARSE_LOCATION. -
public void onCallForwardingIndicatorChanged(boolean cfi)
-> Требуется разрешение READ_PHONE_STATE. -
public void onMessageWaitingIndicatorChanged(boolean mwi)
-> Требуется разрешение READ_PHONE_STATE.
права доступа
Можно увидеть более двух разрешений: READ_PHONE_STATE и ACCESS_COARSE_LOCATION. Они будут объявлены в файле AndroidManifest.xml
.
Обратите внимание на статические разрешения, которые будут использоваться в приложении. Но с API 23 (версия 6.0 — Marshmallow) в Android была создана новая схема разрешений. Теперь разрешения должны быть проверены во время выполнения. Для этого будет создан класс для инкапсуляции необходимых прав доступа. Этот класс будет называться PermissionUtils
.
Этот класс реализует проверку во время выполнения двух разрешений, объявленных в файле AndroidManifest.xml
, в методах canAccessCoarseLocation()
и canReadPhoneState()
(используя внутри них метод hasPermission()
). Также метод alertAndFinish()
закрывает приложение, если пользователь отказывается разрешить запуск приложения во время выполнения.
На изображении ниже показано, как эти разрешения проверяются во время выполнения.
Пользовательский класс PhoneStateListener
В этом классе происходит вся магия. Как было сказано ранее, расширяя класс PhoneStateListener
, некоторые методы могут быть переопределены.
Чтобы показать содержимое каждого обратного вызова, мы будем использовать класс Log
.
public class PhoneCallback extends PhoneStateListener { //-------------------------------------------------- // Constants //-------------------------------------------------- public static final String LOG_TAG = "PhoneCallback" ; //-------------------------------------------------- // Attributes //-------------------------------------------------- private final TextView mTextView; //-------------------------------------------------- // Constructor //-------------------------------------------------- public PhoneCallback (TextView textView) { mTextView = textView; } //-------------------------------------------------- // Methods //-------------------------------------------------- private String serviceStateToString ( int serviceState) { switch (serviceState) { case ServiceState.STATE_IN_SERVICE: return "STATE_IN_SERVICE" ; case ServiceState.STATE_OUT_OF_SERVICE: return "STATE_OUT_OF_SERVICE" ; case ServiceState.STATE_EMERGENCY_ONLY: return "STATE_EMERGENCY_ONLY" ; case ServiceState.STATE_POWER_OFF: return "STATE_POWER_OFF" ; default : return "UNKNOWN_STATE" ; } } private String callStateToString ( int state) { switch (state) { case TelephonyManager.CALL_STATE_IDLE: return "\nonCallStateChanged: CALL_STATE_IDLE, " ; case TelephonyManager.CALL_STATE_RINGING: return "\nonCallStateChanged: CALL_STATE_RINGING, " ; case TelephonyManager.CALL_STATE_OFFHOOK: return "\nonCallStateChanged: CALL_STATE_OFFHOOK, " ; default : return "\nUNKNOWN_STATE: " + state + ", " ; } } //-------------------------------------------------- // PhoneStateListener //-------------------------------------------------- @Override public void onCellInfoChanged (List<CellInfo> cellInfo) { super .onCellInfoChanged(cellInfo); Log.i(LOG_TAG, "onCellInfoChanged: " + cellInfo); } @Override public void onDataActivity ( int direction) { super .onDataActivity(direction); switch (direction) { case TelephonyManager.DATA_ACTIVITY_NONE: Log.i(LOG_TAG, "onDataActivity: DATA_ACTIVITY_NONE" ); break ; case TelephonyManager.DATA_ACTIVITY_IN: Log.i(LOG_TAG, "onDataActivity: DATA_ACTIVITY_IN" ); break ; case TelephonyManager.DATA_ACTIVITY_OUT: Log.i(LOG_TAG, "onDataActivity: DATA_ACTIVITY_OUT" ); break ; case TelephonyManager.DATA_ACTIVITY_INOUT: Log.i(LOG_TAG, "onDataActivity: DATA_ACTIVITY_INOUT" ); break ; case TelephonyManager.DATA_ACTIVITY_DORMANT: Log.i(LOG_TAG, "onDataActivity: DATA_ACTIVITY_DORMANT" ); break ; default : Log.w(LOG_TAG, "onDataActivity: UNKNOWN " + direction); break ; } } @Override public void onServiceStateChanged (ServiceState serviceState) { super .onServiceStateChanged(serviceState); String message = "onServiceStateChanged: " + serviceState + "\n" ; message += "onServiceStateChanged: getOperatorAlphaLong " + serviceState.getOperatorAlphaLong() + "\n" ; message += "onServiceStateChanged: getOperatorAlphaShort " + serviceState.getOperatorAlphaShort() + "\n" ; message += "onServiceStateChanged: getOperatorNumeric " + serviceState.getOperatorNumeric() + "\n" ; message += "onServiceStateChanged: getIsManualSelection " + serviceState.getIsManualSelection() + "\n" ; message += "onServiceStateChanged: getRoaming " + serviceState.getRoaming() + "\n" ; message += "onServiceStateChanged: " + serviceStateToString(serviceState.getState()); Log.i(LOG_TAG, message); } @Override public void onCallStateChanged ( int state, String incomingNumber) { super .onCallStateChanged(state, incomingNumber); callStateToString(state); String message = callStateToString(state) + "incomingNumber: " + incomingNumber; mTextView.setText(message); } @Override public void onCellLocationChanged (CellLocation location) { super .onCellLocationChanged(location); String message = "" ; if (location instanceof GsmCellLocation) { GsmCellLocation gcLoc = (GsmCellLocation) location; message += "onCellLocationChanged: GsmCellLocation " + gcLoc + "\n" ; message += "onCellLocationChanged: GsmCellLocation getCid " + gcLoc.getCid() + "\n" ; message += "onCellLocationChanged: GsmCellLocation getLac " + gcLoc.getLac() + "\n" ; message += "onCellLocationChanged: GsmCellLocation getPsc" + gcLoc.getPsc(); // Requires min API 9 Log.i(LOG_TAG, message); } else if (location instanceof CdmaCellLocation) { CdmaCellLocation ccLoc = (CdmaCellLocation) location; message += "onCellLocationChanged: CdmaCellLocation " + ccLoc + "\n" ;; message += "onCellLocationChanged: CdmaCellLocation getBaseStationId " + ccLoc.getBaseStationId() + "\n" ;; message += "onCellLocationChanged: CdmaCellLocation getBaseStationLatitude " + ccLoc.getBaseStationLatitude() + "\n" ;; message += "onCellLocationChanged: CdmaCellLocation getBaseStationLongitude" + ccLoc.getBaseStationLongitude() + "\n" ;; message += "onCellLocationChanged: CdmaCellLocation getNetworkId " + ccLoc.getNetworkId() + "\n" ;; message += "onCellLocationChanged: CdmaCellLocation getSystemId " + ccLoc.getSystemId(); Log.i(LOG_TAG, message); } else { Log.i(LOG_TAG, "onCellLocationChanged: " + location); } } @Override public void onCallForwardingIndicatorChanged ( boolean changed) { super .onCallForwardingIndicatorChanged(changed); } @Override public void onMessageWaitingIndicatorChanged ( boolean changed) { super .onMessageWaitingIndicatorChanged(changed); } }
Наиболее важным методом, реализованным в этом классе, является метод onCallStateChanged()
. Этот метод прослушивает изменения состояния вызова. Например, когда телефон начинает звонить, звонок переводится в режим удержания или звонок завершается. В этом методе возможно обнаруживать следующие состояния: TelephonyManager.CALL_STATE_IDLE
, TelephonyManager.CALL_STATE_RINGING
или TelephonyManager.CALL_STATE_OFFHOOK
. Это состояния, которые могут предоставить информацию о том, что происходит с состоянием телефона.
MainActivity class
С разрешениями на чтение состояния телефона и пользовательским классом для прослушивания состояний телефона, исходному коду теперь нужно просто вызвать их вместе. Это будет сделано в классе MainActivity
.
public class MainActivity extends AppCompatActivity { //-------------------------------------------------- // Constants //-------------------------------------------------- private static final String[] PERMISSIONS = { Manifest.permission.READ_PHONE_STATE, Manifest.permission.ACCESS_COARSE_LOCATION }; private static final int PERMISSION_REQUEST = 100 ; //-------------------------------------------------- // Attributes //-------------------------------------------------- private TelephonyManager mTelephonyManager; //-------------------------------------------------- // Activity Life Cycle //-------------------------------------------------- @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); checkPermissions(); } //-------------------------------------------------- // Permissions //-------------------------------------------------- @Override public void onRequestPermissionsResult ( int requestCode, String[] permissions, int [] grantResults) { switch (requestCode) { case PERMISSION_REQUEST: { isPermissionGranted(grantResults); return ; } } } private void isPermissionGranted ( int [] grantResults) { if (grantResults.length > 0 ) { Boolean permissionGranted = grantResults[ 0 ] == PackageManager.PERMISSION_GRANTED; if (permissionGranted) { callPhoneManager(); } else { PermissionUtils.alertAndFinish( this ); } } } private void checkPermissions () { // Checks the Android version of the device. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Boolean canWriteExternalStorage = PermissionUtils.canReadPhoneState( this ); Boolean canReadExternalStorage = PermissionUtils.canAccessCoarseLocation( this ); if (!canWriteExternalStorage || !canReadExternalStorage) { requestPermissions(PERMISSIONS, PERMISSION_REQUEST); } else { // Permission was granted. callPhoneManager(); } } else { // Version is below Marshmallow. callPhoneManager(); } } private void callPhoneManager () { TextView textView = (TextView)findViewById(R.id.id_text_view); mTelephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); mTelephonyManager.listen( new PhoneCallback(textView), PhoneStateListener.LISTEN_CALL_STATE | PhoneStateListener.LISTEN_CELL_INFO // Requires API 17 | PhoneStateListener.LISTEN_CELL_LOCATION | PhoneStateListener.LISTEN_DATA_ACTIVITY | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE | PhoneStateListener.LISTEN_SERVICE_STATE | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS | PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR | PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR); } }
В методе checkPermissions()
будет вызван метод checkPermissions()
. В этом методе будут проверяться разрешения Manifest.permission.ACCESS_COARSE_LOCATION
и Manifest.permission.READ_PHONE_STATE
.
При первом requestPermissions(PERMISSIONS, PERMISSION_REQUEST)
приложения, если приложение не имеет доступа к этим разрешениям, будет requestPermissions(PERMISSIONS, PERMISSION_REQUEST)
метод requestPermissions(PERMISSIONS, PERMISSION_REQUEST)
. Если приложение имеет доступ к этим разрешениям, callPhoneManager()
.
Метод requestPermissions(PERMISSIONS, PERMISSION_REQUEST)
перенаправляет поток управления в метод onRequestPermissionsResult()
. Этот метод покажет пользователю диалог с просьбой разрешить приложению доступ к запрошенным разрешениям. Если пользователь разрешает разрешения, callPhoneManager()
метод callPhoneManager()
. Если нет, будет вызван PermissionUtils.alertAndFinish()
. Этот метод показывает сообщение для пользователя (сообщает пользователю, что он должен принять разрешения) и закрывает приложение.
Весь этот поток происходит при первом запуске приложения. Но что происходит во второй раз? Если пользователь разрешил разрешения в первый раз, проверка разрешений не будет выполнена во второй раз, и будет вызван метод callPhoneManager()
. Если это не было разрешено, то эта проверка также не будет выполнена, и будет вызван PermissionUtils.alertAndFinish()
. Если пользователь не выбрал ни одну из этих опций при первом выполнении, диалоговое окно с запросом разрешений будет показано снова.
И последнее, но не менее важное: если пользователь разрешил разрешения, callPhoneManager()
метод callPhoneManager()
. Это метод, который вызывает наш обратный вызов состояния телефона.
private void callPhoneManager () { TextView textView = (TextView)findViewById(R.id.id_text_view); mTelephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE); mTelephonyManager.listen( new PhoneCallback(textView), PhoneStateListener.LISTEN_CALL_STATE | PhoneStateListener.LISTEN_CELL_INFO // Requires API 17 | PhoneStateListener.LISTEN_CELL_LOCATION | PhoneStateListener.LISTEN_DATA_ACTIVITY | PhoneStateListener.LISTEN_DATA_CONNECTION_STATE | PhoneStateListener.LISTEN_SERVICE_STATE | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS | PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR | PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR); }
В этом методе осуществляется доступ к TelephonyManager
, вызывая метод listen()
. Смотрите конструктор этого метода:
Этот метод получает по своему первому параметру конструктор с TextView
(который будет использоваться для печати состояния телефона). И по второму параметру он получает все события, которые будут отслеживаться состоянием телефонного звонка.
Выполнение звонка с помощью эмулятора
Весь исходный код готов. Но не обязательно выполнять звонок для телефона, чтобы активировать обратный вызов состояния телефона.
Это можно сделать, позвонив по телефону или смоделировав вызов через эмулятор. Попробуем выполнить вызов в эмуляторе:
Чтобы выполнить звонок в эмуляторе:
1) Откройте виртуальные устройства Android
2) Выберите эмулятор
3) Откройте расширенные элементы управления
Ниже представлено изображение вкладки «Расширенные элементы управления».
4) Запустите приложение в эмуляторе
5) Выполнить вызов
Ниже приведены сообщения журнала после завершения вызова:
Сообщения журнала показывают, что по умолчанию состояние телефона находится в состоянии ожидания. Затем, когда вызов сделан, состояние переходит в состояние RINGING.
После завершения вызова состояние OFF_HOOK активно. И во всех этих штатах отображается входящий номер.
Вывод
В этом уроке показано, как создать приложение для мониторинга состояния вызова телефона в Android. Были объяснены основные концепции прослушивателей телефонных звонков, а также разрешения времени выполнения для отслеживания обратных вызовов этих прослушивателей. Исходный код приложения создавался по частям, и в конце мы увидели, как можно выполнить имитацию вызова.
И это все!
Другие статьи об Android можно найти на веб-сайте SitePoint.
Дальнейшее чтение
- Исходный код: https://github.com/sitepoint-editors/PhoneCallback
- Документация PhoneStateListener: https://developer.android.com/reference/android/telephony/PhoneStateListener.html