Начало
А началось все с того, что вызывает меня генеральный к себе, и говорит: «Вот видишь телефон? Хочу чтобы там была кнопка, я на нее нажимаю, и у меня в ноутбуке кино включается. Нажимаю другую – музыка играет.» И еще чего-то много наговорил, уж не помню. «Задача понятна? Выполняй!» Вот уж не знаю, с чего такая потребность у него возникла. То ли звезды не под тем углом встали, то ли сон какой приснился. Короче, не поймешь этих богатых… Ну да ладно.
Поначалу полез рыться в Гугл в поисках подходящей программы, а потом подумал – а какого черта? Напишу сам. Тем более, что задача не показалась сложной, да и “зов кода” уже давал о себе знать (этакая профессиональная 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 вставляем следующий:
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 адреса. Надо сказать, что это единственная проверка на правильность введенных данных, все остальное остается на совести пользователя. Еще хочу cказать о кнопке 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>
Соответственно в ресурсах должны быть две картинки одна для нажатого состояния, другая для обычного. Получится вот такой экран:

На этом с разметкой закончим. Переходим к файлу 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" />
Все! Можно собирать и проверять. Вот результат:


