Керований комп'ютером з Android пристрою

Початок
А почалося все з того, що викликає генеральний мене до себе і каже: «Ось бачиш телефон? Хочу щоб там була кнопка, я на неї натискаю, і у мене в ноутбуці кіно включається. Натискаю іншу – музика грає.» І ще чого-то багато наговорив, вже не пам'ятаю. «Завдання зрозуміле? Виконуй!» Ось вже не знаю, з чого така потреба у нього виникла. Чи То зірки не під тим кутом встали, то сон який приснився. Коротше, не зрозумієш цих багатих… Ну да ладно.

Спочатку поліз нишпорити в Гугл в пошуках підходящої програми, а потім подумав – а якого біса? Напишу сам. Тим більше, що завдання не видалася складною, так і «поклик коду» вже давав про себе знати (отака професійна it-ломка). Ось і вирішив поєднати Windows і Android власними силами.

Те, що він просив, я зробив за пару днів. Але тут я не хочу городити багато коду, перевірок та обробок винятків і т. п. Стаття скоріше призначена для самих маленьких, як основа, спираючись на яку, можна побудувати щось більш масштабне. Ні в якому разі не претендую на оригінальність, явно хтось щось подібне писав, я просто пропоную свій варіант. Загалом, всім, кому цікаво, присвячується.

Що ми маємо
Значить так. З одного боку, у нас телефон з Android на борту, з іншого — Windows з установленими програмами, притому деякі з цих програм нам треба запускати, подавши команду з телефону.
Телефон та комп'ютер зв'яжемо через локальну мережу, тут без варіантів (ну не смски ж посилати). Таким чином, будемо писати дві програми. Перша — це сервер, що працює на комп'ютері, завдання цієї програми — відкрити і слухати порт. Якщо на цей порт падає щось корисне, то виконати заданий нами дію. Друга програма — це клієнт, запущений на телефоні, її завдання опрацювати дії користувача, підключитися до сервера і передати інформацію.

Трохи про сокети
Тема програмування сокетів до того вже заїжджена, що і особливо говорити нічого. Але все ж у двох словах, для тих, хто не любить ходити по посиланнях.

Сокет — це програмний інтерфейс, який дозволяє встановлювати зв'язок між двома процесами, використовуючи протокол tcp/ip. Сокет асоційований з двома аспектами: ip-адресою і портом. Де ip-адресу — це адреса хоста (комп'ютера) у мережі, з ним працює протокол IP. Port — це ідентифікатор програми, до якого адресовано з'єднання, тут працює протокол TCP. Порт може бути як TCP, так і UDP, в цій статті я буду використовувати тільки TCP. Оскільки ip-адреса є унікальним як в мережі інтернет, так і в локальній мережі, то він однозначно визначає адресу відправника й адреса приймаючого. Порт ж є унікальним в межах операційної системи, він визначає додаток, з яким ми хочемо взаємодіяти. Порти можуть бути стандартними, наприклад, 80 закріплений за HTTP, або 3389 — RDP. Ви можете використовувати будь незайнятий порт, але стандартні краще не чіпати. Дуже добре і з прикладами про сокети написано тут.

Сервер. Починаємо хуліганити
Запускати Aimp, Windows Media Player і т.п. навіть з телефону — це не цікаво, так і на базі цієї статті ви зможете все це легко реалізувати, трохи переробивши код. Давайте краще побезобразничаем. Будимо крутити-вертіти екран монітора як нам заманеться або виводить несподівані повідомлення (отакий односпрямований ацкий месенджер), і найжахливіше — вимкнемо комп'ютер! Правда, за це можуть і на вила надіти. Ну да ладно, нехай спочатку зловлять.

Отже, приступимо. У Visual Studio створюємо нове Windows Form додатком з ім'ям, скажімо, FunnyJoke. Відкриваємо файл Program.cs і видаляємо весь код в тілі функції Main. Цей код ініціалізує головну форму додатку, нашого сервера ніякі вікна не потрібні, він повинен сидіти тихо мирно і чекати команд.

У класі Program визначимо наступні змінні:

// Порт
static int port = 10000;
// Адреса
static IPAddress ipAddress = IPAddress.Parse("0.0.0.0");
// Надіслати повідомлення
const byte codeMsg = 1; 
// Повернути екран
const byte codeRotate = 2; 
// Вимкнути комп'ютер
const byte codePoff = 3; 

Я взяв порт 10000, саме його і буде слухати наш сервер, замість ip адреси поставив 0.0.0.0 це говорить про те, що будуть оброблятися всі доступні мережеві інтерфейси. Це не зовсім правильно, але для початку зійде. Далі я визначив три константи, які задають коди команд, що надходять від клієнта. На початку проекту не забуваємо підключити:

using System.Net;
using System.Net.Sockets;

Тепер, замість вилученого код в функції Main вставляємо наступний:

Головна
static void Main()
{ 
// Створюємо локальну кінцеву точку
IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, port);
// Створюємо основний сокет
Socket socket = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); 
try
{
// Пов'язуємо сокет з кінцевою точкою
socket.Bind(ipEndPoint);
// Переходимо в режим "прослуховування"
socket.Listen(1); 
while (true)
{ 
// Чекаємо з'єднання. При вдалому з'єднання створюється новий примірник Socket
Socket handler = socket.Accept();
// Масив, де зберігаємо прийняті дані.
byte[] recBytes = new byte[1024];
int nBytes = handler.Receive(recBytes); 
switch (recBytes[0]) // Визначаємося з командами клієнта
{
case codeMsg: // Повідомлення 
nBytes = handler.Receive(recBytes); // Читаємо дані повідомлення
if (nBytes != 0) 
{ 
// Перетворимо отриманий набір байт на рядок
String msg = Encoding.UTF8.GetString(recBytes, 0, nBytes); 
MessageBox.Show(msg, "Привіт Пупсик!");
} 
break;
case codeRotate: // Поворот екрану 
RotateScreen(); 
break;
case codePoff: // Вимикаємо
System.Diagnostics.Process p = new System.Diagnostics.Process();
p.StartInfo.FileName = "shutdown.exe";
p.StartInfo.Arguments = "-s -t 00";
p.Start();
socket.Close(); 
break;
}
// Звільняємо сокети
handler.Shutdown(SocketShutdown.Both);
handler.Close();
} 
}
catch (Exception ex)
{ 
}
} 



Приклад добре прокоментував. Але все ж поясню. Спочатку створюємо локальну кінцеву точку і асоціюємо її з нашим ip-адресою і портом. Потім, визначаємо основний сокет, пов'язуємо його з кінцевою точкою, і переводимо в режим прослуховування. Після цього входимо в нескінченний цикл, починаючи з рядка:

Socket handler = socket.Accept();

наш сервер переходить в стан очікування з'єднання. При вдалому з'єднанні створиться новий примірник Socket, за допомогою якого ми і будемо спілкуватися з нашим клієнтом. Після того як з'єднання встановлене починаємо читати дані:

int nBytes = handler.Receive(recBytes

Команди клієнта закодовані однобайтовым кодом (описано на початку програми), сервер розшифрувавши код команди починає її виконувати, після цього знову переходить в режим очікування. Винятком є codeMsg, оскільки після неї очікується набір байт, що містить рядок повідомлення. Тому, отримавши цю команду сервер знову читає дані з сокету:

nBytes = handler.Receive(recBytes); 
if (nBytes != 0) 
{
String msg = Encoding.UTF8.GetString(recBytes, 0, nBytes); 
MessageBox.Show(msg, "Привіт Пупсик!");
}


Рядок, що приходить від клієнта, має кодування UTF-8, тому перш ніж показати її нещасному користувачеві, необхідно привести її до стандартного вигляду.

Що б спростити програму, і не створювати зайві діалоги я використовував стандартний клас MessageBox, але у такий підходу є один недолік. MessageBox створює модальне вікно, яке блокує потік всього програми. Іншими словами, поки відкрите вікно з повідомленням наш сервер нічого не робить. Мінус звичайно, але за простоту треба платити.

Процедуру, зміни орієнтації екрану, розписувати не буду, її код я виконав так як рекомендує Microsoft ось тут. Як повернути екран засобами .NET я не знайшов. Це легко здійсненно для мобільних платформ, а ось для звичайного PC виявилася нерозв'язна проблема. Але, на допомогу прийшов старий добрий WINAPI і все розрулив.
Вимикаємо комп'ютер штатними засобами Windows, шляхом виклику команди shutdown з відповідними прапорами.

З сервером, мабуть, все. Вихідний код проекту я прикреплю в кінці статті.

Клієнт
Клієнт будемо писати в Android Studio, оскільки мені ця IDE більше подобається ніж Eclipse. Любителям останнього думаю не складе великих труднощів переробити проект. Для налагодження я використовував VirtualBox з встановленою віртуальною машиною Android, бо рідний емулятор моторошно гальмівний, і життя не вистачити що б з його допомогою щось налагодити. Ну і періодично перевіряв на «живому» телефоні. Отже, створюємо проект з ім'ям FunnyJoke, задаємо мінімальну версію API, яку здатний тягнути ваш телефон (у мене 16) і вибираємо Empty Activity. Все інше за замовчуванням. Робимо розмітку подання. З дизайном я дуже не извращался, кому треба нехай малює гарні кнопки, розміщує їх по фен Шую і т. п. Я зробив просто: два поля типу EditText, перше введення ip адреси контрольованого комп'ютера, друге для тексту повідомлення, і кнопка, яка змусить обертатися робочий стіл. А от кнопку завершення роботи я зробив велику і загрозливе червону. Це щоб випадково не натиснути.

activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical"
android:weightSum="1"
android:layout_marginTop="20dp">


<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="IP address:"
android:id="@+id/textView" />

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/edIPaddress"
android:digits="0123456789." />

</LinearLayout>

<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top" >

<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/etMsg"
android:layout_gravity="center_vertical"
android:layout_weight="1" />

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send Msg"
android:layout_weight="0"
android:id="@+id/btnSMsg"
android:layout_gravity="center_vertical"
android:onClick="onClick"
/>

</LinearLayout>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rotate Screen"
android:id="@+id/btnRotate"
android:layout_weight="0"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="20dp"
android:layout_marginTop="20dp"
android:onClick="onClick"
/>

<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/btnPowerOff"
android:layout_gravity="center"
android:src="@drawable/button_img"
android:background="@null"
android:onClick="onClick"
/>
</LinearLayout>


Тут варто звернути увагу на полі edIPaddress, в ньому варто фільтрація на введення тільки цифр в. (точка), так-як поле призначене для введення ip адреси. Треба сказати, що це єдина перевірка на правильність введених даних, все інше залишається на совісті користувача. Ще хочу сказати про кнопці btnPowerOff її стан відстежує селектор, і в залежності від того натиснута вона або ні змінює зображення (інакше, не зрозуміло відбулося натискання, кнопка буде виглядати як статична картинка). Ось код селектора button_img.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:drawable="@drawable/poweroffs"/>
<item android:drawable="@drawable/poweroff"/>
</selector>

Відповідно в ресурсах повинні бути дві картинки одна для нажатого стану, інша для звичайного. Вийде ось такий екран:

image

На цьому з розміткою закінчимо. Переходимо до файлу MainActivity.java. В першу чергу, так само, як і в сервері, визначаємо коди команд і деякі змінні:

String serIpAddress; // адреса сервера
int port = 10000; // порт
String msg; // Повідомлення
final byte codeMsg = 1; // Надіслати повідомлення
final byte codeRotate = 2; // Повернути екран
final byte codePoff = 3; // Вимкнути комп'ютер
byte codeCommand;

Далі переходимо до обробника натискання кнопок. Зверніть увагу, що обробник один, і яка копка була натиснута визначаємо за ідентифікатором. В першу чергу отримуємо рядок з поля edIPaddress, якщо поле не заповнене, то виводимо повідомлення про необхідність введення ip-адреси, і більше нічого не робимо.

public void onClick (View v)
{
// отримуємо рядок у полі ip адреси 
EditText etIPaddress = (EditText)findViewById(R. id.edIPaddress);
serIpAddress = etIPaddress.getText().toString();
// якщо поле не заповнене, то виводимо повідомлення про помилку
if (serIpAddress.isEmpty()){
Toast msgToast = Toast.makeText(this, "Введіть ip-адресу", Toast.LENGTH_SHORT);
msgToast.show();
return;
}

. . . . 
}

В Android не рекомендується створювати процеси в основному потоці, це пов'язано з тим, що можливо «підвисання» програми, і користувач або система може просто закрити програму, не дочекавшись відповіді. До таких довгограючим процесів відноситься і робота з мережею. У цьому випадку необхідно створити додатковий потік, в якому і виконувати «довгий» код. В java є стандартний клас Thread, який дозволяє керувати потоками але його ми не будемо, оскільки в Android існує спеціально призначений для цього клас AsyncTask. Докладно можна почитати тут або здесь.
Створюємо клас, який буде займатися відправкою повідомлення, його батьком робимо AsyncTask і перевизначаємо метод doInBackground в тілі якого і буде знаходиться основний код:

SenderThread
class SenderThread extends AsyncTask <Void, Void, Void>
{
@Override
protected Void doInBackground(Void... params) {
try {
// ip адреса сервера
InetAddress ipAddress = InetAddress.getByName(serIpAddress);
// Створюємо сокет
Socket socket = new Socket(ipAddress, port);
// Отримуємо потоки введення/виводу
OutputStream outputStream = socket.getOutputStream();
DataOutputStream out = new DataOutputStream(outputStream);
switch (codeCommand) { // В залежності від коду команди надсилаємо повідомлення
case codeMsg: // Повідомлення
out.write(codeMsg);
// Встановлюємо кодування UTF-8
byte[] outMsg = msg.getBytes("UTF8");
out.write(outMsg);
break;
case codeRotate: // Поворот екрану
out.write(codeRotate);
break;
case codePoff: // Вимкнути
out.write(codePoff);
break;
}
}
catch (Exception ex)
{
ex.printStackTrace();
}
return null;
}
}


Спочатку створюємо екземпляр класу InetAddress, який буде містити в собі ip сервера. Потім створюємо сокет, пов'язуємо його з віддаленим адресою і портом, і запитуємо стандартний потік введення/виводу (вірніше тільки виводу, тому що наш клієнт нічого не отримує). І нарешті, в залежності від значення змінної codeCommand, надсилаємо повідомлення сервера.

Тепер повернемося до нашого обробника натискання кнопок, створимо екземпляр класу SenderThread, потім в залежності від того, яка кнопка була натиснута ініціалізуємо змінну codeCommand, за нею наш потік буде визначати що ми від нього хочемо. І нарешті, активуємо, викликавши методexecute().

. . . 
SenderThread sender = new SenderThread(); // об'єкт представляє потік відправки повідомлень
switch (v.getId()) // id кнопок
{
case R. id.btnSMsg: // надіслати повідомлення
if (!msg.isEmpty()) {
codeCommand = codeMsg;
sender.execute();
}
else { // Якщо повідомлення не задано, то повідомляємо про це
Toast msgToast = Toast.makeText(this, "Введіть повідомлення", Toast.LENGTH_SHORT);
msgToast.show();
}
break;
case R. id.btnRotate: // поворот
codeCommand = codeRotate;
sender.execute();
break;
case R. id.btnPowerOff: // вимкнути
codeCommand = codePoff;
sender.execute();
break;
}
}

Трохи поправимо маніфест програми, дамо дозвіл на використання мережі і wi-fi, без цього нічого працювати не буде:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

Всі! Можна збирати і перевіряти. Ось результат:

image

image

Посилання
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.