Начало
А началось все с того, что вызывает меня генеральный к себе, и говорит: «Вот видишь телефон? Хочу чтобы там была кнопка, я на нее нажимаю, и у меня в ноутбуке кино включается. Нажимаю другую – музыка играет.» И еще чего-то много наговорил, уж не помню. «Задача понятна? Выполняй!» Вот уж не знаю, с чего такая потребность у него возникла. То ли звезды не под тем углом встали, то ли сон какой приснился. Короче, не поймешь этих богатых… Ну да ладно.
Поначалу полез рыться в Гугл в поисках подходящей программы, а потом подумал – а какого черта? Напишу сам. Тем более, что задача не показалась сложной, да и “зов кода” уже давал о себе знать (этакая профессиональная 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>
Соответственно в ресурсах должны быть две картинки одна для нажатого состояния, другая для обычного. Получится вот такой экран:
![image](https://habrastorage.org/getpro/habr/post_images/030/ac6/baa/030ac6baa62ccc2c9a88a72ff42e5f29.png)
На этом с разметкой закончим. Переходим к файлу 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](https://habrastorage.org/getpro/habr/post_images/99e/69b/a8c/99e69ba8c983ad94a158e414bb13efe2.png)
![image](https://habrastorage.org/getpro/habr/post_images/d45/926/a5c/d45926a5c793a61893accc40c25f16a4.png)