Управляем компьютером с 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 вставляем следующий:

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

На этом с разметкой закончим. Переходим к файлу 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

Ссылки


Поделиться публикацией

Комментарии 32

    +5

    Все это здорово конечно, но я бы teamviewer'ом ограничился.

      0
      Для отладки я использовал VirtualBox с установленной виртуальной машиной Android, ибо родной эмулятор жутко тормозной, и жизни не хватить что бы с его помощью что-то отладить.

      Справедливости ради стоит отметить, что в последнее время эмуляторы на x86 в плане скорости стали гораздо приятнее. Да, с ними очень много других проблем, но хотя бы они стали быстрыми и отзывчивыми.
        0
        Безусловно они стали приятнее, но, к сожалению, новые эммуляторы требуют аппаратной поддержки от ПК.
        требования
        For accelerated emulator: 64-bit operating system and Intel® processor with support for Intel® VT-x, Intel® EM64T (Intel® 64), and Execute Disable (XD) Bit functionality
          0
          Из того, чем пользовался, лучший эмулятор это Xamarin Android Player. Да, бывает подглючивает, но скорость работы нормальная и для работы вполне можно использовать.
          0
          Имхо намного более удобный и современный способ общения между устройствами если уж используется .NET WCF.
          Ожидал в статье увидеть управление устройством не находящимся в той же локальной сети, сам хочу себе подобное сделать aka TeamViewer только без рабочего стола, а лишь с нужными кнопками
            0
            В данном примере вовсе и не обязательно что бы клиент и сервер были в одной подсети. Все будет работать, если сети маршрутизируемы, т.е «видят» друг друга. Более того можно подключатся из интернета, правда для этого нужно будет сделать проброс порта (в данном случае это порт 10000) в NAT-е Вашего роутера.
              0
              Я думаю более интересный вариант с двумя серыми ip адресами и сервера посредника.
              Пришел в голову вариант с использованием Google Drive, на машине с win пишем скрипты, которые отрабатывают при появлении файла в папке и всё
            0
            Для отладки я использовал VirtualBox с установленной виртуальной машиной Android, ибо родной эмулятор жутко тормозной, и жизни не хватить что бы с его помощью что-то отладить


            Genymotion попробуйте, основана на VirtualBox, с готовыми шаблонами устройств, можно настраивать всякие параметры. Устройство создается практически в пару кликов. Есть плагин интеграции для студии. Бесплатная версия урезана, но для разработки и тестов вполне хватает.
              0
              «В Visual Studio создаем новое Windows Form приложением с именем, скажем, FunnyJoke. Открываем файл Program.cs и удаляем весь код в теле функции Main.» — Думаю в следующий раз стоит сразу создать консольное приложение или службу.
                0
                Правильнее конечно служба, просто не хотелось усложнять.
                0
                Почему AsyncTask, а не Service? Есть аргументация выбора?
                  0
                  Необходимо было разработать приложение, которое нужно запустить, нажать на кнопку и закрыть. В фоне не надо что бы оно работало. Может быть вы имели в виду Thread?
                    0
                    Я представил это как: запустил приложение, ввел ip, стартовал сервис(внутри сокет соединился), затем я могу 20 раз отправить сообщение, затем повернуть экран и потом выключить — в таком случае было бы не логично на каждое событие разрывать и устанавливать сокет соединение, достаточно было бы onBind к сервису и передать какие-то коды.
                    Возможно, в описанном Вами сценарии, AsyncTask и имеет смысл, хотя я бы лучше использовал Thread, т.к. ответ в UI Вы не получаете.
                      0
                      Ну это смотря от поставленной задачи, если часто надо посылать сообщения, то думаю — да, каждый раз разрывать соединение смысла нет.
                  0
                  Вообще, логичней было бы прицепить через xmpp.org/software/libraries.html
                    –6
                    Сервер на сишарпе?
                    Мусье знает толк в извращениях

                    PS по хорошему серверная честь это должен быть виндовый сервис написанный на С++ с использованием винапи
                      0
                      PPS Мое скромное имхо: связка Visual Studio + Visual GDB + BlueStacks лучший вариант для разработки.
                      Главное что и клиент и сервер в рамках одного солюшена и на одной машине можно тестировать
                        0
                        А чем, собственно, «виндовый» сервис выигрывает у .NET службы?
                        Все равно не платформонезависимый код, а программировать на C# быстрее и проще, все из коробки есть.
                          –3
                          Ссылаться на размер и производительность в наше время уже не актуально но все же.
                          Не знаю, писать такие вещи на шарпе… Я свое мнение высказал — имхо это извращение.

                          Да и не сказал бы я что на шарпе это делать быстрее.
                          Скорее всего одинаково или даже на С++ побыстрее будет
                            0
                            Аргументировано!
                          0
                          Извращение — это в 2016 году использовать WinApi. C++ хороший язык, но раскрывается он точно не в сервисах. Гораздо удобнее юзать тот же .Net для таких вещей.
                          0

                          Я бы лучше взял за основу Android версию KDE Connect, там очень удобный UI для управления компьютером, разобрался с её протоколом, и написал бы клиентскую версию для Windows.

                            0
                            Можно ли реализовать клиент не в виде приложения для Android, а скажем на вебсайте?
                              0

                              Делал такое когда разбирался в Go, сейчас пока в состоянии стагнации — https://github.com/vblazhnov/RemoteControl. К сожалению, ничего не понимаю в frontend, поэтому с интерфейсом все плохо. Пытался проектировать так, что бы получилась система "плагинов" времени компиляции, сейчас реализовано управление мышью, клавиатурой, громкостью, вызов команды shutdown.
                              Буду рад пул-реквестам, особенно по интерфейсу.

                                0
                                В развитие вопроса keslo прошу сообщить о возможности реализации клиент — Айphone, сервер под Windows, а связь по кабелю USB?
                                0
                                Как раз недавно реализовывал подобную штуку. ПК часть писал на java. C помощью библиотеки Roboto можно эмулировать нажатие клавиш и клики мышкой. В ответ ПК постоянно слал скриншоты. Получалось что то около 2 скриншотов в секунду. Фильм конечно не посмотришь с такой скоростью, но управлять курсором получалось вполне комфортно.
                                  0
                                  А почему бы не описать в статье что-то более полезное и интересное, чем бесполезную затею вращать экран?

                                  Я пишу аналогичную программу, но реализация оказалась невозможной в связи с тем, что перед тем, как управлять компом с телефона, нужно комп разблокировать, а программно это невозможно. Или возможно? Может автор знает как? ;)
                                    0
                                    На счет разблокирования — не знаю. Если система при этом запросит пароль на вход, то скорее всего не получится. А почему обязательно нужно разблокировать?
                                      0
                                      Ну потому что если писать полноценную программу дистанционного управления компом с телефона, то это подразумевает под собой весь набор функций — включение компьютера, запуск приложений, управление приложеними, отключение монитора, включение монитора, выключение компа (режим сна). Я реализовал почти всё, но когда дело дошло до необходимости разблокировки компа после выхода из режима сна — тут я и понял, что проделал всю эту немаленькую работу абсолютно напрасно. Windows не позволяет программно разблокировать систему. Только интерактивно, то есть все такие программы заведомо бесполезны. Вот так, спасибо компании микрософт. Хотя наверно у них есть на то причины.
                                        0
                                        На самом деле, это должно быть возможно.
                                        Для WinXP через GINA, для Vista и более поздних, через механизм Credential Providers
                                        Конечно, это не так тривиально.
                                        Вроде бы, софт logmein такое умеет.
                                          0
                                          GINA вроде как для W7 уже нету, а вот насчёт Credential Providers… читал об этом, но говорят что решения всё же нету. Неуверен, почитаю ещё раз, когда в тот раз копал, решения найти не смог.
                                    0
                                    а можно все это выложить в готовых файлах, типа поставил на винду, поставил на андроид, привязал к друг другу и пользовать?

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                    Самое читаемое