Pull to refresh

Управляем компьютером с Android устройства

Reading time10 min
Views55K

Начало


А началось все с того, что вызывает меня генеральный к себе, и говорит: «Вот видишь телефон? Хочу чтобы там была кнопка, я на нее нажимаю, и у меня в ноутбуке кино включается. Нажимаю другую – музыка играет.» И еще чего-то много наговорил, уж не помню. «Задача понятна? Выполняй!» Вот уж не знаю, с чего такая потребность у него возникла. То ли звезды не под тем углом встали, то ли сон какой приснился. Короче, не поймешь этих богатых… Ну да ладно.

Поначалу полез рыться в Гугл в поисках подходящей программы, а потом подумал – а какого черта? Напишу сам. Тем более, что задача не показалась сложной, да и “зов кода” уже давал о себе знать (этакая профессиональная 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

Ссылки


Tags:
Hubs:
+13
Comments32

Articles

Change theme settings