[JAVA] Création d'un logiciel qui reflète l'écran Android sur un PC 2 Édition tactile en temps réel

introduction

Cet article est la suite de Créer un logiciel qui reflète les écrans Android sur un PC 1. Comment créer une fonction pour refléter l'écran d'Android y est expliqué.

Je vais faire quelque chose comme ça

capture5.gif

Vous permet d'utiliser votre appareil Android avec votre souris.

spécification

fig7.png J'aimerais n'avoir qu'une seule connexion entre Android et PC, mais j'ai fait un compromis car le programme semblait compliqué.

Être un logiciel client

Affiche les données provenant de FFmpeg. En outre, l'opération de la souris est convertie en une opération tactile sur l'écran et projetée du côté du terminal.

Être du côté Android

Comme précédemment, le contenu de l'écran est encodé et envoyé côté PC. Il reçoit également des événements tactiles du côté PC et intervient dans le système.

Faire fonctionner un appareil Android à partir d'un PC

Cette section décrit comment les appareils Android gèrent le toucher et les frappes. Vous pouvez implémenter le toucher en temps réel sans le savoir, donc si vous n'êtes pas intéressé, passez à "Implémentation".

Comment puis-je utiliser Android à partir d'un PC? Commençons par un exemple simple.

Entrez dans le shell du terminal et appuyez sur la commande

Android est un système d'exploitation basé sur Linux. Ainsi, bien qu'il s'agisse d'une édition limitée, vous pouvez utiliser le terminal Linux.

adb shell

Vous pouvez entrer dans le dialogue en faisant. Ensuite, il y a une commande appelée ** input ** qui envoie des opérations tactiles et des opérations sur les touches, alors utilisez-la. Un exemple est donné ci-dessous.

input touchscreen tap x y
input touchscreen swipe x1 y1 x2 y2
input keyevent Key
input text Text

En les exécutant, vous pouvez facilement envoyer un événement au terminal. Mais quand je l'exécute, cela prend ** du temps avant que l'événement ne soit déclenché **. Cela ne convient pas pour un fonctionnement en temps réel. De plus, que se passe-t-il si vous souhaitez effectuer des opérations compliquées autres que le tapotement et le balayage?

getEvent Utiliser sendEvent

La commande ** getevent ** est une commande qui génère les données envoyées à partir de l'écran tactile ou de la touche physique du terminal. Essayez d'utiliser l'écran tactile après avoir exécuté la commande suivante.

getevent

Ensuite, les valeurs numériques seront affichées dans une ligne comme indiqué dans l'image ci-dessous. image.png Ce sont les données envoyées depuis le panneau tactile. Le système d'exploitation interprète et reflète ces données. Comment il est réellement interprété est expliqué dans [Android] Toucher le terminal depuis le programme [ADB]. Jetez un coup d'oeil s'il vous plait.

** sendevent ** est une commande qui peut envoyer des données arbitraires comme si elles étaient envoyées depuis un écran tactile. En d'autres termes, l'opération tactile peut être reproduite en renvoyant les données obtenues par ** getevent ** avec ** sendevent **. Cependant, cette commande est également lente à exécuter et ne peut pas être entièrement reproduite.

Manipulez directement les fichiers de l'appareil

Étant donné qu'Android est basé sur Linux, il utilise des fichiers de périphérique. Cela peut ne pas être familier à ceux qui utilisent des systèmes d'exploitation non Unix tels que Un fichier de périphérique est un fichier spécial utilisé pour interagir avec divers périphériques connectés. Par exemple, si vous ouvrez le fichier de périphérique de l'écran tactile, vous pouvez lire les données relatives au fonctionnement de l'écran tactile telles qu'obtenues par getevent. Inversement, si vous écrivez dans un fichier de périphérique, les données écrites seront traitées comme si elles provenaient de ce périphérique, de la même manière que sendevent. Fichier périphérique [Fichier spécial de l'appareil](https://uc2.h2np.net/index.php/%E3%83%87%E3%83%90%E3%82%A4%E3%82%B9%E3%82%B9 % E3% 83% 9A% E3% 82% B7% E3% 83% A3% E3% 83% AB% E3% 83% 95% E3% 82% A1% E3% 82% A4% E3% 83% AB) Je pense que ce domaine sera utile.

Ouvrons en fait le fichier de l'appareil. Je pense que vous pouvez voir le chemin / dev / input / event4 dans l'image ci-dessus. Ce sera l'emplacement du fichier de l'appareil. Il existe plusieurs fichiers de périphérique tels que event0, event1 .... dans le répertoire / dev / input, qui correspondent aux panneaux tactiles, aux touches physiques, aux capteurs, etc. ** Veuillez noter que le numéro correspondant diffère selon le terminal. ** **

cat /dev/input/event●

Lorsque vous exécutez cette commande, les données de cet appareil circuleront. Cependant, cela devient comme ça.

image.png Les données brutes obtenues à partir du fichier de l'appareil sont des données binaires, non affichées en caractères. GetEvent, sendEvent et la commande d'entrée introduite au début convertissent entre binaire et caractère (valeur numérique).

Au fait,

cat /dev/input/event● > /sdcard/event.bin

Ce faisant, vous pouvez enregistrer les données brutes dans un fichier.

cat /sdcard/event.bin > /dev/input/event●

Ensuite, vous pouvez reproduire les données tactiles, mais il y a aussi un piège ici. Ça court trop vite (sourire amer). Les données d'événement enregistrées en quelques secondes couleront en un instant, donc je pense qu'il est trop tôt pour fonctionner correctement. Afin de reproduire l'opération, il est nécessaire d'insérer le sommeil afin que les données puissent être envoyées à un moment approprié.

Que faire après tout

Le fait est que l'événement tactile envoyé du côté PC peut être envoyé au fichier de l'appareil. Vous pourrez peut-être l'écrire avec un script shell, mais puisque nous développons avec Android Studio, Il est facile d'écrire en Java / Kotlin. Cependant, j'avais l'habitude d'accéder aux fichiers de périphériques comme j'aime, mais c'est une technique qui ne peut être réalisée qu'avec les privilèges du shell. En d'autres termes, les applications régulières n'ont pas l'autorisation de jouer avec les fichiers système. (Sauf s'il est enraciné) J'étais sur le point d'abandonner, mais j'ai trouvé cette question. How does vysor create touch events on a non rooted device? Comment Vysor est un événement tactile en temps réel sur un appareil non rooté Est-ce que tu cours? Est la question. Et la réponse est

What he does is, he then starts his Main class as a separate process using this shell user. Now, the Java code inside that Main class has the same privileges as the shell user (because duh, it's linux).

Ce qu'il fait, c'est utiliser les privilèges de l'utilisateur Shell pour lancer la classe Main dans un processus séparé. Désormais, le code Java de la classe Main a les mêmes autorisations que l'utilisateur Shell (car Android est Linux).

En d'autres termes, si vous exécutez la classe incluse dans le package apk depuis le shell, le programme lancé peut également utiliser le privilège du shell. Ce n'est pas étonnant, mais je n'y ai pas pensé. Cette fois, je vais implémenter le contact en temps réel avec cette méthode.

Maintenant, la mise en œuvre

Comme mentionné ci-dessus, la puissance de la coque est nécessaire pour intervenir dans la zone système à partir de l'application. Tout d'abord, pour démarrer la classe incluse dans le package apk avec les privilèges du shell, procédez comme suit.

sh -c "CLASSPATH=[Chemin vers le fichier apk] /system/bin/app_process /system/bin [nom du paquet].[Le nom de la classe qui contient la méthode principale]"

Le chemin d'accès au fichier apk est

pm path [nom du paquet]

Vous pouvez l'obtenir avec, mais si vous déboguez normalement avec Android Studio ici, plusieurs chemins seront affichés. Il s'agit d'un phénomène qui se produit lors de l'utilisation d'Instant Run, une fonction qui raccourcit le temps de construction et reflète les changements de programme en temps réel. En divisant apk en plusieurs parties, il semble que seule la différence soit construite et installée au moment du changement pour gagner du temps. Cependant, si elle est divisée, la commande ci-dessus ne fonctionnera pas correctement, vous devez donc désactiver Instant Run. L'emplacement du paramètre est le suivant. image.png

Il est encore plus important de noter que l'application elle-même lancée par l'utilisateur et le programme lancé avec les privilèges du shell appartiennent au même package, mais comme les processus sont différents, le partage statique ne peut pas être effectué du tout. Par conséquent, si vous souhaitez communiquer, vous devrez le faire via socket, etc.

Intervention des événements dans le système à partir du programme

Regardez d'abord le code ci-dessous.

InputService.java


public class InputService {
    InputManager im;
    Method injectInputEventMethod;


    public InputService() throws Exception {

        //Obtenir une instance de InputManager
        im = (InputManager) InputManager.class.getDeclaredMethod("getInstance").invoke(null, new Object[0]);

        //Vous permet d'appeler des méthodes statiques qui génèrent MotionEvent
        MotionEvent.class.getDeclaredMethod(
                "obtain",
                long.class, long.class, int.class, int.class,
                MotionEvent.PointerProperties[].class, MotionEvent.PointerCoords[].class,
                int.class, int.class, float.class, float.class, int.class, int.class, int.class, int.class
        ).setAccessible(true);

        //Obtenir une méthode pour intervenir sur un événement dans le système
        injectInputEventMethod = InputManager.class.getDeclaredMethod("injectInputEvent", new Class[]{InputEvent.class, int.class});

    }

    //Générer un événement tactile
    public void injectMotionEvent(int inputSource, int action, float x, float y) throws InvocationTargetException, IllegalAccessException {

        MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[1];
        pointerProperties[0] = new MotionEvent.PointerProperties();
        pointerProperties[0].id = 0;


        MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[1];
        pointerCoords[0] = new MotionEvent.PointerCoords();
        pointerCoords[0].pressure = 1;
        pointerCoords[0].size = 1;
        pointerCoords[0].touchMajor = 1;
        pointerCoords[0].touchMinor = 1;
        pointerCoords[0].x = x;
        pointerCoords[0].y = y;

        MotionEvent event = MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), action, 1, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, inputSource, 0);
        injectInputEventMethod.invoke(im, new Object[]{event, 0});
    }

    //Générer un événement clé
    public void injectKeyEvent(KeyEvent event)throws InvocationTargetException, IllegalAccessException{
        injectInputEventMethod.invoke(im, new Object[]{event, 0});
    }
}

Ce code est basé sur here et utilise la nouvelle API. Je l'ai changé pour l'utiliser.

Nous utilisons les privilèges du shell pour accéder aux méthodes système et aux instances qui sont normalement inaccessibles par réflexion. Cela s'appelle "injectInputEvent", et si vous passez des données d'événement à cette méthode, l'événement sera exécuté. En regardant la source AOSP pour voir où la méthode existe réellement, [ici](http://tools.oesf.biz/android-8.1.0_r1.0/xref/frameworks/base/core/java/android C'était sur la ligne 914 de /hardware/input/InputManager.java). L'annotation Hide est normalement inaccessible. réflexion? Pour ceux qui disent, cet article peut être utile.

Implémentation du serveur hôte d'entrée

Exécutez l'événement envoyé depuis l'ordinateur personnel à l'aide de la classe InputService ci-dessus.

InputHost.java


public class InputHost {
    static InputService inputService;

    static ServerSocket listener;//Socket serveur

    static Socket clientSocket;//Prise côté client
    static InputStream inputStream;//Pour recevoir des messages de clients
    static OutputStream outputStream;//Stream pour l'envoi de données au client


    static boolean runnning = false;


    public static void main(String args[]) {
        try {
            inputService = new InputService();
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        try {
            listener = new ServerSocket();
            listener.setReuseAddress(true);
            listener.bind(new InetSocketAddress(8081));
            System.out.println("Server listening on port 8081...");

            clientSocket = listener.accept();//Attendez la connexion

            System.out.println("Connected");

            inputStream = clientSocket.getInputStream();
            outputStream = clientSocket.getOutputStream();

            runnning = true;

            BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

            while (runnning) {
                String msg = reader.readLine();
                String[] data = msg.split(" ");

                if (data.length > 0) {
                    if (data[0].equals("screen")) {//Pour les données tactiles
                        inputService.injectMotionEvent(InputDeviceCompat.SOURCE_TOUCHSCREEN, Integer.valueOf(data[1]), Integer.valueOf(data[2]), Integer.valueOf(data[3]));
                    } else if (data[0].equals("key")) {//Pour les clés
                        inputService.injectKeyEvent(new KeyEvent(Integer.valueOf(data[1]), Integer.valueOf(data[2])));
                    } else if (data[0].equals("exit")) {//Fin d'appel
                        Disconnect();
                    }
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
            Disconnect();
        }

    }

    //Processus de coupe
    private static void Disconnect() {

        runnning = false;

        try {

            listener.close();
            if (clientSocket != null)
                clientSocket.close();

        } catch (Exception ex) {
            ex.printStackTrace();
        }

        System.out.println("Disconnected");

    }
}

C'est un simple programme serveur. J'ai également pensé à moderniser l'échange de données avec le PC en utilisant json. Ce n'est pas un gros problème, j'ai donc décidé de l'envoyer dans un format comme csv séparé par des espaces.

C'est la seule implémentation du côté Android. ** Le code complet est ici **

Ensuite, implémentez le côté PC.

Créer un client

Je voudrais le créer avec C # & WPF. Pourquoi n'avez-vous pas choisi WinForms? En effet, il était difficile d'afficher l'image à 60 FPS. Quand je l'ai implémenté, c'était environ 30 FPS. Lorsque DoubleBuffered a été désactivé, il est devenu 60FPS, mais le scintillement était si grand qu'il n'était pas pratique.

WPF se sent également subtil, mais il est meilleur que WinForms car il utilise le GPU pour dessiner. Si vous le faites sérieusement, vous utiliserez une API graphique telle que OpenGL (OpenTK pour C #) ... Je n'ai jamais pensé que le côté réception serait le goulot d'étranglement plutôt que le côté envoi ^^;

** Cliquez ici pour consulter le code client complet (https://github.com/SIY1121/ScreenCastClient) **

UI image.png

MainWindow.xaml


<Window x:Class="ScreenCastClient.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ScreenCastClient"
        mc:Ignorable="d"
        Title="ScreenCastClient" Height="819.649" Width="420.611" Loaded="Window_Loaded" Closing="Window_Closing">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="60"/>
        </Grid.RowDefinitions>
        <Image x:Name="image" MouseDown="image_MouseDown" MouseUp="image_MouseUp" MouseMove="image_MouseMove"/>

        <Grid Grid.Row="1" Background="#FF008BFF">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="204*"/>
                <ColumnDefinition Width="193*"/>
            </Grid.ColumnDefinitions>
            <Polygon Points="0,15 25,0 25,30" Fill="White" Margin="30,17,0,0" HorizontalAlignment="Left" Width="36" MouseDown="Polygon_MouseDown" MouseUp="Polygon_MouseUp" />
            <Ellipse Fill="White" Margin="186,18,181,12" Width="30" HorizontalAlignment="Center" MouseDown="Ellipse_MouseDown" MouseUp="Ellipse_MouseUp" Grid.ColumnSpan="2"/>
            <Rectangle Fill="White" Margin="0,17,30,10" HorizontalAlignment="Right" Width="30" MouseDown="Rectangle_MouseDown" MouseUp="Rectangle_MouseUp" Grid.Column="1"/>
        </Grid>
    </Grid>
</Window>

Méthode à utiliser à l'avenir

Voici un résumé des fonctions qui seront fréquemment utilisées à l'avenir. Les expressions régulières sont très utiles lorsque vous souhaitez extraire uniquement les nombres requis de certaines données. De plus, pour la vérification Testeur et débogueur de regex en ligne: PHP, PCRE, Python, Golang et JavaScript Ces sites sont très utiles.

C#:MainWindow.xaml.cs


        //Exécutez simplement la commande et renvoyez la sortie standard
        private string Exec(string str)
        {
            Process process = new Process
            {
                StartInfo =
                 {
                    FileName =  "cmd",
                    Arguments = @"/c " + str,
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardInput = true,
                    RedirectStandardError = true,
                    RedirectStandardOutput = true
                 },
                EnableRaisingEvents = true
            };
            process.Start();
            string results = process.StandardOutput.ReadToEnd();
            process.WaitForExit();
            process.Close();
            return results;
        }

        //Renvoie un tableau de données correspondant à une expression régulière
        private string[] GetRegexResult(string src, string pattern)
        {
            Regex regex = new Regex(pattern);
            Match match = regex.Match(src);
            string[] res = new string[match.Groups.Count - 1];
            for (int i = 1; i < match.Groups.Count; i++)
                res[i - 1] = match.Groups[i].Value;
            return res;
        }

Décoder et afficher des vidéos en coopération avec FFmpeg

Les applications de tout système d'exploitation ont des fonctions d'entrée et de sortie standard. fig8-2.png

Dans les logiciels de conversion vidéo généraux, l'entrée et la sortie sont effectuées par des fichiers, mais comme FFmpeg peut utiliser l'entrée et la sortie standard, vous pouvez lire les données décodées à partir du programme en définissant la destination de sortie sur stdout. De plus, dans le cas de FFmpeg, la spécification est que le journal est sorti de stderr. Le programme suivant démarre FFmpeg et établit une connexion avec stdout et stderr.

C#:MainWindow.xaml.cs


        private void StartFFmpeg()
        {
            //Paramètres du port
            Exec("adb forward tcp:8080 tcp:8080");

            var inputArgs = "-framerate 60  -analyzeduration 100 -i tcp://127.0.0.1:8080";
            var outputArgs = "-f rawvideo -pix_fmt bgr24 -r 60 -flags +global_header - ";
            Process process = new Process
            {
                StartInfo =
                 {
                    FileName = "ffmpeg.exe",
                    Arguments = $"{inputArgs} {outputArgs}",
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardInput = true,
                    RedirectStandardError = true,//Rendre stderr lisible
                    RedirectStandardOutput=true//Rendre stdout lisible
                 },
                EnableRaisingEvents = true
            };
            process.ErrorDataReceived += Process_ErrorDataReceived;//Les journaux proviendront de stderr, donc traitez-les séparément.
            process.Start();
            rawStream = process.StandardOutput.BaseStream;//Les données circulent depuis stdout, alors récupérez le flux
            process.BeginErrorReadLine();
            running = true;
            Task.Run(() =>
            {
                //Commencer à lire dans un autre fil
                ReadRawData();
            });
        }

Définissez les arguments requis et démarrez FFmpeg. Il existe deux façons d'obtenir des données à partir de la sortie, la première consiste à s'inscrire à un événement et la seconde est d'obtenir un flux et de le lire vous-même. Le premier peut être facilement obtenu, mais il ne peut pas être utilisé pour échanger des données binaires car il est converti en données de caractères. Ce dernier gère les flux, donc c'est un peu encombrant, mais un contrôle fin est possible. Cette fois, puisque le journal provient de stderr, le premier est lu, et comme les données binaires de l'image proviennent de stdout, les données sont lues par la dernière méthode.

Journaux de processus envoyés depuis FFmpeg

C#:MainWindow.xaml.cs


        //Lire la sortie d'erreur standard de FFmpeg
        private void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (e.Data == null) return;

            Console.WriteLine(e.Data);

            if (imageWidth == 0 && imageHeight == 0)//Lorsque la taille à envoyer n'est pas encore confirmée
            {
                //Travail approximatif d'extraction de la taille de la sortie FFmpeg
                string[] res = GetRegexResult(e.Data, @"([0-9]*?)x([0-9]*?), [0-9]*? fps");
                if (res.Length == 2)
                {
                    imageWidth = int.Parse(res[0]);
                    imageHeight = int.Parse(res[1]);
                    bytePerframe = imageWidth * imageHeight * 3;

                    if(imageWidth>imageHeight)//Pour écran paysage
                    {
                        //Échangez les valeurs maximale et minimale des coordonnées tactiles
                        int tmp = displayWidth;
                        displayWidth = displayHeight;
                        displayHeight = tmp;
                    }

                    Dispatcher.Invoke(() => {//Si vous ne créez pas de bitmap dans le thread de l'interface utilisateur, il ne peut pas être reflété dans l'interface utilisateur
                        writeableBitmap = new WriteableBitmap(imageWidth, imageHeight, 96, 96, PixelFormats.Bgr24, null);
                        image.Source = writeableBitmap;
                    });
                }
            }

        }

La taille de l'image est essentielle pour restaurer les données brutes envoyées dans le futur. Lorsque FFmpeg commence la conversion, il sort les informations du flux à afficher dans le journal, de sorte qu'il effectue un travail approximatif pour en extraire la taille de l'image. Une variable appelée bytePerFrame est le nombre d'octets requis pour générer une image à une image. Il peut être calculé par le nombre d'octets utilisés pour vertical x horizontal x 1 pixel de l'image. Cette fois, FFmpeg est configuré pour sortir avec rgb24 (8 bits pour chaque rgb équivaut à 24 bits), de sorte que le nombre d'octets utilisés pour 1 pixel est de 3. Si vous vous inquiétez du mécanisme des images, Vous ne savez pas? Connaissance de base des images et de la structure des fichiers. .

Renvoie les données envoyées de FFmpeg à l'image

C#:MainWindow.xaml.cs


        //Lire rawStream à partir de FFmpeg et écrire dans Bitmap
        private void ReadRawData()
        {
            MemoryStream ms = new MemoryStream();

            byte[] buf = new byte[10240];
            while (running)
            {
                int resSize = rawStream.Read(buf, 0, buf.Length);

                if (ms.Length + resSize >= bytePerframe)//Lorsque les données lues à cette heure atteignent ou dépassent les données pour une image
                {
                    int needSize = bytePerframe - (int)ms.Length;//La taille des données restantes requises pour une image
                    int remainSize = (int)ms.Length + resSize - bytePerframe;//Taille des données excédentaires

                    ms.Write(buf, 0, bytePerframe - (int)ms.Length);//Lisez le reste des données nécessaires dans un cadre

                    Dispatcher.Invoke(() =>
                    {
                        if (writeableBitmap != null)//Écrire des données
                            writeableBitmap.WritePixels(new Int32Rect(0, 0, imageWidth, imageHeight), ms.ToArray(), 3 * imageWidth, 0);
                    });

                    ms.Close();
                    ms = new MemoryStream();
                    ms.Write(buf, needSize + 1, remainSize);//Écrire des données excédentaires
                }
                else
                {
                    ms.Write(buf, 0, resSize);//Accumuler des données
                }
            }
        }

Les données sont acquises à partir du flux et accumulées dans MemoryStream. Lorsqu'une trame de données est sécurisée, les données de MemoryStream sont restaurées dans une image. WritableBitmap a une méthode qui restaure à partir d'un tableau, alors utilisez-la. En outre, l'accès à WritableBitmap doit être effectué dans le thread d'interface utilisateur.

Démarrez InputHost et connectez-vous

C#:MainWindows.xaml.cs


        //Démarrez InputHost et connectez-vous
        private void StartInputHost()
        {
            string inputInfo = Exec("adb shell getevent -i");//Obtenez des données liées à l'entrée du terminal Android
            //Extraire la valeur maximale des coordonnées tactiles de l'intérieur
            string[] tmp = GetRegexResult(inputInfo, @"ABS[\s\S]*?35.*?max (.*?),[\s\S]*?max (.*?),");
            displayWidth = int.Parse(tmp[0]);
            displayHeight = int.Parse(tmp[1]);

            //Paramètres du port
            Exec("adb forward tcp:8081 tcp:8081");
            //Obtenez le chemin de l'application
            //Supprimer les caractères supplémentaires et le code de saut de ligne
            string pathToPackage = Exec("adb shell pm path space.siy.screencastsample").Replace("package:", "").Replace("\r\n", "");

            Process process = new Process
            {
                StartInfo =
                 {
                    FileName = "adb",
                    Arguments = $"shell",
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardInput = true,
                    RedirectStandardError = true,
                    RedirectStandardOutput = true
                 },
                EnableRaisingEvents = true
            };
            process.Start();
            process.OutputDataReceived += (s, e) =>
            {
                Console.WriteLine(e.Data);//Je peux faire quelque chose dans le futur
            };
            process.BeginOutputReadLine();
            //Démarrez InputHost avec les privilèges Shell
            process.StandardInput.WriteLine($"sh -c \"CLASSPATH={pathToPackage} /system/bin/app_process /system/bin space.siy.screencastsample.InputHost\"");
            System.Threading.Thread.Sleep(1000);//Attendez qu'il démarre
            TcpClient tcp = new TcpClient("127.0.0.1", 8081);//Connectez-vous à InputHost
            streamToInputHost = tcp.GetStream();
        }

J'exécute d'abord adb shell getevent -i et je lis le résultat, Cette commande affiche des données sur le périphérique d'entrée Android. Ici, la valeur maximale des coordonnées que le panneau tactile peut prendre est acquise. Et nous démarrons InputHost. Veuillez noter que nous faisons des choses difficiles comme attendre 1 seconde jusqu'à ce qu'il démarre.

Envoyer des données à InputHost

Maintenant que les préparations autour de la connexion sont terminées, il ne nous reste plus qu'à envoyer les données à InputHost.

C#:MainWindow.xaml.cs


        private void image_MouseDown(object sender, MouseButtonEventArgs e)
        {
            Point p = GetDisplayPosition(e.GetPosition(image));
            byte[] sendByte = Encoding.UTF8.GetBytes($"screen 0 {p.X} {p.Y}\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
            mouseDown = true;
        }

        private void image_MouseMove(object sender, MouseEventArgs e)
        {
            if (mouseDown)
            {
                Point p = GetDisplayPosition(e.GetPosition(image));
                byte[] sendByte = Encoding.UTF8.GetBytes($"screen 2 {p.X} {p.Y}\n");
                streamToInputHost.Write(sendByte, 0, sendByte.Length);
            }
        }

        private void image_MouseUp(object sender, MouseButtonEventArgs e)
        {
            Point p = GetDisplayPosition(e.GetPosition(image));
            byte[] sendByte = Encoding.UTF8.GetBytes($"screen 1 {p.X} {p.Y}\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
            mouseDown = false;
        }

        //Conversion de la position de la souris en coordonnées tactiles du terminal
        private Point GetDisplayPosition(Point p)
        {
            int x = (int)(p.X / image.ActualWidth * displayWidth);
            int y = (int)(p.Y / image.ActualHeight * displayHeight);
            return new Point(x, y);
        }

J'envoie des données en utilisant un événement sur la souris dans l'image. Le deuxième nombre 0,1,2 dans le délimiteur vide signifie 0 pour Donw, 1 pour Up et 2 pour Move. De plus, l'événement clé est le suivant.

C#:MainWindow.xaml.cs


        private void Polygon_MouseDown(object sender, MouseButtonEventArgs e)
        {
            byte[] sendByte = Encoding.UTF8.GetBytes($"key 0 4\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
        }

        private void Polygon_MouseUp(object sender, MouseButtonEventArgs e)
        {
            byte[] sendByte = Encoding.UTF8.GetBytes($"key 1 4\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
        }

        private void Ellipse_MouseDown(object sender, MouseButtonEventArgs e)
        {
            byte[] sendByte = Encoding.UTF8.GetBytes($"key 0 3\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
        }

        private void Ellipse_MouseUp(object sender, MouseButtonEventArgs e)
        {
            byte[] sendByte = Encoding.UTF8.GetBytes($"key 1 3\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
        }

        private void Rectangle_MouseDown(object sender, MouseButtonEventArgs e)
        {
            byte[] sendByte = Encoding.UTF8.GetBytes($"key 0 187\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
        }

        private void Rectangle_MouseUp(object sender, MouseButtonEventArgs e)
        {
            byte[] sendByte = Encoding.UTF8.GetBytes($"key 1 187\n");
            streamToInputHost.Write(sendByte, 0, sendByte.Length);
        }

Le deuxième délimiteur vide a la même signification que ci-dessus. Le troisième est le numéro unique de la clé, qui peut être confirmé sur KeyEvent. Vous pouvez également envoyer des clés qui ne sont pas implémentées sur votre appareil. Par exemple, 120 se voit attribuer une touche PrintScreen. La capture d'écran est généralement effectuée en appuyant sur la touche d'alimentation et en diminuant le volume en même temps. Vous pouvez prendre une capture d'écran simplement en envoyant cette clé. Si vous voulez l'essayer tout de suite

adb shell input keyevent 120

Peut être reproduit avec.

Je vais vraiment le déplacer

Redirection de port, lancement d'InputHost depuis le shell, etc. Toutes les choses gênantes sont implémentées dans le logiciel client, donc c'est facile.

procédure

    1. Lancez l'application sur le côté Android et appuyez sur Démarrer
  1. Démarrez le logiciel client

Seulement ça. L'écran est maintenant projeté et vous pouvez utiliser l'écran avec la souris.

Impressions

J'étais confus au début car exécuter la classe dans apk avec Shell n'est pas un développement d'application normal, mais j'ai pu l'implémenter. Depuis adb est devenu indispensable avec cette fonction, il est devenu nécessaire d'avoir adb installé s'il est distribué aux utilisateurs normaux. Surtout, maintenant que seul adb peut être téléchargé, je pense que le seuil d'introduction a été abaissé. (Est-ce inclus?)

Maintenant, je suis assez proche de Vysor. Cependant, je n'ai pas encore été en mesure de saisir des caractères ou de transférer des fichiers, alors j'aimerais l'implémenter ensuite. Ensuite, merci d'avoir regardé jusqu'au bout.

Recommended Posts

Création d'un logiciel qui reflète l'écran Android sur un PC 2 Édition tactile en temps réel
Création d'un logiciel qui visualise la structure des données ~ Heap ~
[Python / C] J'ai créé un appareil qui fait défiler sans fil l'écran d'un PC à distance.
Afficher un écran qui nécessite une connexion à l'affichage numérique
Transition vers l'écran de mise à jour avec le Django a tag
Création d'un script Python prenant en charge l'API e-Stat (ver.2)
La première étape de la création d'une application sans serveur avec Zappa
Faisons un robot qui résout le Rubik Cube! 3 Logiciel
J'ai essayé de créer une configuration RAID logicielle SATA qui démarre le système d'exploitation sur Ubuntu Server