16 January 2021
© Денис Колисниченко
В этой статье мы рассмотрим семь полезных рецептов, которые пригодятся практически любому Android-программисту. Сразу скажу: рецепты рассчитаны на то, что читатель сей статьи уже знаком с разработкой приложений для Android. Азов здесь не будет. Все рецепты будут написаны на Java, а сами проекты будут создаваться в Eclipse.
Класс TelephonyManager можно использовать для получения информации о телефоне, определения его состояния и набора номера абонента. В первом рецепте мы поговорим о получении информации о телефоне.
Первым делом создаем проект приложения по умолчанию (пусть наше приложение будет называться TM). В файл манифеста нужно добавить следующую строку:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
Без этого нельзя будет прочитать состояние телефона и наше приложение, увы, работать не будет.
Рис. 1. Редактирование файла манифеста
Файл разметки тоже практически будет без изменения. Мы добавим лишь свойство id для элемента TextView. Файл разметки приведен в листинге 1.
Листинг 1. Файл разметки TM/res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:id="@+id/info"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/hello"
/>
</LinearLayout>
После создания файла разметки можно приступить к написанию кода. Примерный код см. в листинге 2. Полный код приложения ты найдешь на компакт-диске (такое решение было принято, дабы не растягивать текст статьи) в файле TMActivity.java.
Листинг 2. Фрагмент кода приложения
String EOL = "\n";
// Находим текстовую область в разметке
info =(TextView) findViewById(R.id.info);
// Создаем объект tm для получения информации о телефоне
tm = (TelephonyManager)getSystemService(TELEPHONY_SERVICE);
// буфер строк
StringBuilder sb = new StringBuilder();
// Общая информация об устройстве
sb.append("Общая информация:\n\n");
sb.append("ID устройства :")
.append(tm.getDeviceId()).append(EOL);
sb.append("Версия ПО: ")
.append(tm.getDeviceSoftwareVersion()).append(EOL);
sb.append("Номер телефона: ")
.append(tm.getLine1Number()).append(EOL);
// Информация об операторе, выдавшем SIM-карту
sb.append("\nОператор:\n\n");
sb.append("Код страны (ISO): ")
.append(tm.getSimCountryIso()).append(EOL);
sb.append("Оператор: ")
.append(tm.getSimOperator()).append(EOL);
sb.append("Название оператора: ")
.append(tm.getSimOperatorName()).append(EOL);
sb.append("Серийный номер SIM-карты: ")
.append(tm.getSimSerialNumber()).append(EOL);
// Информация о текущей сети
sb.append("\nСеть:\n\n");
sb.append("Код страны (ISO): ")
.append(tm.getNetworkCountryIso()).append(EOL);
sb.append("Оператор сети: ")
.append(tm.getNetworkOperator()).append(EOL);
sb.append("Название оператора сети: ")
.append(tm.getNetworkOperatorName()).append(EOL);
// Голосовая почта и другая информация
sb.append("\nДругая информация:\n\n");
sb.append("ID подписчика: ")
.append(tm.getSubscriberId()).append(EOL);
sb.append("Альфа-тег голосовой почты: ")
.append(tm.getVoiceMailAlphaTag()).append(EOL);
sb.append("Номер голосового почтового ящика: ")
.append(tm.getVoiceMailNumber()).append(EOL);
// Выводим содержимое буфера строк в текстовую область
info.setText(sb.toString());
Думаю, код достаточно закомментирован и не нуждается в дополнительных комментариях. Мы создаем экземпляр класса TelephonyManager и "вытягиваем" из него полезную информацию.
Работающее приложение представлено на рис. 2.
Рис. 2. Информация о телефоне
Теперь разберемся, как набрать номер телефона. Чтобы получить разрешение на набор номера, добавляем в файл манифеста такую строку:
<uses-permission android:name="android.permission.CALL_PHONE" />
Затем ты можешь использовать одну из двух операций — или ACTION_CALL или ACTION_DIAL. Первая операция отобразит диалог с набираемым номером (как обычно при наборе номера вручную), вторая операция наберет номер без показа какого-либо интерфейса пользователя.
Пример использования этих двух операций:
startActivity(new Intent(Intent.ACTION_CALL, Uri.parse("tel:номер")));
startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse("tel:номер")));
Как видишь, ничего сложного и можно сразу же перейти к третьему рецепту, где мы определим номер входящего звонка.
Для прослушивания состояния телефона с целью ожидания какого-то события используются "прослушки", которые подробно описаны на странице руководства разработчика Android:
https://developer.android.com/reference/android/telephony/PhoneStateListener.html
Мы рассмотрим только наиболее часто используемую "прослушку" PhoneStateListener.LISTEN_CALL_STATE, позволяющую определить номер входящего звонка (и, соответственно, выполнить определенные действия при входящем звонке). Возможны три состояния звонка:
q CALL_STATE_IDLE — устройство не используется для телефонного звонка (не принимается входящий звонок и не устанавливается исходящий);
q CALL_STATE_RINGING — устройство принимает входящий звонок;
q CALL_STATE_OFFHOOK — пользователь говорит по телефону.
Сейчас мы напишем программу, которая реагирует на все три состояния звонка и ничего не делает — действия ты определишь сам. Такое решение было принято, дабы не захламлять код. А что делать, решай сам — можешь, например, вывести уведомление при получении звонка. Чтобы задать собственные действия при изменении состояния звонка, мы переопределим метод onCallStateChanged().
Прежде, чем приступить к написанию кода, добавляем в файл манифеста следующую строку:
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
Файл разметки будет таким же, как у приложения TM (см. листинг 1). Фрагмент кода приложения представлен в листинге 3. Полный код приложения ты найдешь на CD в файле CallState.java.
Листинг 3. Реакция на изменение состояния звонка
...
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
...
info =(TextView) findViewById(R.id.info);
// Создаем объект класса TelephonyManager
tm = (TelephonyManager)getSystemService(TELEPHONY_SERVICE);
// Устанавливаем "прослушку" для LISTEN_CALL_STATE
tm.listen(new TelListener(),PhoneStateListener.LISTEN_CALL_STATE);
...
private class TelListener extends PhoneStateListener {
public void onCallStateChanged(int state, String incomingNumber) {
super.onCallStateChanged(state, incomingNumber);
switch (state) {
case TelephonyManager.CALL_STATE_IDLE:
info.setText("IDLE");
break;
case TelephonyManager.CALL_STATE_OFFHOOK:
info.SetText("OFFHOOK, Вход. номер:" +incomingNumber);
break;
case TelephonyManager.CALL_STATE_RINGING:
info.SetText("RINGING, Вход. номер:" +incomingNumber);
break;
default:
break;
} // switch
} // onCallStateChanged
}
}
Наше приложение выводит в текстовую область (TextView) с именем info состояние телефона и номер входящего звонка, если таковой имеется.
Современные мобилки оснащены всевозможными датчиками (сенсорами) - камерой, акселерометром, датчиком температуры и т.д.
Наиболее популярным сенсором является камера, но управление нею заслуживает отдельного разговора — мы обязательно поговорим об управлении камерой, но чуть позже. А пока рассмотрим другие датчики, которыми может быть оснащено мобильное устройство:
q TYPE_ACCELEROMETER — акселерометр, позволяет определить ускорение мобильного устройство. Таким датчиком оснащаются не все смартфоны, преимущественно акселерометр можно найти на смартфонах, оснащенных функцией GPS (хотя это необязательно — все зависит от производителя устройства).
q TYPE_LIGHT — датчик света. Очень полезная штука: с его помощью можешь управлять подсветкой дисплея, например, увеличить яркость, когда стало темно. В конечном итоге датчик помогает экономить заряд аккумулятора.
q TYPE_TEMPERATURE — температурный датчик.
q TYPE_PRESSURE — датчик атмосферного давления.
В твоем устройстве могут быть дополнительные датчики. Используй метод getSensorList() класса SensorManager, чтобы получить список датчиков.
Сейчас мы рассмотрим чтение показаний датчика температуры. Примеры чтения других датчиков можно найти в документации разработчика Android.
Для чтения датчиков подключаем следующие пакеты:
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
Далее определить объекты класса SensorManager:
private SensorManager myManager = null;
myManager = (SensorManager)getSystemService(SENSOR_SERVICE);
myManager.registerListener(tempSensorListener,
myManager.getDefaultSensor(Sensor.TYPE_TEMPERATURE),
SensorManager.SENSOR_DELAY_GAME);
…
Методу registerListener() передают три параметра. Первый — название обработчика датчика температуры. В нашем случае — это tempListener, который будет определен позже. Второй параметр — датчик по умолчанию, в нашем случае — датчик температуры. Третий параметр задает время обновления показаний датчика. Для наиболее быстрого обновления используйте SENSOR_DELAY_GAME, для обычного обновления — SENSOR_DELAY_NORMAL.
Следующий код определяет "прослушку" tempListener. Наша задача — переопределить методы onAccuracyChanged() и onSensorChanged(). Первый метод нам не нужен, поэтому мы определим его как пустой метод. А второй метод будет устанавливать текст области info (элемент TextView в разметке) — мы будем показывать температуру.
private final SensorEventListener tempListener = new SensorEventListener(){
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
@Override
public void onSensorChanged(SensorEvent event) {
if(event.sensor.getType()==Sensor.TYPE_TEMPERATURE){
info.setText("Температура: "+event.values[0]);
}
}
};
Дополнительную информацию можно получить по адресу:
https://developer.android.com/reference/android/hardware/SensorManager.html
Для привлечения внимания к уведомлению или диалогу программы нужно использовать вибрацию. Для управления виброзвонком добавляем в файл манифеста следующее разрешение:
<uses-permission android:name="android.permission.VIBRATE" />
Далее используем класс Vibrator так:
Vibrator Vib = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
Vib.vibrate(3000); // Вибрировать 3 секунды
Метод cancel() используется для преждевременной отмены вибрации (например, если ты установил вибрацию 3 секунды, а пользователь отреагировал раньше):
Vib.cancel();
Для передачи данных с использованием Bluetooth нужно:
q Включить адаптер Bluetooth.
q Найти доступные Bluetooth-устройства.
q Подключиться к одному из устройств.
q Произвести, собственно, обмен данными.
В файл манифеста Android-приложения, использующего Bluetooth, добавляем строки:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
В пакете android.bluetooth определены следующие классы:
q BluetoothAdapter — представляет интерфейс обнаружения и установки Bluetooth-соединений.
q BluetoothClass — описывает общие характеристики Bluetooth-устройства.
q BluetoothDevice — представляет удаленное Bluetooth-устройство.
q BluetoothSocket — сокет или точка соединения для данных, которыми наша система обменивается с другим Bluetooth-устройством.
q BluetoothServerSocket — сокет для прослушивания входящих Bluetooth-соединений.
Включение Bluetooth-адаптера
Первым делом получаем адаптер по умолчанию:
BluetoothAdapter myBluetooth = BluetoothAdapter.getDefaultAdapter();
Активировать Bluetooth-адаптер можно с помощью следующего кода:
// Если Bluetooth-выключен
if(!myBluetooth.isEnabled()) {
// Создаем действие ACTION_REQUEST_ENABLE — запрашивает включение
// адаптера
Intent eIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
// Выполняем действие
startActivity(eIntent);
}
Обнаружение устройств по соседству
Для обнаружения соседних устройств используется код из листинга 4. Обнаруженные устройства попросту выводятся в журнал с помощью Log.d().
Листинг 4. Поиск Bluetooth-устройств
import android.util.Log;
...
private final BroadcastReceiver myReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Когда найдено устройство
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// Получаем объект BluetoothDevice из Intent
BluetoothDevice device = intent.getParcelableExtra(
BluetoothDevice.EXTRA_DEVICE);
// Выводим сообщение в журнал (его можно будет просмотреть
// в Eclipse при запуске приложения).
Log.v("BlueTooth Discovery: ",device.getName() + "\n"
+ device.getAddress());
}
}
};
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(myReceiver, filter);
myBluetooth.startDiscovery();
Установка соединения с Bluetooth-устройством
Можно разработать как приложение-сервер, которое будет ожидать входящих запросов, так и приложение-клиент, которое будет устанавливать запрос с сервером. В листинге 5 приведен код, ожидающий соединения от программы-клиента.
Листинг 5. Ожидание запроса на подключение от клиента
// Класс AcceptBluetoothThread принимает входящие запросы
private class AcceptBluetoothThread extends Thread {
private final BluetoothServerSocket myServerSocket;
public AcceptThread() {
// Используем временный объект, который позже
// будет присвоен члену myServerSocket, поскольку
// myServerSocket — финальный член класса и потом уже
// не может быть изменен
BluetoothServerSocket tmp = null;
try {
// MY_UUID — идентификатор, также используемый клиентом
tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME,MY_UUID);
} catch (IOException e) { }
// Присваиваем tmp члену класса myServerSocket
myServerSocket = tmp;
}
public void run() {
BluetoothSocket socket = null;
// Прослушиваем соединения
while (true) {
try { // Принимаем соединение
socket = myServerSocket.accept();
} catch (IOException e) {
break;
}
// Если соединение было принято
if (socket != null) {
// Производим обработку соединения — в отдельном потоке
DoSomethingWith(socket);
// После обработки соединения закрываем сокет
myServerSocket.close();
break;
}
}
}
/** Действие в случае отмены соединения */
public void cancel() {
try { // Закрываем сокет
myServerSocket.close();
} catch (IOException e) { }
}
}
Теперь осталось написать приложение-клиент, устанавливающее соединение с Bluetooth-сокетом. Пример класса, который используется для установки соединения, приведен в листинге 6.
Листинг 6. Установка соединения с сервером (с Bluetooth-сокетом)
private class ConnectThread extends Thread {
private final BluetoothSocket mySocket;
private final BluetoothDevice myDevice;
public ConnectThread(BluetoothDevice device) {
// Используем временный объект, который позже
// будет присвоен члену mySocket, поскольку
// mySocket — финальный член класса и потом уже
// не может быть изменен
BluetoothSocket tmp = null;
myDevice = device;
// Получаем BluetoothSocket для соединения с BluetoothDevice
try {
// MY_UUID — идентификатор, такой же использует сервер
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) { }
mySocket = tmp;
}
public void run() {
// Отключаем обнаружение устройств, поскольку оно замедляет
// соединение
mAdapter.cancelDiscovery();
try {
// Соединяемся с устройством через сокет
mySocket.connect();
} catch (IOException connectException) {
// Невозможно подключиться, закрываем сокет
try {
mySocket.close();
} catch (IOException closeException) { }
return;
}
// Соединение установлено, производим его обработку в
// отдельном потоке
DoSomethingWith(mySocket);
}
// Отмена соединения, закрываем сокет
public void cancel() {
try {
mySocket.close();
} catch (IOException e) { }
}
}
Наконец-то мы поговорим о доступе к камере. Есть два способа доступа к камере. Первый заключается в вызове стандартного интерфейса управления камерой. Второй заключается в использовании класса Camera. Все зависит от поставленной задачи.
Начнем с первого способа. Для отображения стандартного интерфейса управления камерой используется следующий код:
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
startActivity(intent);
Просто? Да. Но не интересно. Если ты — настоящий программист, тебя интересует второй, более сложный способ.
Перед использованием класса Camera необходимо добавить в файл манифеста соответствующе разрешение:
<uses-permission android:name="android.permission.CAMERA" />
Для работы с камерой нам понадобятся следующие классы:
q Camera — доступ к "железу" (непосредственно к самой камере);
q Camera.Parameters — позволяет указать параметры камеры, например, размер изображения, качество изображения и т.д.
q SurfaceView — позволяет выделить поверхность, которая будет использоваться в качестве области предварительного просмотра для камеры.
Создайте новый проект MyCamera (имя пакета com.samples.mycamera). Основной файл разметки res/layout/main.xml для нашего проекта приведен в листинге 4. Элемент SurfaceView будет использоваться как область предварительного просмотра камеры.
Листинг 4. Основной файл разметки main.xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<SurfaceView android:id="@+id/surface"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
</SurfaceView>
</LinearLayout>
Управляющий камерой интерфейс будет описан в отдельном файле разметки. Назовите его cameraui.xml и поместите в каталог res/layout. Самое главное - это описание кнопки Получить фото, оно и приведено в листинге 5. А остальной код ты найдешь на CD в файле cameraui.xml.
Листинг 5. Файл cameraui.xml
...
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Получить фото"
/>
...
Теперь приступаем к коду. Метод takePicture(), используемый для получения фото, требует указания трех методов:
q SurfaceCallback() — используется для управления поверхностью, может также использоваться для применения различных графических эффектов к полученному фото;
q PictureCallback() — используется для управления памятью — что делать с фото, если не хватило памяти;
q CompressionCallback() — используется для сжатия фото.
Для управления поверхностью (surface) используется интерфейс SurfaceHolder.Callback. Нам нужно переопределить три его метода:
q surfaceCreated() — вызывается во время создания поверхности, используется для инициализации объектов;
q surfaceChanged — вызывается после создания поверхности и когда изменены параметры поверхности, например, ее размер;
q surfaceDestroyed() — вызывается при удалении поверхности, используется для очистки памяти.
Полный листинг приложения с моими комментариями находится на CD в файле MyCamera.java. Фрагмент кода приведен в листинге 8.
Листинг 8. Приложения MyCamera (фрагмент кода)
public class MyCamera extends Activity implements SurfaceHolder.Callback {
private LayoutInflater mInflater = null;
Camera MyCam; // Камера
byte[] tempdata; // Массив для временных данных
boolean mPreviewRunning = false;
// Поверхность и владелец поверхности (SurfaceHolder)
private SurfaceHolder PreviewHolder;
private SurfaceView Preview;
// Кнопка
Button GetPicture;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setFormat(PixelFormat.TRANSLUCENT);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.main);
// Находим поверхность и устанавливаем SurfaceHolder
Preview = (SurfaceView)findViewById(R.id.surface);
PreviewHolder = Preview.getHolder();
PreviewHolder.addCallback(this);
PreviewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
mInflater = LayoutInflater.from(this);
View overView = mInflater.inflate(R.layout.cameraoverlay, null);
this.addContentView(overView,
new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.FILL_PARENT));
// Находим кнопку
GetPicture = (Button) findViewById(R.id.button);
// Устанавливаем обработчик нажатия кнопки
GetPicture.setOnClickListener(new OnClickListener(){
public void onClick(View view){
// Вызываем метод takePicture() для получения картинки
MyCam.takePicture(Shutter, PicCallback, compress);
}
});
}
// Пустая заглушка — ничего не делаем
ShutterCallback Shutter = new ShutterCallback(){
@Override
public void onShutter() {}
};
// Пустая заглушка — ничего не делаем
PictureCallback PicCallback = new PictureCallback() {
public void onPictureTaken(byte[] data, Camera c) {}
};
// Заполняем массив с временными данными и вызываем
// функцию done()
PictureCallback compress = new PictureCallback() {
public void onPictureTaken(byte[] data, Camera c) {
if(data !=null) {
tempdata=data;
done();
}
}
};
void done() {
// Получаем растровое изображение путем декодирования
// массива tempdata
Bitmap bm = BitmapFactory.decodeByteArray(tempdata,
0, tempdata.length);
String url = Images.Media.insertImage(getContentResolver(),
bm, null, null);
bm.recycle();
Bundle bundle = new Bundle();
if(url!=null) {
bundle.putString("url", url);
Intent mIntent = new Intent();
mIntent.putExtras(bundle);
setResult(RESULT_OK, mIntent);
} else {
Toast.makeText(this, "Ошибка получения картинки",
Toast.LENGTH_SHORT).show();
}
finish();
}
// Реакция на изменение поверхности
@Override
public void surfaceChanged(SurfaceHolder holder, int format,
int w, int h) {
try {
if (mPreviewRunning) {
MyCam.stopPreview();
mPreviewRunning = false;
}
// Получаем параметры камеры
Camera.Parameters p = MyCam.getParameters();
// Устанавливаем размер пред. просмотра
p.setPreviewSize(w, h);
// Устанавливаем параметры камеры
MyCam.setParameters(p);
// Устанавливаем владельца поверхности
MyCam.setPreviewDisplay(holder);
// Запускаем пред. просмотр
MyCam.startPreview();
// Флаг пред. просмотра
mPreviewRunning = true;
} catch(Exception e) {
// Действие в случае исключения, для упрощения кода
// оставлено незаполненным. Можно вывести номер
// ошибки как уведомление с помощью метода
// e.toString()
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// Действие при создании поверхности — открываем камеру
MyCam = Camera.open();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// Останавливаем пред. просмотр
MyCam.stopPreview();
// Сбрасываем флаг
mPreviewRunning = false;
// Освобождаем ресурсы
MyCam.release();
MyCam=null;
}
}
На этом все. Буду рад выслушать ваши замечания и предложения по адресу dhsilabs@mail.ru.