Résumé de la célèbre fosse d'escalade du défi du rouge à lèvres d'Android Douyin

Contexte du projet

Cette année, je crois que beaucoup de gens verront diverses machines à poupées, boîtes porte-bonheur, défis de rouge à lèvres et autres machines similaires dans divers centres commerciaux ou cinémas. Sur Douyin, le "Lipstick Challenge", c'est aussi la pagaille. Vous pouvez gagner un rouge à lèvres YSL pour seulement 10 yuans. C'est tentant d'y penser. Peut-être que les programmeurs connaîtront les astuces de ces machines en un coup d'il, mais cette machine s'adresse aux consommateurs sujets à la consommation impulsive, comme les couples, les filles, les adultes avec enfants ; les créatures étranges comme les programmeurs sont généralement ignorées directement haha.

Ensuite, notre société fournit des solutions pour le fonctionnement normal de ces appareils. C'est pourquoi j'ai mon résumé de l'escalade des fosses d'aujourd'hui, hahahaha...

La solution que nous proposons est la suivante : un magasin comprendra les équipements suivants : machine à poupées, défi de rouge à lèvres, classement, contrôle central et, bien sûr, nos services d'arrière-plan. Alors tout d'abord, je vais d'abord vous présenter l'architecture de l'ensemble du système et les responsabilités de chaque appareil :

  • Schéma d'architecture système

Obtenez gratuitement des données Android de haut niveau pour rejoindre le groupe : 185873940

  • Contexte des services
  • Le backend de service n'est pas la responsabilité du côté Android, vous n'avez donc pas besoin d'y prêter trop d'attention.Par rapport à l'équipement du magasin, l'une des responsabilités du backend de service est d'envoyer la qualité de la machine à la machine, surveiller l'état de l'appareil, et quelques autres informations.
  • Commande centrale (service local : non connecté au réseau externe) : Cette commande centrale est également un appareil Android, et elle a deux fonctions :
  • Liste de classement : elle est utilisée pour recevoir les informations de la poupée dans le dossier utilisateur envoyé par la machine à poupées, et enfin les afficher sur la liste de classement.
  • Contrôle central de la distribution des ressources : en tant que centre de distribution des ressources, le contrôle central doit distribuer les packages d'installation apk, les images,
    • Machine à griffes : cette pièce contient en fait deux parties, l'une est un appareil Android et l'autre est un appareil matériel.
  • Les appareils Android sont principalement utilisés pour afficher des bannières, traiter les données du serveur (par exemple : score supérieur), se connecter au contrôle central (mise à jour des ressources, retour de données, etc.) et se connecter aux périphériques matériels
  • L'appareil matériel traite principalement les points, les cadeaux, les battements de cur et d'autres informations de l'utilisateur, et transmet ces informations à l'appareil Android pour traitement.
    • Lipstick Challenge : En ce qui concerne le Lipstick Challenge, cet appareil peut être divisé en 3 modules, à savoir, le jeu de couture, le module de programme régulier et le module matériel.
  • Ce jeu sur les trous d'épingle est réalisé avec le moteur d'aigrette, et finalement intégré dans l'APP sous la forme de h5, qui est principalement responsable de la logique principale du jeu et de la rétroaction des données logiques du jeu avec le module principal du programme.
  • Les modules conventionnels comprennent principalement le traitement des données en arrière-plan du service, les modules matériels d'ancrage (sélection de la grille, ouverture de la grille, etc.), les jeux d'ancrage (démarrage des jeux, retour des résultats du jeu, etc.) et la gestion de l'arrière-plan matériel.
    • fusée : Ce programme est un peu spécial, car l'utilisateur ne peut pas le voir. Lorsqu'il quitte l'usine, ce programme est écrit, il est donc responsable du travail suivant :
  • Protégez le programme systemui et le programme de lancement de l'appareil pour empêcher les utilisateurs d'effectuer certaines opérations illégales.
  • Détecter si le disque U est inséré, puis déplacer ou être responsable de la formulation des fichiers (paquet d'installation apk, fichier ipconfig.json, fichier de configuration triple config.json, fichier de ressources h5, etc.)
  • Recevez des émissions, installez ou mettez à jour automatiquement la machine à poupées ou le programme de défi de rouge à lèvres
  • problème à résoudre

    Charger les ressources de manière synchrone

    En ce qui concerne la synchronisation des ressources, tout d'abord, examinons les ressources dont nous avons besoin pour synchroniser.Ces ressources sont : le package d'installation apk, les images et les ressources d'index liées à h5.

    Comment les ressources sont mises à jour

    En ce qui concerne la méthode de mise à jour, il y a en fait un gouffre ici. Au début, la méthode de mise à jour des ressources que nous avons choisie était stupide, et nous avons directement utilisé websocket pour mettre à jour les ressources. Au début, il n'y avait qu'un seul appareil à connecter. Le problème n'est pas Cependant, il a été découvert plus tard que le problème est particulièrement grave lorsque plusieurs appareils sont connectés pour mettre à jour les ressources en même temps, et la connexion est souvent déconnectée, ce qui entraîne l'échec de la mise à jour des ressources. Voici donc la première fosse que j'ai rencontrée. Après avoir trouvé cette fosse, ma méthode de sélection des mises à jour de ressources a été modifiée en : NanoHttpd. NanoHttpd est une bibliothèque open source, implémentée en Java, qui peut créer un serveur Web léger sur les appareils Android. En fait, créer un serveur Web léger sur un appareil Android est la direction que nous devrions choisir dès le début. Pourquoi? Tout d'abord, l'utilisation de NanoHttpd est relativement simple, nous n'avons donc besoin que de quelques lignes de code pour implémenter un serveur web ; deuxièmement, NanoHttpd est relativement stable, ce qui est beaucoup plus stable que lorsque nous utilisons manuellement websocket pour implémenter une distribution de ressources .

    Puis après avoir choisi la manière de mettre à jour la ressource, un autre problème est apparu, à propos de l'adresse IP du serveur. Nous savons tous que lorsqu'un appareil Android est connecté à l'Internet mobile ou au Wi-Fi, une adresse IP lui sera automatiquement attribuée, donc cette adresse IP changera. Notre appareil s'éteindra chaque nuit, puis s'allumera et redémarrera le lendemain. Il se verra attribuer une nouvelle adresse IP, de sorte que l'adresse IP du serveur change constamment, donc ce que nous devons faire ici est de trouver un moyen de corriger l'adresse IP d'un certain appareil. Parlons ensuite de NanoHttpd créant un serveur Web léger et comment résoudre le problème des changements d'adresse IP.

    NanoHttpd implémente un serveur Web

    • Adresse du projet NanoHttpd
    • https://github.com/NanoHttpd/nanohttpd
    • dépendances de niveau
    implémentation 'org.nanohttpd:nanohttpd-webserver:2.3.1'
    • Méthode pour réaliser
    File resourceDir = new File(Environment.getExternalStorageDirectory(), "myRootDir"); SimpleWebServer httpServer = new SimpleWebServer(null, 18103, resourceDir, true, "*"); httpServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
    • Paramètres du constructeur SimpleWebServer
    • hôte : adresse IP du serveur
    • port : numéro de port (plage de valeurs : 1024~65535)
    • wwwroot : répertoire racine où sont placées les ressources statiques
    • silencieux : s'il est en mode silencieux
    • cors :
    • méthode d'entretien
    • Sous le même réseau local, nous entrons ensuite l'adresse dans le navigateur : et nous pouvons accéder aux ressources de notre serveur. Bien sûr, le serveur actuellement implémenté est statique et ne peut traiter que get Les demandes de ressources ne peuvent pas traiter d'autres demandes telles que post, put, etc., qui ne peuvent pas être traitées actuellement. Si vous devez traiter d'autres demandes telles que post et put vous-même, vous pouvez vous rendre à l'adresse d'origine du projet et vous référer à son utilisation pour l'implémenter, ici Pas grand chose à dire.

    Résoudre le problème des changements d'IP

    Dans les appareils Android, l'une de ses adresses IP changera et chaque magasin aura son propre ordinateur de contrôle interne, nous devons donc faire face au problème des changements d'adresse IP. Notre solution comporte les deux étapes suivantes :

  • En fonction de l'adresse Mac du routeur, définissez une adresse IP fixe pour le dispositif de contrôle central du magasin
  • Fournissez un fichier de configuration d'adresse IP pour chaque machine de poupée et appareil de défi de rouge à lèvres.Ce fichier contient les informations d'adresse IP du contrôle central du magasin, qui est placé dans le répertoire spécifié du disque U, mais lorsque l'appareil est inséré, le programme Rocket enregistrera le fichier à partir du fichier. Copiez le fichier de configuration du disque U vers le répertoire spécifié de l'appareil. Chaque fois que l'appareil démarre, il doit d'abord lire le fichier de configuration, puis se connecter au serveur local.
  • Quand la ressource sera-t-elle mise à jour ?

    Concernant la mise à jour des ressources, nous devons d'abord clarifier quelles ressources nous devons mettre à jour et comment nous devons mettre à jour.

    ressources mises à jour

    • Ressource.json
    • paquet apk
    • Afficher le carrousel dans la machine à poupées
    • La ressource h5 de la bannière affichée dans la machine à griffes

    fichier de configuration mis à jour

    • Toutes les données sur nos ressources et les nouvelles sont stockées dans le dossier Resource.json, puis nous obtenons Resource.json du serveur de contrôle central (dans le réseau local) toutes les 5 minutes, puis chaque type de ressource est écrit dans le les données dans Resource.json sont évaluées. Ensuite, l'implémentation et le contenu spécifique écrit dans le fichier Resource.json sont les suivants :
  • Modèle ResList pour les ressources
  • classe publique ResListModel { // ressources h5 de la bannière de la machine à griffes (fichiers tels que index.html) HashMap publique < Chaîne, Chaîne > bannerFiles = new HashMap(); // Le carrousel que toutes les machines à poupées du magasin afficheront // la clé est la valeur de hachage de l'image // la valeur est le chemin relatif de l'image dans le serveur HashMap publique < Chaîne, Chaîne > PublicFiles = new HashMap(); // Carrousel de présentation privé d'une machine à griffes spécifique dans le magasin // la clé est l'identifiant de l'appareil // la valeur est les informations de hachage et de chemin de l'image image (correspondant à PublicFiles) HashMap publique < Chaîne, HashMap < Chaîne, Chaîne > > FichiersPrivés = new HashMap(); // chemin apk mis à jour chaîne publique UpdateApk ; // nom du package apk mis à jour chaîne publique UpdateApkPackageName ; // nom de la version apk mis à jour chaîne publique UpdateApkVersion ; // numéro de version apk mis à jour public int UpdateApkVersionCode ; }
  • Ecrire dans le fichier Resourse.json
  • ResListModel res = new ResListModel(); // ignore le processus d'ajout de données ...; Fichier resourceFile = new File(baseDir, "Resource.json"); RandomAccessFile out = new RandomAccessFile(resourceFile, "rw"); octet json = JsonStream.serialize(res).getBytes("utf-8"); out.setLength(json.length); out.write(json); out.close();
  • Contenu de Resources.json
  • { "FichiersPrivés":{}, "Fichiers Publics": { "1A7D3394A6F10D3668FB29D8CCA1CA8B":"Public/timg.jpg" }, "UpdateApk":null, "UpdateApkPackageName":null, "Mise à jourApkVersion":null, "Mise à jourApkVersionCode":0, "bannerFiles": { "C609D70832710E3DCF0FB88918113B18":"banner/Resource.json", "FC1CF2C83E898357E1AD60CEF87BE6EB":"banner/app.8113390c.js", "27FBF214DF1E66D0307B7F78FEB8266F":"banner/manifest.json", "A192A95BFF57FF326185543A27058DE5":"banner/index.html", "61469B10DBD17FDEEB14C35C730E03C7":"banner/app.8113390c.css" } }

    Mise à jour du fichier de ressource d'image et de bannière de ressource

    • Les méthodes de mise à jour des fichiers de ressources des images et des bannières sont similaires, sauf que les chemins de stockage ne sont pas dans le même répertoire. Ensuite, pour la mise à jour de ces ressources, nous jugeons par la valeur de hachage et le nom de fichier de la ressource technique. La machine à poupée ou le dispositif de défi de rouge à lèvres obtiendra le fichier Resourse.json du contrôle central toutes les 5 minutes, puis retirera le ResListModel, qui a été introduit auparavant et est le fichier de configuration pour enregistrer les mises à jour des ressources ; puis nous retirons la configuration relative à partir de celui-ci, d'abord selon le fichier Le nom détermine si le fichier existe déjà localement. S'il n'existe pas, il est directement ajouté à la liste des mises à jour des ressources. S'il existe, on juge si la valeur de hachage est la même. S'il s'agit du même, il ne sera pas mis à jour. Ajoutez-le simplement à la liste des ressources mises à jour.
    • Organigramme de mise à jour des ressources d'image et de bannière :

    Obtenez gratuitement des données Android de haut niveau pour rejoindre le groupe : 185873940

    • Contrôle central des ressources informatiques de votre valeur de hachage

    essayer {

    // fichier de ressource de bannière

    String fileName = fileFilter.getAbsolutePath().substring(baseDirLength);

    RandomAccessFile randomAccessFile = new RandomAccessFile(fileFilter,"r");

    octet buf = nouvel octet ;

    randomAccessFile.read(buf);

    randomAccessFile.close();

    MessageDigest md5 = MessageDigest.getInstance("md5");

    hachage d'octets = md5.digest(buf);

    Chaîne hashStr = ByteToHex(hash,0,hash.length);

    res.bannerFiles.put(hashStr,fileName);

    } catch (FileNotFoundException e) {

    e.printStackTrace();

    } capture (IOException e) {

    e.printStackTrace();

    }

    // les octets sont convertis en hexadécimal

    public static String ByteToHex(byte bt, int offset, int len) {

    StringBuffer sb = new StringBuffer();

    pour (int je = décalage; je < décalage + longueur; i++) {

    int tmp = bt et 0xff ;

    String tmpStr = Integer.toHexString(tmp);

    si (tmpStr.length() < 2)

    sb.append("0");

    sb.append(tmpStr);

    }

    return sb.toString().toUpperCase();

    }

    • Le dispositif de machine à griffes vérifie les mises à jour (par exemple : fichier de ressources de bannière)
    public statique Observable < booléen > updateBannerRes(ResListBean resListBean) lance IOException, NoSuchAlgorithmException { // Récupère le fichier de la bannière distante HashMap < Fichier, Chaîne > remoteFiles = new HashMap(); pour (HashMap.Entry < Chaîne, Chaîne > entrée : resListBean.bannerFiles.entrySet()) { remoteFiles.put(nouveau fichier(entry.getValue()), entry.getKey()); } FileUtils.GetFilesInDir(bannerDir,localBannerList,null); int baseDirLength = resDir.getAbsolutePath().length()+1 ; // étape1 : supprimez les fichiers locaux (fichiers qui ne sont pas dans la bannière distante) for (Fichier fichier local : liste de bannières locales) { FilechileFile = new File(localFile.getAbsolutePath().substring(baseDirLength)); if (!fichiersdistants.containsKey(chileFile)) { MainActivity.appendAndScrollLog(String.format("Supprimer le fichier de ressources de bannière %s\n", localFile.getAbsolutePath())); localFile.delete(); } } // Télécharger les fichiers qui ne sont pas disponibles localement Liste des tableaux < Observable < Dossier > > taskList = new ArrayList(); pour (Map.Entry < Fichier, Chaîne > fileEntry : remoteFiles.entrySet()) { Fichier file = new File(resDir,fileEntry.getKey().getAbsolutePath()); // étape2 : le même nom de fichier existe dans le local et le distant si (localBannerList. contient (fichier)) { // étape3 : Déterminer s'il s'agit du même fichier en fonction de la valeur de hachage Chaîne hashStr = FileUtils.getFileHashStr(fichier); if (TextUtils.equals(hashStr,fileEntry.getValue())){ MainActivity.appendAndScrollLog(String.format("Conserver le fichier bannière %s\n", file.getAbsolutePath())); taskList.add(Observable.just(file)); Continuez; } } // étape 4 : Télécharger les fichiers qui ne sont pas disponibles localement URL de chaîne = nouvelle URL("http", Config.instance.centralServerAddress, Config.instance.httpPort, nouveau fichier (BuildConfig.APPLICATION_ID, fileEntry.getKey().getAbsolutePath()).getAbsolutePath()).toString(); // étape 5 : Ajouter une liste de téléchargement de fichiers taskList.add(DownLoadUtils.getDownLoadFile(url,fichier)); } retourner Observable.concat(taskList) .toFlowable(BackpressureStrategy.MISSING) .parallèle() .runOn(Planificateurs.io()) .séquentiel() .lister() .observeOn(Schedulers.computation()) .map(nouvelle fonction < Liste < Dossier > , Liste des tableaux < Dossier > > () { @Passer outre public ArrayList < Dossier > appliquer(Liste < Dossier > fichiers) lance une exception { Liste des tableaux < Dossier > liste = nouvelle ArrayList(); pour (Fichier fichier : fichiers) { si (!file.getAbsolutePath().isEmpty()) { liste.add(fichier); } } si (liste.taille() > 0) { if (!Utils.EqualCollection(list, localBannerList)) { Collections.sort(liste); } autre { liste.clear(); } } liste de retour ; } }) .observeOn(AndroidSchedulers.mainThread()) .map(nouvelle fonction < Liste des tableaux < Dossier > , booléen > () { @Passer outre public booléen appliquer(ArrayList < Dossier > liste) lance une exception { si (liste.taille() > 0) { localBannerList = liste ; webViewHasLoad = false ; chargeH5(); } retourner vrai ; } }) .observeOn(Planificateurs.io()) .map(nouvelle fonction < booléen, booléen > () { @Passer outre public Boolean apply(Boolean aBoolean) lance une exception { FileUtils.DelEmptyDir(resDir); retourner vrai ; } }) .toObservable(); }

    problème de mise à jour du programme

    Concernant la mise à jour du programme, c'est beaucoup plus simple que la mise à jour des ressources images.

    • Les étapes de notre mise à jour de la version d'implémentation sont les suivantes :
    • Étape 1 : recherchez le fichier apk qui existe localement (l'apk de l'appareil a un chemin et un nom de fichier spécifiés) et supprimez-le.
    • étape 2 : déterminez si le numéro de version du package d'installation dans le contrôle central est supérieur au numéro de version du programme local, et si c'est le cas, entrez l'étape 3 ; sinon, ignorez-le et aucune mise à niveau du programme n'est requise
    • étape 3 : Téléchargez la dernière version du package d'installation apk
    • étape 4 : Une fois le téléchargement réussi, envoyez une diffusion (action : nom du package ; extra : chemin du fichier apk) au programme de fusée
    • step5 : Le programme de fusée met à jour le programme après avoir reçu la diffusion
    • Organigramme de mise à niveau du programme

    Obtenez gratuitement des données Android de haut niveau pour rejoindre le groupe : 185873940

    • implémentation de code spécifique

    public statique Observable < booléen > updateGame(ResListBean res) lance IOException, InterruptedException {

    Liste des tableaux < Dossier > apkList = new ArrayList();

    FileUtils.GetFilesInDir(resDir, apkList, nouvelle chaîne{

    ".apk",

    });

    // supprimer le package apk existant localement

    pour (fichier fichier : apkList) {

    fichier.delete();

    }

    fais {

    if (res.UpdateApk == null || res.UpdateApkVersion == null) {

    Pause;

    }

    // Déterminez si vous devez mettre à jour

    si (BuildConfig.VERSION_CODE > = res.UpdateApkVersionCode) {

    Pause;

    }

    // URL de l'apk

    URL de chaîne finale = nouvelle URL ("http", Config.instance.centralServerAddress, Config.instance.httpPort, nouveau fichier (BuildConfig.APPLICATION_ID, res.UpdateApk).getAbsolutePath()).toString();

    MainActivity.appendAndScrollLog(String.format("Télécharger le fichier de mise à jour %s\n", url));

    // télécharger le fichier apk

    return DownLoadUtils.getDownLoadFile(url,resDir.getAbsolutePath(),res.UpdateApk)

    .subscribeOn(Planificateurs.io())

    .observeOn(Planificateurs.io())

    .flatMap (nouvelle fonction < Fichier, ObservableSource < Chaîne de caractères > > () {

    @Passer outre

    public ObservableSource < Chaîne de caractères > appliquer (fichier fichier) lance une exception {

    chemin de chaîne = file.getAbsolutePath();

    MainActivity.appendAndScrollLog(String.format("Téléchargement du fichier de mise à jour terminé %s %s\n", chemin, url));

    PackageManager pm = MainActivity.instance.getPackageManager();

    PackageInfo pi = pm.getPackageArchiveInfo(chemin, 0);

    si (pi == nul) {

    MainActivity.appendAndScrollLog(String.format("Impossible d'ouvrir le fichier de mise à niveau %s\n", chemin));

    return Observable.just("");

    }

    MainActivity.appendAndScrollLog(String.format("Comparaison des fichiers de mise à niveau : natif(%s %s)/distant(%s %s)\n", BuildConfig.APPLICATION_ID, BuildConfig.VERSION_NAME, pi.packageName, pi.versionName) );

    si (!BuildConfig.APPLICATION_ID.equals(pi.packageName)

    || BuildConfig.VERSION_CODE > = pi.versionCode) {

    return Observable.just("");

    }

    return Observable.just(path);

    }

    })

    .flatMap (nouvelle fonction < Chaîne, observable < booléen > > () {

    @Passer outre

    public observable < booléen > apply(String updateApk) lève une exception {

    si (!updateApk.isEmpty()) {

    Log.e(TAG, "Attendez que le fichier de mise à jour soit installé après la fin du jeu...");

    MainActivity.appendAndScrollLog("Attendre la fin du jeu pour installer le fichier de mise à jour...\n");

    synchronisé (GamePlay.class) {// Empêche la mise à jour de la version pendant que le jeu est en cours d'exécution

    Log.e(TAG, "Post diffusion");

    Intention intent = new Intent();

    intention.setAction(Config.updateBroadcast);

    intention.putExtra("apk", updateApk);

    MainActivity.instance.sendBroadcast(intention);

    System.exit(0);

    }

    }

    retourner Observable.just(true);

    }

    });

    } tandis que (faux);

    retourner Observable.just(true);

    }

    Téléchargement du fichier de ressources

    Concernant le téléchargement des fichiers ressources, je choisis okdownload. okdownload est un moteur de téléchargement qui prend en charge le multithreading, le multitâche, le téléchargement avec reprise, fiable, flexible, performant et puissant. Pour plus de détails, consultez l'adresse okdownload GitHub

    • manière dépendante
    implémentation 'com.liulishuo.okdownload:okdownload:1.0.5' implémentation 'com.liulishuo.okdownload:okhttp:1.0.5'
    • Exemple simple et pratique

    Téléchargement de fichier unique

    Tâche DownloadTask = new DownloadTask.Builder(url, parentFile) .setFilename (nom de fichier) // l'intervalle minimal en millisecondes pour la progression du rappel .setMinIntervalMillisCallbackProcess(30) // retélécharge même si la tâche a déjà été effectuée dans le passé. .setPassIfAlreadyCompleted(false) .construire(); tâche.enqueue(écouteur); // annuler tâche. annuler(); // exécute la tâche synchronisée tâche.execute(écouteur);

    Téléchargement de plusieurs fichiers

    tâches DownloadTask finales = nouvelle DownloadTask ; tâches = new DownloadTask.Builder("url1", "path", "filename1").build(); tâches = new DownloadTask.Builder("url2", "path", "filename1").build(); DownloadTask.enqueue(tâches, écouteur);
    • Combiné avec Rxjava pour réaliser le téléchargement de fichiers
    classe publique DownLoadUtils { /** * Télécharger des fichiers du contrôle central vers le local * URL @param * @param parentPath chemin du fichier parent enregistré dans le fichier local * @param downloadFileName enregistrer dans le nom de fichier local * @revenir */ public statique Observable < Dossier > getDownLoadFile(String url,String parentPath,String downloadFileName){ // Télécharger les fichiers qui ne sont pas disponibles localement MainActivity.appendAndScrollLog(String.format("Démarrer le téléchargement du fichier de ressources %s\n", url)); tâche DownloadTask finale = new DownloadTask.Builder(url, parentPath, downloadFileName).build(); return Observable.create(new ObservableOnSubscribe < Dossier > () { @Passer outre public void subscribe(final ObservableEmitter < Dossier > émetteur) lance une exception { tâche.enqueue(new DownloadListener2() { @Passer outre public void taskStart (tâche de téléchargement) { } @Passer outre public void taskEnd (tâche de téléchargement, cause de cause de fin, cause réelle d'exception) { if (cause != EndCause.COMPLETED) { MainActivity.appendAndScrollLog(String.format("Impossible de télécharger le fichier de ressources %s %s\n", cause.toString(), task.getUrl())); émetteur.onNext(nouveau fichier("")); émetteur.onComplete(); revenir; } Fichier fichier = tâche.getFile(); MainActivity.appendAndScrollLog(String.format("Téléchargement du fichier de ressources terminé %s\n", file.getAbsolutePath())); émetteur.onNext(fichier); émetteur.onComplete(); } }); } }).recommencez(); } /** * Télécharger des fichiers du contrôle central vers le local * URL @param * @param saveFile enregistrer dans un fichier local * @revenir */ public statique Observable < Dossier > getDownLoadFile(chaîne url, fichier saveFile){ return getDownLoadFile(url, saveFile.getParentFile().getAbsolutePath(),saveFile.getName()); } }

    Bloquer les menus déroulants et les barres de navigation inférieures

    Des appareils comme la machine à poupées et la machine à treillis sont directement orientés vers les utilisateurs hors ligne, nous ne pouvons donc pas montrer tous nos appareils Android à nos utilisateurs, nous devons imposer certaines restrictions sur le comportement de l'utilisateur, comme interdire aux utilisateurs d'utiliser la barre de navigation ou Les menus déroulants quittent le programme en cours, les empêchant d'effectuer certaines opérations dangereuses. Ma solution consiste à définir le programme de fusée actuel comme application de démarrage et de bureau par défaut, et à désactiver le programme de lancement et le programme systemui fournis avec l'appareil Android, afin que notre application de fusée soit lancée au démarrage de l'appareil et interdit avec succès aux utilisateurs de utiliser la barre de navigation et le menu déroulant pour effectuer des opérations illégales.

    • Recherchez le nom de package correspondant du programme de lancement et du programme systemui fourni avec l'appareil Android
    • Nous pouvons utiliser adb shell pm list packages pour connaître la liste des programmes installés sur l'appareil, principalement affichés par nom de package.
    • Trouver le nom du package du programme de lancement , découvrez le nom du package : com.android.launcher3
    LW-PC0920@lw1002022 MINGW64 ~/Desktop $ adb shell pm lister les packages | lanceur grep paquet : com.android.launcher3
    • Trouver le nom du package du programme systemui : Découvrez le nom du package : com.android.systemui
    LW-PC0920@lw1002022 MINGW64 ~/Desktop $ adb shell pm liste les packages | grep systemui paquet : com.android.systemui
    • Interdire l'utilisation du programme de lancement et du programme systemui fourni avec les appareils Android
    • Interdire l'utilisation du programme de lancement
    adb shell pm désactiver com.android.launcher3
    • Désactiver l'utilisation des programmes systemui
    adb shell pm désactiver com.android.systemui
    • L'implémentation du code interdit l'utilisation du programme de lancement et du programme systemui fourni avec l'appareil Android
    public static void enableLauncher (booléen activé) { Liste < Informations sur le paquet > piList = MainActivity.instance.packageManager.getInstalledPackages(0); Liste des tableaux < Chaîne de caractères > packages = new ArrayList(); for (PackageInfo pi : piList) { Nom de la chaîne = pi.packageName ; if (nom.contains("systemui") || nom.contains("lanceur")) { packages.add(nom); } } for (String packageName : packages) { su(String.format("pm %s %s\n", activé ? "enable" : "disable", packageName)); } } /** * Exécutez la commande adb * */ public static int su(String cmd) { essayer { Processus p = Runtime.getRuntime().exec("su"); DataOutputStream os = new DataOutputStream(p.getOutputStream()); os.writeBytes(cmd); os.writeBytes("exit\n"); os.flush(); os.close(); return p.waitFor(); } catch (Exception ex) { retour -1 ; } }

    Mise en uvre de l'IdO

    Concernant la mise en uvre de l'IoT, nous utilisons ici le service "Micro Message Queue for IoT" d'Ali. Concernant le service "Micro Message Queue for IoT", l'explication d'Ali est la suivante :

    Micro Message Queue pour IoT est un sous-produit de Message Queuing (MQ). En réponse aux exigences particulières de transmission de messages des utilisateurs de l'Internet mobile et de l'Internet des objets, Message Queue (MQ) a ouvert la prise en charge complète du protocole MQTT en lançant Micro Message Queue pour l'IoT

    • Protocole MQTT ?
    • Le nom complet de MQTT est : Message Queuing Telemetry Transport, qui est un protocole de messagerie instantanée léger basé sur le modèle de publication-abonnement. La conception du protocole est ouverte, le protocole est simple et la plate-forme est riche en support.Il peut connecter presque tous les éléments Internet au monde extérieur, il présente donc de nombreux avantages dans le domaine de l'Internet mobile et de l'Internet des objets.
    • Fonctionnalités de MQTT
    • Utilisez le mode de message de publication/abonnement (Pub/Sub) pour fournir une distribution de messages un à plusieurs et libérer le couplage entre les applications ;
    • Transmission de message masquée au contenu de la charge utile ;
    • Utilisez TCP/IP pour fournir une connectivité réseau de base ;
    • Il existe trois niveaux de services de messagerie ;
    • Petits transferts avec peu de surcharge (la longueur de l'en-tête est fixée à 2 octets) et la commutation de protocole est minimisée pour réduire le trafic réseau.
    • Explication des termes clés Nom Explication Parent Topic Le protocole MQTT est basé sur le modèle Pub/Sub, donc tout message appartient à un Topic. Selon le protocole MQTT, il existe plusieurs niveaux de rubriques et la rubrique de premier niveau est définie comme la rubrique parent (Sujet parent). Avant d'utiliser MQTT, la rubrique parent doit être créée dans la console MQ. Sous-rubrique La rubrique de second niveau de MQTT et même la rubrique de troisième niveau sont des sous-classes sous la rubrique parent. Lors de l'utilisation, définissez-le directement dans le code sans le créer. Il convient de noter que MQTT limite la longueur totale de la rubrique parent et de la sous-rubrique à 64 caractères. Le dépassement de la limite de longueur entraînera une exception client. ID client L'ID client de MQTT est l'identifiant unique de chaque client, qui doit être unique au monde. L'utilisation du même ID client pour se connecter au service MQTT sera rejetée.

    Implémentation de l'iot dans Android

    Le processus de mise en uvre de l'affichage de la connexion iot est le suivant : d'abord, nous générons le triplet de l'appareil à partir de l'arrière-plan de gestion par lots, et le format du nom de fichier est deviceName.json (Par exemple : 00001.json), qui est la triple information sur chaque appareil ; puis nous insérons le disque U contenant le triple fichier dans l'appareil Android (machine à griffes ou défi de rouge à lèvres) ; le programme de fusée détectera automatiquement l'Insérez le U disque et coupez le fichier dans le répertoire spécifié de l'appareil Android ; l'appareil Android peut alors lire les informations du triplet dans le fichier spécifié ; enfin, utilisez ce triplet pour connecter mqtt.

    • ajouter des dépendances
    implémentation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.0'
    • À propos des triplés
    • Trois choses dont il faut se préoccuper dans les appareils Android, les trois éléments nécessaires dans le protocole mqtt pour identifier un appareil, s'il y a les mêmes triplets, il doit y avoir une erreur, entraînant une déconnexion et une reconnexion fréquentes de mqtt. Le triplet est principalement généré dans l'arrière-plan de gestion d'Ali, et il ne doit être utilisé que du côté de l'appareil Android.
    • L'attribut est utilisé productKey correspond à la clé du programme, similaire à appid deviceName correspond à l'ID client ci-dessus, utilisé pour identifier de manière unique le deviceSecret d'un appareil Android Utilisez l'algorithme HmacSHA1 pour calculer la chaîne de signature et définissez la chaîne de signature sur le paramètre Mot de passe pour l'authentification
    • sujet sur les abonnements
    • Les sujets sont définis dans la gestion d'arrière-plan d'Alibaba Cloud, et nous envoyons et recevons des messages via ces sujets.
    • Code pour implémenter la connexion iot
    • Couper le triple fichier de configuration
    /** * Couper le fichier de configuration (triple) * @param packageName */ public static void moveConfig(String packageName) { Fichier usbConfigDir = nouveau fichier (UsbStorage.usbPath, Config.wejoyConfigDirInUsb); Fichier extProjectDir = nouveau fichier (Environment.getExternalStorageDirectory(), Config.resourceDirName); Fichier extConfigFile = nouveau fichier (extProjectDir, Config.wejoyConfigFileInSdcard); if (!usbConfigDir.exists() || extConfigFile.exists()) { revenir; } extProjectDir.mkdirs(); Fichier configFiles = usbConfigDir.listFiles(); si (configFiles.length > 0) { Arrays.sort(configFiles); moveFile(configFiles, extConfigFile); } } public static void moveFile(Fichier src, Fichier dst) { su(String.format("mv -f %s %s\n", src.getAbsolutePath(), dst.getAbsolutePath())); }
    • Lire les informations du fichier de configuration du chemin spécifié (triple)

    public static File configFile = new File(new File(Environment.getExternalStorageDirectory(), "WejoyRes"), "Config.json");

    static void read() lance IOException {

    si (configFile.exists()) {

    RandomAccessFile in = new RandomAccessFile(configFile, "r");

    octet buf = nouvel octet ;

    in.read(buf);

    joindre();

    instance = JsonIterator.deserialize(new String(buf, "utf-8"), Config.class);

    } autre {

    instance = nouvelle configuration();

    }

    mqttRequestTopic = String.format("/sys/%s/%s/rrpc/request/", instance.productKey, instance.deviceName);

    mqttResponseTopic = String.format("/sys/%s/%s/rrpc/response/", instance.productKey, instance.deviceName);

    mqttPublishTopic = String.format("/%s/%s/update", instance.productKey, instance.deviceName);

    }

    • connecter mqtt
    statique void init() { instance = nouvel IoT(); DeviceInfo deviceInfo = new DeviceInfo(); deviceInfo.productKey = Config.instance.productKey ; deviceInfo.deviceName = Config.instance.deviceName ; deviceInfo.deviceSecret = Config.instance.deviceSecret ; paramètres LinkKitInitParams finaux = new LinkKitInitParams(); params.deviceInfo = deviceInfo ; params.connectConfig = new IoTApiClientConfig(); LinkKit.getInstance().registerOnPushListener(instance); initDisposable = Observable.interval(0, Config.instance.mqttConnectIntervalSeconds, TimeUnit.SECONDS) .subscribeOn(Planificateurs.io()) .observeOn(Planificateurs.io()) .map(nouvelle fonction < Long, booléen > () { @Passer outre public Boolean apply(Long aLong) lance une exception { si (!initialisé) { LinkKit.getInstance().init(MainActivity.instance, params, instance); } retour initialisé ; } }) .subscribe(nouveau Consommateur < booléen > () { @Passer outre public void accept(Boolean aBoolean) lance une exception { si (un booléen) { initDisposable.dispose(); } } }); }
    • Envoyer un message:
    • Lors de l'envoi d'un message, nous devons spécifier le sujet, sinon le serveur ne peut pas recevoir notre message.
    publication vide statique (chaîne json) { Log.e(TAG, "publier : "+json ); MqttPublishRequest res = new MqttPublishRequest(); res.isRPC = faux ; res.topic = Config.mqttPublishTopic ; res.payloadObj = json; LinkKit.getInstance().publish(res, new IConnectSendListener() { @Passer outre public void onResponse(ARequest aRequest, AReponse aResponse) { } @Passer outre public void onFailure(ARequest aRequest, AError aError) { } }); }
    • Recevoir un message : Lors de la réception d'un message, nous devons également déterminer de quel sujet il provient. À l'exception du sujet que nous avons spécifié, nous ne traitons pas d'autres sujets ; lorsque nous recevons un message du serveur, nous déterminons d'abord le type de message, puis réagissons différemment selon le type correspondant. Par exemple, lorsque nous recevons une instruction pour attribuer un score supérieur à la machine à poupée à partir de la demande d'arrière-plan, nous envoyons une commande de score supérieur au module matériel de l'appareil, attendons que l'appareil réponde et envoyons un message de réponse à l'arrière-plan. Ce message de réponse doit être terminé dans le délai spécifié, sinon il est considéré comme expiré.
    @Passer outre public void onNotify(String s, final String topic, final AMessage aMessage) { if (!topic.startsWith(Config.mqttRequestTopic)) { revenir; } Observable.create(new ObservableOnSubscribe < MqttMessage > () { @Passer outre public void subscribe(ObservableEmitter < MqttMessage > émetteur) lance une exception { MqttMessage msg = JsonIterator.deserialize(new String((byte) aMessage.data, "utf-8"), MqttMessage.class); si (msg == null) { revenir; } émetteur.onNext(msg); émetteur.onComplete(); } }) .subscribeOn(Planificateurs.io()) .observeOn(Planificateurs.io()) .flatMap (nouvelle fonction < MqttMessage, ObservableSource < MqttMessage > > () { @Passer outre public ObservableSource < MqttMessage > appliquer (MqttMessage msg) lance une exception { Log.e(TAG, "Clé message reçu :"+msg.key+" msg:"+msg.body.m); switch(msg.clé) { cas "h": {// SetHeartBeatDownstream setHeartBeatDownstream = msg.body.m.as(SetHeartBeatDownstream.class); // Communiquer avec l'appareil et attendre la réponse de l'appareil return Device.setHeartBeat(setHeartBeatDownstream); } cas "b": {// AddCoinsDownstream addCoinsDownstream = msg.body.m.as(AddCoinsDownstream.class); // Communiquer avec l'appareil et attendre la réponse de l'appareil return Device.addCoins(addCoinsDownstream); } cas "g": {// // Communiquer avec l'appareil et attendre la réponse de l'appareil return Device.getParam(); } cas "s": {// SetParamDownstream setParamDownstream = msg.body.m.as(SetParamDownstream.class); // Communiquer avec l'appareil et attendre la réponse de l'appareil return Device.setParam(setParamDownstream); } } return Observable.jamais(); } }) .observeOn(Planificateurs.io()) .map(nouvelle fonction < MqttMessage, booléen > () { @Passer outre public Boolean apply (MqttMessage msg) lance une exception { MqttPublishRequest res = new MqttPublishRequest(); res.isRPC = faux ; res.topic = topic.replace("demande", "réponse"); //res.msgId = sujet.split("/"); res.payloadObj = JsonStream.serialize(msg); LinkKit.getInstance().publish(res, new IConnectSendListener() { @Passer outre public void onResponse(ARequest aRequest, AReponse aResponse) { } @Passer outre public void onFailure(ARequest aRequest, AError aError) { } }); retourner vrai ; } }) .s'abonner(); }

    Communication Android et matérielle

    Dans les deux appareils de la machine à poupée et du défi du rouge à lèvres, nous avons tous besoin de communiquer avec l'appareil, tels que : pièce de monnaie de la machine à poupée, retour d'information sur les cadeaux de la machine à poupée, pression sur la grille de rouge à lèvres sélectionnée, etc. Ils doivent tous communiquer avec les modules matériels. .communication. En termes de sélection de trame pour la communication série, nous choisissons principalement l'api android-serialport-api de Google à implémenter. Adresse d'origine du projet

    • manière dépendante
  • Ajouter à la racine build.gradle
  • tous les projets { dépôts { ... maven {url 'https://jitpack.io'} } }
  • Les sous-modules ajoutent des dépendances
  • dépendances { implémentation 'com.github.licheedev.Android-SerialPort-API:serialport:1.0.1' }
    • Modifier le chemin su
    // Le chemin par défaut de su est "/system/bin/su" // peut être modifié par cette méthode SerialPort.setSuPath("/system/xbin/su");
    • Méthode de connexion

    Lors de la connexion au port série, vous devez spécifier le numéro de port série et le débit en bauds, puis traiter régulièrement les instructions envoyées par la machine.

    static void init() lance IOException { SerialPort.setSuPath("/system/xbin/su"); // Définir le numéro de port série et le débit en bauds serialPort = nouveau SerialPort(Config.serialPort, Config.baudrate); // reçoit le flux de commandes inputStream = serialPort.getInputStream(); // envoie le flux de commandes outputStream = serialPort.getOutputStream(); // Traiter les informations machine toutes les 100 ms Observable.interval(100, TimeUnit.MILLISECONDS) .observeOn(serialScheduler) .subscribe(nouveau Consommateur < Long > () { @Passer outre public void accept(Long aLong) lance une exception { // traite la commande envoyée par la machine handleRecv(); } }); }
    • Envoyer des instructions à la machine

    Lors de l'envoi d'instructions à la machine, il est implémenté en conjonction avec Rxjava. De plus, l'envoi d'instructions à la machine doit avoir un format prescrit (protocole de communication formulé en interne). Les données que nous envoyons et recevons sont un tableau d'octets, notre format doit donc être strictement conforme au protocole que nous avons formulé, comme suit Voici un exemple simple d'une machine à poupée à pièces :

    ObservableSource statique < MqttMessage > addCoins(final AddCoinsDownstream msg) {

    return Observable.create(new ObservableOnSubscribe < MqttMessage > () {

    @Passer outre

    public void subscribe(ObservableEmitter < MqttMessage > émetteur) lance une exception {

    currentUser = msg.u ;

    currentHeadUrl = msg.h;

    currentNickname = msg.nk;

    octet buf = nouvel octet{0x11, addCoinsCmd, msg.num, msg.c, 0, 0x00, 0x00} ;

    octet ret = signe(buf);

    essayer {

    outputStream.write(ret);

    } capture (IOException e) {

    e.printStackTrace();

    }

    en attenteCmd = addCoinsCmd ;

    en attendantEmitter = émetteur ;

    }

    })

    .subscribeOn(serialScheduler);

    }

    • recevoir les instructions de la machine

    La partie de réception des messages de la machine est effectuée toutes les 100 ms. Lors du traitement des instructions de la machine, il est nécessaire de filtrer d'abord les octets invalides, puis de traiter les messages selon le protocole que nous avons établi pour déterminer s'il s'agit de la machine à poupée ou du jeu. résultat, etc., et enfin effectuer un contrôle CRC16 sur les données renvoyées par la machine.

    statique vide handleRecv() { essayer { pour (; ; ) { int len = inputStream.available(); si (len < = 0) { Pause; } len = inputStream.read(buf, bufReadOffset, buf.length - bufReadOffset); //Log.d("serialPort", String.format("read: %s", byteToHex(buf, bufReadOffset, len))); bufReadOffset += len; pour (; ; ) { si (bufParseEnd == -1) { for (; bufParseStart < bufReadOffset; bufParseStart++) { si (buf == (octet) 0xAA) { bufParseEnd = bufParseStart + 1; Pause; } } } si (bufParseEnd != -1) { pour (; bufParseFin < bufReadOffset; bufParseEnd++) { si (buf == (octet) 0xAA) { bufParseStart = bufParseEnd; bufParseEnd += 1; Continuez; } si (buf == (octet) 0xDD) { si (bufParseEnd - bufParseStart > = 5) { bufParseEnd += 1; taille en octets = buf ; indice d'octet = buf ; octet cmd = buf; contrôle d'octet = (octet) (taille ^ index ^ cmd); pour (int je = bufParseStart + 4; je < bufParseEnd - 2; i++) { vérifier ^= buf ; } si (vérifier == buf) { //Log.d("serialPort", String.format("protocole : %s, taille : %d, index : %d, cmd : %d, vérification : %d, données : %s", byteToHex(buf, bufParseStart, bufParseEnd - bufParseStart), taille, index, cmd, vérification, byteToHex(buf, bufParseStart + 4, taille - 3))); commutateur (cmd) { // battement de coeur cas heartBeatCmd : { } Pause; // meilleur score cas addCoinsCmd : { } Pause; // résultat du jeu cas gameResultCmd : { cadeau booléen = buf != 0; IoT.sendGameResult(cadeau); si (cadeau) { // Envoi des informations utilisateur au contrôle central pour l'affichage du classement WSSender.getInstance().sendUserInfo(currentUser, currentHeadUrl, currentNickname); } } Pause; défaut: Pause; } } } bufParseStart = bufParseEnd; bufParseEnd = -1; Pause; } } } si (bufParseStart > = bufReadOffset || bufParseEnd > = bufReadOffset) { Pause; } } if (bufReadOffset == buf.length) { System.arraycopy(buf, bufParseStart, buf, 0, bufReadOffset - bufParseStart); si (bufParseEnd != -1) { bufParseEnd -= bufParseStart; bufReadOffset = bufParseEnd; } autre { bufReadOffset = 0 ; } bufParseStart = 0; } } } capture (IOException e) { e.printStackTrace(); } }

    communication par socket Web

    Dans le mode de communication entre la commande centrale et la machine à poupées, nous choisissons websocket. Le terminal de contrôle central est le serveur, puis la machine à griffes est le client.

    serveur

    • Mise en uvre du serveur : à l'heure actuelle, la mise en uvre du serveur consiste uniquement à recevoir les données en retour de la machine à poupées, il n'y a donc pas d'opération compliquée.
    la classe WSServer étend WebSocketServer { activité principale privée activité principale ; public void setMainActivity(MainActivity mainActivity) { this.mainActivity = mainActivity ; } WSServer (adresse InetSocketAddress) { super(adresse); } @Passer outre public void onOpen(WebSocket conn, ClientHandshake handshake) { mainActivity.appendAndScrollLog("Client :" + conn.getRemoteSocketAddress() + "Connecté\n"); } @Passer outre public void onClose(WebSocket conn, int code, String Reason, boolean remote) { mainActivity.appendAndScrollLog("Client : " + conn.getRemoteSocketAddress() + " déconnecté\n"); } @Passer outre public void onMessage (connexion WebSocket, message de chaîne final) { Observable.create(new ObservableOnSubscribe < SocketMessage > () { @Passer outre public void subscribe(ObservableEmitter < SocketMessage > émetteur) lance une exception { dernier SocketMessage socketMessage = JsonIterator.deserialize(message, SocketMessage.class); émetteur.onNext(socketMessage); émetteur.onComplete(); } }) .subscribeOn(Schedulers.newThread()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(nouveau Consommateur < SocketMessage > () { @Passer outre public void accept(SocketMessage socketMessage) lance une exception { si (socketMessage.getCode() == SocketMessage.TYPE_USER) { // clip sur la poupée } sinon si (socketMessage.getCode() == SocketMessage.TYPE_SAY_HELLO) { // connecter le message d'accueil } } }); } @Passer outre public void onError(WebSocket conn, Exception ex) { } @Passer outre public void onStart() { } }
    • Simple à utiliser
    appendAndScrollLog("Initialiser le service WebSocket...\n"); WSServer wsServer = nouveau WSServer(18104); wsServer.setMainActivity(MainActivity.this); wsServer.setConnectionLostTimeout(5); wsServer.setReuseAddr(true); wsServer.start(); appendAndScrollLog("Initialisation du service WebSocket terminée\n");

    client

    Côté client, les caractères à effectuer incluent actuellement la déconnexion, la reconnexion et la transmission de données. Lors de la déconnexion et de la reconnexion, cela doit être fait dans un nouveau sous-thread, sinon l'erreur suivante sera signalée :

    Vous ne pouvez pas initialiser une reconnexion à partir du thread websocket. Utilisez la reconnexion dans un autre thread pour assurer un nettoyage réussi

    Par conséquent, chaque fois que nous nous déconnectons et redémarrons, nous devons le faire dans un nouveau thread enfant. De plus, lors de l'envoi de données, si le socket n'est pas connecté, l'envoi de données signalera une exception, donc lorsque nous aurons des données à envoyer, si le socket n'est pas connecté, mettez-le en cache localement et attendez que le socket soit connecté. téléchargement, les données bloquées sont envoyées en une seule fois.

    • Paramétrage des dépendances
    implémentation 'org.java-websocket:java-WebSocket:1.3.9'
    • WSClient.java
    la classe WSClient étend WebSocketClient { chaîne finale statique privée TAG = "WSClient" ; instance WSClient statique privée ; URI statique privé surUri ; récepteur WS privé mWSReceiver ; privé jetable mReconnectDisposable ; privé ConnectCallback mConnectCallback ; /** * étape 1: besoin d'appeler d'abord, définissez l'url * @param uri */ public static void setUri(URI uri){ sUri = uri ; } /** * étape 1: * Besoin d'appeler d'abord, définissez l'url du serveur * @param adresseip * port @param */ public static void setUri(String ipAddress,int port){ essayer { sUri = new URI(String.format("ws://%s:%d", ipAddress, port)); } catch (URISyntaxException e) { e.printStackTrace(); } } public statique WSClient getInstance(){ si (instance == null) { synchronisé (WSClient.class){ si (instance == null) { instance = new WSClient(sUri); } } } instance de retour ; } /** * étape 2 : connectez le websocket */ public void onConnect(){ setConnectionLostTimeout(Config.instance.webSocketTimeoutSeconds); setReuseAddr(true); relier(); } client WS privé (serveur URI) { super(serveur); // Initialise l'expéditeur du message WSSender.getInstance().setWSClient(this); // Initialise le destinataire du message mWSReceiver = nouveau WSReceiver(); mWSReceiver.setWSClient(this); mWSReceiver.setWSSender(WSSender.getInstance()); } @Passer outre public void onOpen(ServerHandshake handshakedata) { Log.d(TAG, "onOpen : "); MainActivity.appendAndScrollLog("websocket connecté\n"); Observable.just("") .subscribeOn(AndroidSchedulers.mainThread()) .subscribe(nouveau Consommateur < Objet > () { @Passer outre public void accept(Object o) lance une exception { si (mConnectCallback != null) { mConnectCallback.onWebsocketConnected(); } } }); // efface tous les messages bloqués WSSender.getInstance().clearAllMessage(); } @Passer outre public void onMessage(chaîne message) { Log.d(TAG, "onMessage : "); mWSReceiver.handlerMessage(message); } @Passer outre public void onClose (code int, raison de la chaîne, booléen distant) { Log.d(TAG, "onClose : "); MainActivity.appendAndScrollLog(String.format("websocket a été déconnecté, raison de la déconnexion : %s\n",raison)); Observable.just("") .subscribeOn(AndroidSchedulers.mainThread()) .subscribe(nouveau Consommateur < Objet > () { @Passer outre public void accept(Object o) lance une exception { si (mConnectCallback != null) { mConnectCallback.onWebsocketClosed(); } } }); onReconnect(); } @Passer outre public void onError(Exception ex) { si (ex != nul) { Log.d(TAG, "onError : "+ex.getMessage()); MainActivity.appendAndScrollLog(String.format("Une erreur s'est produite dans le websocket, la raison de l'erreur : %s\n",ex.getMessage())); } onReconnect(); } public void onReconnect() { si (mReconnectDisposable != null !mReconnectDisposable.isDisposed()){ revenir; } mReconnectDisposable = Observable.timer(1, TimeUnit.SECONDS) .subscribeOn(Planificateurs.io()) .subscribe(nouveau Consommateur < Long > () { @Passer outre public void accept(Long aLong) lance une exception { Log.d(TAG, "websocket reconnect"); WSClient.this.reconnect(); mReconnectDisposable.dispose(); } }); } public void setConnectCallback(ConnectCallback mConnectCallback) { this.mConnectCallback = mConnectCallback ; } interface publique ConnectCallback{ void onWebsocketConnected(); void onWebsocketClosed(); } }
    • WSSender.java

    /**

    * Créé par runla le 2018/10/26.

    * Description du fichier : expéditeur du message pour Websocket

    */

    classe publique WSSender {

    chaîne finale statique privée TAG = "WSSender" ;

    public statique final int MAX_MESSAGE_COUNT = 128 ;

    instance WSSender statique privée ;

    mWSClientManager privé ;

    // file d'attente de messages

    liste liée privée < Chaîne de caractères > mMessageList = nouvelle liste liée < > ();

    expéditeur WSS privé() {

    }

    public statique WSSender getInstance() {

    si (instance == null) {

    synchronisé (WSSender.class) {

    si (instance == null) {

    instance = new WSSender();

    }

    }

    }

    instance de retour ;

    }

    public void setWSClient(WSClient wsClientManager) {

    this.mWSClientManager = wsClientManager ;

    }

    /**

    * Envoyer tous les messages bloqués

    */

    public void clearAllMessage() {

    si (mWSClientManager == null) {

    revenir;

    }

    tandis que (mMessageList.size() > 0

    mMessageList.getFirst() != null) {

    Log.d(TAG, "sendMessage : " + mMessageList.size());

    mWSClientManager.send(mMessageList.getFirst());

    mMessageList.removeFirst();

    }

    }

    /**

    * Envoyez un message, si le message ne peut pas être envoyé, attendez que la connexion réussisse et essayez de l'envoyer à nouveau

    *

    * @param msg

    * @revenir

    */

    public booléen sendMessage(String msg) {

    si (mWSClientManager == null) {

    throw new NullPointerException("le client websocket est nul");

    }

    si (TextUtils.isEmpty(msg)) {

    retourner faux ;

    }

    // Ajoute les données à envoyer à la fin de la file d'attente

    mMessageList.addLast(msg);

    tandis que (mMessageList.size() > 0

    mMessageList.getFirst() != null) {

    Log.d(TAG, "sendMessage : " + mMessageList.size());

    si (!mWSClientManager.isOpen()) {

    // essaie de se reconnecter

    mWSClientManager.onReconnect();

    Pause;

    } autre {

    mWSClientManager.send(mMessageList.getFirst());

    mMessageList.removeFirst();

    }

    }

    // Si la file d'attente de messages dépasse la capacité maximale que nous avons définie, supprimez le premier message ajouté

    si (mMessageList.size() > = MAX_MESSAGE_COUNT) {

    mMessageList.removeFirst();

    }

    retourner faux ;

    }

    }

    • WSReceiver.java

    /**

    * Créé par runla le 2018/10/26.

    * Description du fichier : Récepteur de message Websocket

    */

    public class WSReceiver {

    mWSClientManager privé ;

    WSSender privé mWSSender ;

    privé OnMessageCallback onMessageCallback ;

    public WSReceiver() {

    }

    public void setWSClient(WSClient mWSClientManager) {

    this.mWSClientManager = mWSClientManager ;

    }

    public void setWSSender(WSSender mWSSender) {

    this.mWSSender = mWSSender ;

    }

    /**

    * Gérer les messages entrants

    * Message @param

    */

    public void handlerMessage(chaîne message){

    si (onMessageCallback != null){

    onMessageCallback.onHandlerMessage(message);

    }

    }

    public void setOnMessageCallback(OnMessageCallback onMessageCallback) {

    this.onMessageCallback = onMessageCallback ;

    }

    interface publique OnMessageCallback{

    void onHandlerMessage (message de chaîne);

    }

    }

    • connecter un appel
    appendAndScrollLog("Initialiser le client WebSocket...\n"); WSClient.setUri(Config.instance.centralServerAddress, Config.instance.webSocketPort); WSClient.getInstance().onConnect(); WSClient.getInstance().setConnectCallback(MainActivity.this); appendAndScrollLog("Initialisation du client WebSocket terminée\n");
    • envoi de données
    // efface tous les messages bloqués WSSender.getInstance().clearAllMessage(); // Envoyer un message WSSender.getInstance().sendMessage(msg);

    stockage de base de données

    Dans le terminal de contrôle central, nous devons afficher la version de classement, qui est utilisée pour afficher le classement des poupées dans les poupées des utilisateurs dans les clips ce mois-ci et cette semaine, nous devons donc enregistrer le nombre de poupées dans le les clips de l'utilisateur et d'autres informations personnelles sur le terminal de contrôle central.Information, GreenDAO est un framework ORM léger et rapide orienté Android open source, qui mappe les objets Java aux bases de données SQLite.Lorsque nous exploitons la base de données, nous n'avons plus besoin d'écrire du SQL complexe En termes de performances, GreenDAO a implémenté une solution hautement optimisée, avec une surcharge de mémoire minimale, de petites dépendances et une prise en charge du chiffrement de la base de données. Concernant l'utilisation de GreenDAO, je ne le ferai pas ici, pour une utilisation spécifique, merci de vous référer au site officiel GreenDAO.

    écrire à la fin

    En ce qui concerne la construction de l'ensemble du système, j'ai rencontré de nombreux pièges. Voici quelques-unes des solutions que j'ai fournies pour ce projet. Il est impossible d'écrire toutes les solutions actuelles. Ce projet a actuellement des magasins à Xi'an, Chengdu et ailleurs Cliquez, selon les commentaires, le profit est énorme, mais la période de bonus de ce type de projet ne sera pas trop longue, on estime qu'elle est d'environ 2 à 3 ans.

    Ici, j'ai mis du matériel d'apprentissage sur Android à la fin, dans l'espoir d'aider tout le monde.

    1. Essentiels Avancés Avancés

    2. Réglage complet des performances de l'application Android

    3. Développement mixte de l'applet WeChat et Hybird

    Vous voulez obtenir des informations Android plus avancées ? Vous souhaitez améliorer votre technique ? Suivez + message privé pour répondre "Données Android" gratuitement !

    framework de développement rapide Android, la bibliothèque de la fondation, la bibliothèque de styles, composants, intégration des composants
    Précédent
    Après Wanda, qui est le roi du prochain Cinémas
    Prochain
    Module de jeu de contrôle: seul survivant, le mélange de type sol et correspond Zagu
    de promotion estivale, 50% de réduction de Nintendo sur le solde du jeu 6
    Elon Musk a dit Tesla sortira « inattendu » de nouveaux produits, à la fin est quoi?
    Jouez en mode contrôle: jusqu'à la couleur, la machine d'arme d'assaut Graz
    arme de poulet, écran 120Hz avec GTX 10606GB carte graphique, ASUS Flying Fortress évaluation de la 5ème génération
    Module de jeu de commande: Jim une puissance plus élevée au changement, vraiment cool
    Honda a lancé la première voiture électrique véhicules impression 3D au Japon
    Ces concepteurs « une jupe célèbre » la science et la technologie noire
    contrôleur de jeu en mode: reload avec haut G-SYSTEM FA78-1
    Les marques veulent être célèbre? Puis une querelle avec Trump
    Est-ce un bug ou le bien-être? « Titanfall 2 » essai gratuit dans l'origine peut être 10.000 heures
    Nuit « rien », exactement comment la station de base pseudo-mis à sac votre porte-monnaie?