Журнал LinuxFormat - перейти на главную

LXF163:Android: Об­ще­ние с дру­ги­ми

Материал из Linuxformat
Перейти к: навигация, поиск


Cвяжем ваши устройства через Bluetooth

Содержание

Об­ще­ние с дру­ги­ми

Джуль­ет­та Кемп рассказывает, как Bluetooth по­мо­га­ет об­щать­ся с дру­ги­ми уст­рой­ст­ва­ми, и ­проясняет сер­вис об­ме­на со­об­ще­ния­ми.

(thumbnail)
Наш эксперт Джуль­ет­та Кемп пи­шет ста­тьи для раз­лич­ных изда­­ний и ра­бо­та­ет сис­тем­ным ад­ми­ни­ст­ра­то­ром. Во вре­мя на­пи­са­ния этой ста­тьи ей при­шлось не­ма­ло по­жонг­ли­ро­вать с те­ле­фо­ном.

Се­го­дня Bluetooth мож­но най­ти на лю­бых уст­рой­ст­вах, и с его по­мо­щью лег­ко об­менивать­ся дан­ны­ми ме­ж­ду уст­рой­ст­ва­ми. На этом уро­ке мы на­пи­шем сер­вис об­ме­на со­об­щения­ми по Bluetooth и уз­на­ем, как об­на­ру­жить дру­гие уст­рой­ст­ва (спа­рен­ные или нет), за­пустить сер­вер Bluetooth, от­пра­вить за­прос на под­клю­чение со сто­ро­ны кли­ен­та и пе­ре­дать дан­ные ме­ж­ду дву­мя уст­рой­ст­ва­ми. От­ме­тим, что этот про­ект де­лал­ся под Android 10 (2.3.3), но сра­бо­та­ет­ся и с бо­лее поздними вер­сия­ми. Пол­ный код уро­ка мож­но най­ти на LXFDVD, но помните, что он не бу­дет вы­пол­нять­ся «как есть» – для ком­пи­ля­ции в ва­шей локаль­ной сре­де про­ек­ты Android нуж­но на­стро­ить ли­бо вруч­ную, ли­бо в Eclipse.

Ус­та­нав­ли­ва­ем со­еди­не­ние по Bluetooth

Из­менения, ко­то­рые надо про­де­лать в фай­ле AndroidManifest.xml, опи­са­ны на на­шем DVD. Для об­ме­на дан­ны­ми по Bluetooth по­на­до­бят­ся и кли­ент, и сер­вер. Сер­вер слу­ша­ет под­клю­чения, от­крыв со­кет BluetoothServerSocket; кли­ент соз­да­ет BluetoothSocket, от­кры­ва­ет ка­нал свя­зи с сер­ве­ром RFCOMM и от­прав­ля­ет за­прос на со­единение. Сер­вер принима­ет со­единение и от­кры­ва­ет свой BluetoothSocket. Когда оба со­ке­та BluetoothSocket на од­ном и том же ка­на­ле RFCOMM бу­дут от­кры­ты, кли­ент и сер­вер под­клю­чат­ся друг к дру­гу и смо­гут об­менивать­ся дан­ны­ми.

По­сле уста­нов­ки со­единения сер­вер дол­жен за­крыть свой со­кет BluetoothServerSocket, ес­ли вам не нуж­но под­дер­жи­вать несколь­ко со­единений од­но­вре­мен­но.

Итак, есть два ва­ри­ан­та соз­дания под­клю­чения:

» на­пи­сать от­дель­ные про­грам­мы кли­ен­та и сер­ве­ра;

» на­пи­сать про­грам­му, спо­соб­ную вес­ти се­бя и как кли­ент, и как сер­вер, и за­пра­ши­вать или принимать под­клю­чение.

Вто­рой ва­ри­ант ис­поль­зу­ет­ся во мно­гих при­ло­жениях Bluetooth; его мы и вы­бе­рем. Ес­ли у вас нет двух уст­ройств для про­вер­ки со­единения, мож­но раз­вер­нуть сер­вер Java на но­ут­бу­ке или ПК. По уста­нов­ке со­единения и сер­вер, и кли­ент бу­дут пе­ре­да­вать и принимать дан­ные. По­это­му наш класс BluetoothIMService бу­дет вы­пол­нять три от­дель­ных дей­ст­вия:

» слу­шать по­пыт­ки со­единения («сер­вер»);

» от­прав­лять за­прос на со­единения («кли­ент»);

» об­ра­ба­ты­вать пе­ре­да­чу дан­ных по­сле уста­нов­ки со­единения.

Эти дей­ст­вия бу­дут реа­ли­зо­ва­ны в от­дель­ных нитях, и мы смо­жем слу­шать со­единения, не пре­ры­вая про­чую ак­тив­ность при­ло­жений и сис­те­мы.

Мы соз­да­ем и за­пуска­ем сер­вис сле­дую­щим об­ра­зом:

public class BluetoothIMService {

// Раз­лич­ные по­ля; под­роб­но­сти см. в ко­де на DVD

public BluetoothIMService(Context context, Handler h) {

adapter = BluetoothAdapter.getDefaultAdapter();

handler = h;

state = STATE_NONE;

}

public synchronized void start() {

if (connectThread != null) {connectThread.cancel(); connectThread = null;}

if (dataThread != null) {dataThread.cancel(); dataThread = null;}

setState(STATE_LISTEN);

if (acceptThread == null) {

acceptThread = new AcceptThread(true);

acceptThread.start();

Ме­тод start() за­вер­ша­ет все су­ще­ст­вую­щие нити, пы­таю­щие­ся под­клю­чить­ся или уже от­прав­ляю­щие дан­ные (по­это­му под­хва­тить ста­рую сес­сию не по­лу­чит­ся), и, если надо, за­пуска­ет но­вую нить сер­ве­ра для прие­ма со­единений. Най­ди­те на LXFDVD код вспо­мо­га­тель­но­го ме­то­да setState(), ко­то­рый оп­ре­де­ля­ет те­ку­щее со­стояние сер­ви­са и возвраща­ет ин­фор­ма­цию в глав­ное Activity.

Под­клю­ча­ем­ся

Код нити сер­ве­ра AcceptThread слиш­ком длине­н, что­бы при­вес­ти его здесь це­ли­ком, так что ищи­те его то­же на LXFDVD. Для соз­дания слу­шаю­ще­го со­ке­та мы поль­зу­ем­ся ме­то­дом API listenUsingRfcommWithServiceRecord(). Ему пе­ре­да­ют­ся UUID и имя сер­ви­са, ко­то­рое до­ба­вит­ся в запись сер­ви­са RFCOMM. UUID уникаль­но иден­ти­фи­ци­ру­ет сер­вис и дол­жен быть оди­на­ко­вым для кли­ен­та и сер­ве­ра, под­клю­чаю­щих­ся друг к дру­гу. Су­ще­ст­ву­ют он­лайн-сер­ви­сы для генера­ции UUID.

В ме­то­де run() мы про­ве­ря­ем, что уже ни к ко­му не под­клю­че­ны (ес­ли да, то не принима­ем дру­гие со­единения), и ес­ли нет, слу­ша­ем со­кет. Вы­зов serverSocket.accept() — бло­ки­рую­щий; это оз­на­ча­ет, что он за­вер­шит­ся толь­ко при успеш­ном под­клю­чении ли­бо при на­ли­чии ис­клю­чения. Он не за­вер­ша­ет­ся по тай­мау­ту. По­это­му так важ­но за­пускать его в от­дель­ной нити, что­бы не вли­ять ни на что дру­гое.

Ес­ли у нас есть при­ня­тое со­единение (т. е. пе­ре­мен­ная socket име­ет зна­чение), мы дей­ст­ву­ем в за­ви­си­мо­сти от те­ку­ще­го со­стояния сер­ви­са. Ес­ли сер­вис под­клю­ча­ет­ся (“connecting”), мы идем даль­ше и за­пуска­ем нить под­клю­чения в ме­то­де connected() основ­но­го клас­са, ко­то­рый мы вско­ре рас­смот­рим. Ес­ли сер­вис уже под­клю­чен, мы из­бав­ля­ем­ся от со­ке­та, так как под­дер­жи­ва­ем толь­ко од­но со­единение в один мо­мент вре­мени.

Дей­ст­вия в ка­че­­ст­ве кли­ен­та

Класс AcceptThread реа­ли­зу­ет дей­ст­вия про­грам­мы в ка­че­­ст­ве сер­ве­ра; а как на­счет кли­ен­та? Для это­го у нас есть ме­тод connect() и внут­ренний класс ConnectThread.

public synchronized void connect(BluetoothDevice device) {

if (state == STATE_CONNECTING) {

if (connectThread != null) {connectThread.cancel(); connectThread = null;}

}

if (dataThread != null) {dataThread.cancel(); dataThread = null;}

connectThread = new ConnectThread(device);

connectThread.start();

setState(STATE_CONNECTING);

}

Ес­ли мы уже под­клю­ча­ем­ся, за­вер­шим все су­ще­ст­вую­щие нити под­клю­чений. Мы так­же за­вер­ша­ем все уста­нов­лен­ные под­клю­чения, а за­тем соз­да­ем и за­пуска­ем но­вую нить ConnectThread:

private class ConnectThread extends Thread {

private final BluetoothSocket socket;

private final BluetoothDevice device;

public ConnectThread(BluetoothDevice d) {

device = d;

BluetoothSocket tmp = null;

try {

tmp = device.createRfcommSocketToServiceRecord(MY_UUID);

} catch (IOException e) {

Log.e(TAG, “create() failed”, e);

}

socket = tmp;

}

public void run() {

setName(“ConnectThread”);

adapter.cancelDiscovery();

try {

socket.connect();

} catch (IOException e) {

try {

socket.close();

} catch (IOException e2) {

Log.e(TAG, “unable to close() socket during connection failure”, e2);

}

connectionFailed();

return;

}

synchronized (BluetoothIMService.this) {

connectThread = null;

}

connected(socket, device);

}

public void cancel() {

try {


socket.close();

} catch (IOException e) {

Log.e(TAG, “close() of connect socket failed”, e);

}

}

}

Класс socket яв­ля­ет­ся фи­наль­ным и инициа­ли­зи­ру­ет­ся толь­ко один раз, и кон­ст­рук­тор для соз­да­ния вре­мен­но­го со­ке­та BluetoothSocket RFCOMM для сер­ви­са, иден­ти­фи­ци­руе­мо­го UUID, ис­поль­зу­ет ме­тод createRfcommSocketToServiceRecord().

Ме­тод socket.connect() то­же яв­ля­ет­ся бло­ки­рую­щим, по­это­му он то­же дол­жен быть в от­дель­ной нити, как и здесь. Ес­ли мы по­лу­ча­ем ис­клю­чение, то под­клю­чение невоз­мож­но, и мы за­кры­ва­ем со­кет и за­вер­ша­ем ме­тод. Ес­ли под­клю­чение воз­мож­но, мы вы­зы­ва­ем ме­тод connected() внешнего клас­са.

Пе­ре­да­ча дан­ных

На дан­ный мо­мент, неза­ви­си­мо от то­го, яв­ля­ем­ся ли мы сер­ве­ром (с нитью AcceptThread) или кли­ен­том (с нитью ConnectThread), у нас есть под­клю­чен­ный со­кет, и с ним нуж­но кое-что сде­лать. Вот ме­тод connected() клас­са BluetoothIMService:

public synchronized void connected(BluetoothSocket socket,

BluetoothDevice device) {

if (connectThread != null) { connectThread.cancel(); connectThread = null; }

if (dataThread != null) { dataThread.cancel(); dataThread = null; }

if (acceptThread != null) { acceptThread.cancel(); acceptThread = null; }

dataThread = new DataTransferThread(socket);

dataThread.start();

Message msg = handler.obtainMessage(BluetoothIM.MESSAGE_DEVICE_NAME);

Bundle bundle = new Bundle();

bundle.putString(BluetoothIM.DEVICE_NAME, device.getName());

msg.setData(bundle);

handler.sendMessage(msg);

setState(STATE_CONNECTED);

}

Мы сно­ва за­вер­ша­ем все су­ще­ст­вую­щие нити, а по­сле это­го соз­да­ем и за­пуска­ем нить пе­ре­да­чи и прие­ма со­об­щений DataTransferThread. Мы так­же со­об­ща­ем глав­но­му За­ня­тию имя уст­рой­ст­ва, к ко­то­ро­му под­клю­ча­ем­ся. На­конец, есть нить DataTransferThread. Ее код опять же слиш­ком длинен, и у нас на него здесь не хва­тит мес­та (а у вас вряд ли хва­тит тер­пения на пе­ре­пе­чат­ку его из жур­на­ла), по­это­му бе­ри­те его с DVD. Кон­ст­рук­тор за­да­ет зна­чение со­ке­та, по­то­ков InputStream и OutputStream и под­клю­ча­ет по­то­ки с со­ке­ту.


Ме­тод run() слу­ша­ет InputStream и пе­ре­да­ет глав­но­му За­ня­тию при­ня­тые со­об­щения че­рез об­ра­бот­чик. Ме­тод write() этой нити бу­дет вы­зван ме­то­дом write() внешнего клас­са BluetoothIMService:

public void write(byte[] out) {

DataTransferThread r;

synchronized (this) {

if (state != STATE_CONNECTED) return;

r = dataThread;

}

r.write(out);

}

Здесь так­же есть па­ра ме­то­дов об­ра­бот­ки под­клю­чений (для уте­рян­ных или неудач­ных под­клю­чений) – см. код на DVD для под­роб­но­стей.

Свя­зы­ва­ем Activity вме­сте

Те­перь у нас есть спи­сок доступ­ных уст­ройств и сер­вис, ко­то­рый мо­жет под­клю­чить­ся к уст­рой­ст­вам и об­ра­бо­тать пе­ре­да­чу дан­ных. Сле­дую­щая за­да­ча – иниции­ро­вать со­единение с кон­крет­ным уст­рой­ст­вом.

Для на­ча­ла за­пустим нить AcceptThread при за­пуске Activity на слу­чай, ес­ли есть дру­гое уст­рой­ст­во, ко­то­рое ждет, по­ка на­ши уст­рой­ст­ва ста­нут доступ­ны­ми. До­бавь­те сле­дую­щий код в ме­тод onStart():

if (!bluetoothAdapter.isEnabled()) {

// О ра­бо­те с Bluetooth, ко­гда он не вклю­чен, см. врез­ку

} else {

if (imService == null) {

imService = new BluetoothIMService(this, handler);

}

ensureDiscoverable();

}

Ме­тод ensureDiscoverable() (см. код на DVD) де­ла­ет уст­рой­ст­во доступ­ным для об­на­ру­жения на пять ми­нут. Эту воз­мож­ность сле­ду­ет ак­ти­ви­ро­вать из ме­ню, а не ав­то­ма­ти­че­­ски.

До­бавь­те к спи­ску уст­ройств ме­тод onClickListener, что­бы по щелч­ку на уст­рой­ст­ве поль­зо­ва­тель мог под­клю­чить­ся к нему:

// До­бавь­те по­доб­ные стро­ки к обо­им ме­то­дам ска­ни­ро­ва­ния

pairedDevicesListView.setOnItemClickListener(listOnClickListener);

// ....

private AdapterView.OnItemClickListener

listOnClickListener =

new AdapterView.OnItemClickListener() {

public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

String device[] = ((String)parent.getItemAtPosition(position)).split(“\\n”);

BluetoothDevice chosenDevice = bluetoothAdapter.getRemoteDevice(device[1]);

unregisterReceiver(receiver);

isReceiverRegistered = false;

setupIM();

imService.connect(chosenDevice);

}

}; Этот код уда­ля­ет ре­ги­ст­ра­цию BroadcastReceiver (т. е. по­сле об­на­ру­жения уст­рой­ст­ва, к ко­то­ро­му нуж­но под­клю­чить­ся, мы пе­ре­ста­ем ис­кать но­вые), за­да­ет па­ра­мет­ры об­ме­на со­об­щения­ми и вы­зы­ва­ет BluetoothIMService для под­клю­чения к вы­бран­но­му уст­рой­ст­ву.

От­прав­ка и при­ем со­об­ще­ний

Для по­ка­за со­об­щений нуж­но из­менить рас­по­ло­жение ком­понен­тов. Для пе­ре­дан­ных и при­ня­тых со­об­щений ис­поль­зу­ем спи­сок List, для вво­да со­об­щения – тек­сто­вое по­ле EditText, для от­прав­ки со­об­щения – кноп­ку. Ме­тод setupIM() вы­гля­дит сле­дую­щим об­ра­зом (XML-код см. на DVD):

private void setupIM() {

setContentView(R.layout.messaging);

conversationArrayAdapter = new ArrayAdapter<String>(this, R.layout.message);

conversationListView = (ListView) findViewById(R.id.conversation);

conversationListView.setAdapter(conversationArrayAdapter);

messageOutEditText = (EditText) findViewById(R.id.send_edittext);

messageOutEditText.setOnEditorActionListener(writeListener);

sendButton = (Button) findViewById(R.id.button_send);

sendButton.setOnClickListener(new OnClickListener() {

public void onClick(View v) {

TextView view = (TextView) findViewById(R.id.send_edittext);

String message = view.getText().toString();

sendMessage(message);

}

});

if (imService == null) {

imService = new BluetoothIMService(this, handler);

}

outMessageBuffer = new StringBuffer(“”);

}

ArrayAdapter взаи­мо­дей­ст­ву­ет с ListView; кноп­ка бе­рет со­дер­жи­мое EditText и от­прав­ля­ет его в ка­че­­ст­ве со­об­щения. На­конец, при необ­хо­ди­мо­сти инициа­ли­зи­ру­ет­ся сер­вис об­ме­на со­об­щения­ми, а так­же инициа­ли­зи­ру­ет­ся бу­фер для ис­хо­дя­щих со­об­щений. Ме­тод sendMessage() вы­зы­ва­ет сер­вис об­ме­на со­об­щения­ми для пе­ре­да­чи дан­ных:

private void sendMessage(String message) {

if (imService.getState() != BluetoothIMService.STATE_CONNECTED) {

Toast.makeText(this, R.string.notConnected, Toast.LENGTH_SHORT).show();

return;

}

if (message.length() > 0) {

byte[] send = message.getBytes();

imService.write(send);

outMessageBuffer.setLength(0);

messageOutEditText.setText(outMessageBuffer);

}

}

Мы про­ве­ря­ем, что сер­вис под­клю­чен и что есть со­об­щение для от­прав­ки, за­тем пе­ре­да­ем его сер­ви­су об­ме­на со­об­щения­ми и об­ну­ля­ем бу­фер со­об­щения и по­ле вво­да EditText. Как на­счет вхо­дя­щих со­об­щений? В клас­се BluetoothIMService нить DataTransfer слу­ша­ет вхо­дя­щие дан­ные и от­прав­ля­ет со­об­щение при их по­лу­чении. Об­ра­бот­чик ра­бо­та­ет с со­об­щения­ми и Runnables, по­зво­ляя за­планиро­вать бу­ду­щие дей­ст­вия или до­ба­вить дей­ст­вия в оче­редь для вы­полнения в дру­гой нити.

private final Handler handler = new Handler() {

public void handleMessage(Message msg) {

switch (msg.what) {

case MESSAGE_STATE_CHANGE:

switch (msg.arg1) {

case BluetoothIMService.STATE_CONNECTED:

setupIM();

conversationArrayAdapter.clear();

break;

case BluetoothIMService.STATE_CONNECTING:

case BluetoothIMService.STATE_LISTEN:

case BluetoothIMService.STATE_NONE:

break;

}

break;

case MESSAGE_WRITE:

byte[] writeBuf = (byte[]) msg.obj;

String writeMessage = new String(writeBuf);

conversationArrayAdapter.add(

getResources().getString(R.string.thisDevice) +

getResources().getString(R.string.idDivider) + writeMessage);

break;

case MESSAGE_READ:

byte[] readBuf = (byte[]) msg.obj;

String readMessage = new String(readBuf, 0, msg.arg1);

conversationArrayAdapter.add(connectedDeviceName +

getResources().getString(R.string.idDivider) + readMessage);

break;

case MESSAGE_DEVICE_NAME:

connectedDeviceName = msg.getData().getString(DEVICE_NAME);

Toast.makeText(getApplicationContext(),

getResources().getString(R.string.connectionSucceededDisplay) + connectedDeviceName, Toast.LENGTH_SHORT).show();

break;

case MESSAGE_TOAST:

Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST),

Toast.LENGTH_SHORT).show();

break;

}

}

};

Ес­ли сер­вис об­ме­на со­об­щения­ми под­клю­ча­ет­ся или под­клю­чен, мы за­да­ем IM display и сбра­сы­ва­ем его. Ес­ли бы­ло на­пи­са­но со­об­щение, мы до­бав­ля­ем его в ArrayAdapter, ко­то­рый ото­бра­жа­ет раз­го­вор. Ес­ли со­об­щение бы­ло про­чи­та­но, мы де­ла­ем то же са­мое, до­бав­ляя имя уст­рой­ст­ва, ко­то­рое от­пра­ви­ло его. А при по­лу­чении имени уст­рой­ст­ва мы за­пи­сы­ва­ем его в пе­ре­мен­ную и ото­бра­жа­ем со­об­щение Toast.

На этом все! Ском­пи­ли­руй­те, за­пусти­те и про­тес­ти­руй­те про­грам­му на несколь­ких те­ле­фо­нах. Мно­гое в ней мож­но улуч­шить – на­при­мер, сде­лать несколь­ко вкла­док и од­но­вре­мен­но об­ра­ба­ты­вать несколь­ко под­клю­чений. Так­же мож­но адап­ти­ро­вать код, сде­лав его од­но­на­прав­лен­ным, а не дву­на­прав­лен­ным – для транс­ля­ции со­об­щений по Bluetooth на дру­гие уст­рой­ст­ва.

|

Персональные инструменты
купить
подписаться
Яндекс.Метрика