Skip to content

5. TP 2 - Controlar Arduinos com um tablet Android

Agora vamos aprender a controlar uma placa Arduino com um tablet. O exemplo a ser seguido é o do projeto [client-android-skel] do curso (ver parágrafo 2).

5.1. Arquitetura do projeto

O projeto como um todo terá a seguinte arquitetura:

  • o bloco [1], servidor web / jSON e os Arduinos serão fornecidos a vocês;
  • vocês deverão montar o bloco [2] e programar o tablet Android para se comunicar com o servidor web / jSON.

5.2. O material

Vocês têm à disposição os seguintes componentes:

  • um Arduino com uma extensão Ethernet, um LED e um sensor de temperatura;
  • um miniHub para ser compartilhado com outro aluno;
  • um cabo USB para alimentar o Arduino;
  • dois cabos de rede para conectar o Arduino e o PC a uma mesma rede privada;
  • um tablet Android;

5.2.1. O Arduino

Veja como proceder para conectar os diferentes componentes entre si:

  • retire o cabo de rede do seu PC;
  • conecte seu PC ao Arduino por meio de um cabo de rede;
  • o Arduino que você terá já estará programado. Seu endereço IP será [192.168.2.2]. Para que seu PC reconheça o Arduino, é necessário atribuir a ele o endereço IP na rede [192.168.2]. Os Arduinos foram programados para se comunicarem com um PC com o endereço IP [192.168.2.1]. Veja como proceder:

Acesse o [Panneau de configuration\Réseau et Internet\Centre Réseau et partage]:

 
  • em [1], clique no link [réseau local];
  • em [2], clique no botão [Propriétés] da rede local;
  • em [3], clique nas propriedades [IPv4] do mapa [réseau local];
  • em [4], atribua a este mapa o endereço IP [192.168.2.1] e a máscara de sub-rede [255.255.255.0];
  • em [5], clique em [OK] quantas vezes forem necessárias para sair do assistente.

5.2.2. O tablet

  • usando sua senha de Wi-Fi, conecte seu computador à rede Wi-Fi que lhe for indicada. Faça o mesmo com seu tablet;
  • Verifique o endereço Wi-Fi do seu IP digitando [ipconfig] em uma janela do DOS. Você encontrará um endereço do tipo [192.168.x.y];

dos>ipconfig

Configuration IP de Windows

Carte réseau sans fil Wi-Fi :

   Suffixe DNS propre à la connexion. . . :
   Adresse IPv6 de liaison locale. . . . .: fe80::39aa:47f6:7537:f8e1%2
   Adresse IPv4. . . . . . . . . . . . . .: 192.168.1.25
   Masque de sous-réseau. . . . . . . . . : 255.255.255.0
   Passerelle par défaut. . . . . . . . . : 192.168.1.1
  • Verifique o endereço Wi-Fi do seu tablet (IP). Pergunte ao seu orientador como fazer isso, caso não saiba. Você encontrará um endereço do tipo [192.168.x.z];
  • desative o firewall do seu PC, caso ele esteja ativo [Panneau de configuration\Système et sécurité\Pare-feu Windows];
  • em uma janela do DOS, verifique se o PC e o tablet conseguem se comunicar digitando o comando [ping 192.168.x.z], onde [192.168.x.z] é o endereço IP do seu tablet. O tablet deve então responder:
dos>ping 192.168.1.26

Envoi d'une requête 'Ping'  192.168.1.26 avec 32 octets de données :
Réponse de 192.168.1.26 : octets=32 temps=102 ms TTL=64
Réponse de 192.168.1.26 : octets=32 temps=134 ms TTL=64
Réponse de 192.168.1.26 : octets=32 temps=168 ms TTL=64
Réponse de 192.168.1.26 : octets=32 temps=208 ms TTL=64

Statistiques Ping pour 192.168.1.26:
    Paquets : envoyés = 4, reçus = 4, perdus = 0 (perte 0%),
Durée approximative des boucles en millisecondes :
    Minimum = 102ms, Maximum = 208ms, Moyenne = 153ms

A configuração de rede do seu sistema já está pronta.

5.2.3. O emulador [Genymotion]

O emulador [Genymotion] (ver parágrafo 6.9) é uma excelente alternativa ao tablet. Ele é praticamente tão rápido quanto e não requer rede Wi-Fi. Recomenda-se utilizar esse método. Você poderá usar o tablet para a verificação final do seu aplicativo.

5.3. Programação dos Arduinos

Aqui, vamos nos concentrar na escrita do código C dos Arduinos:

Leia também

  • instalação do ambiente de desenvolvimento Arduino (ver parágrafo 6.1);
  • uso das bibliotecas jSON (Anexos, parágrafo 6.6);
  • no Arduino, testar o exemplo de um servidor IDE (por exemplo, o servidor web) e o de um cliente TCP (por exemplo, o cliente Telnet);
  • os anexos sobre o ambiente de programação dos Arduinos no parágrafo 6.1.

Um Arduino é um conjunto de pinos conectados a um hardware. Esses pinos são entradas ou saídas. Seu valor é binário ou analógico. Para controlar o Arduino, há duas operações básicas:

  • gravar um valor binário/analógico em um pino identificado por seu número;
  • ler um valor binário/analógico em um pino designado por seu número;

A essas duas operações básicas, acrescentaremos uma terceira:

  • fazer um LED piscar por um determinado período e com uma determinada frequência. Essa operação pode ser realizada chamando repetidamente as duas operações básicas anteriores. Mas veremos nos testes que as trocas de dados entre a camada [DAO] e um Arduino ocorrem na ordem de segundos. Portanto, não é possível fazer um LED piscar a cada 100 milissegundos, por exemplo. Assim, implementaremos essa função de piscar diretamente no próprio Arduino.

O funcionamento do Arduino será o seguinte:

  • as comunicações entre a camada [DAO] e um Arduino ocorrem por meio de uma rede TCP-IP, por meio de trocas de linhas de texto no formato jSON (JavaScript Object Notation);
  • ao iniciar, o Arduino se conecta à porta 100 de um servidor de registro presente na camada [DAO]. Ele envia ao servidor uma única linha de texto:
{"id":"cuisine","desc":"duemilanove","mac":"90:A2:DA:00:1D:A7","port":102}

Trata-se de uma sequência jSON que identifica o Arduino que está se conectando:

  • id: um identificador do Arduino;
  • desc: uma descrição do que o Arduino é capaz de fazer. Aqui, indicamos simplesmente o tipo do Arduino;
  • mac: endereço MAC do Arduino;
  • port: o número da porta na qual o Arduino aguardará os comandos da camada [DAO].

Todas essas informações são do tipo cadeia de caracteres, exceto a porta, que é um número inteiro.

  • Assim que o Arduino se registra no servidor de registro, ele fica à escuta na porta que indicou ao servidor (102, conforme mencionado acima). Ele aguarda comandos jSON no seguinte formato:
{"id":"identifiant","ac":"une_action","pa":{"param1":"valeur1","param2":"valeur2",...}}

Trata-se de uma sequência jSON com os seguintes elementos:

  • id: um identificador do comando. Pode ser qualquer valor;
  • ac: uma ação. Existem três:
  • pw (pin write) para gravar um valor em um pino,
  • pr (pin read) para ler o valor de um pino,
  • cl (piscar) para fazer um LED piscar;
  • pa: os parâmetros da ação. Eles dependem da ação.
  • O Arduino sempre retorna uma resposta ao seu cliente. Essa resposta é uma sequência de caracteres jSON com o seguinte formato:
{"id":"1","er":"0","et":{"pinx":"valx"}}

onde

  • id: o identificador do comando ao qual se responde;
  • er (erro): um código de erro caso tenha ocorrido um erro; caso contrário, 0;
  • e (estado): um dicionário sempre vazio, exceto para o comando de leitura pr. Nesse caso, o dicionário contém o valor do pino nº x solicitado.

Aqui estão alguns exemplos para esclarecer as especificações anteriores:

Fazer o LED nº 8 piscar 10 vezes com um intervalo de 100 milissegundos:

Comando
{"id":"1","ac":"cl","pa":{"pin":"8","dur":"100","nb":"10"}}
Resposta
{"id":"1","er":"0","et":{}}

Os parâmetros pa do comando cl são: a duração dur, em milissegundos, de um piscar, o número nb de piscadas e o número do pino do LED.

Escreva o valor binário 1 no pino nº 7:

Comando
{"id":"2","ac":"pw","pa":{"pin":"7","mod":"b","val":"1"}}
Resposta
{"id":"2","er":"0","et":{}}

Os parâmetros pa do comando pw são: o modo mod b (binário) ou a (analógico) da gravação, o valor val a ser gravado e o número do pino. Para uma gravação binária, val é 0 ou 1. Para uma gravação analógica, val está no intervalo [0,255].

Gravar o valor analógico 120 no pino nº 2:

Comando
{"id":"3","ac":"pw","pa":{"pin":"2","mod":"a","val":"120"}}
Resposta
{"id":"3","er":"0","et":{}}

Ler o valor analógico do pino 0:

Comando
{"id":"4","ac":"pr","pa":{"pin":"0","mod":"a"}}
Resposta
{"id":"4","er":"0","et":{"pin0":"1023"}}

Os parâmetros “pa” do comando “pr” são: o modo “mod b” (binário) ou “a” (analógico) da leitura, e o número do pino. Se não houver erro, o Arduino insere no dicionário “et” de sua resposta o valor do pino solicitado. Aqui, pin0 indica que foi solicitado o valor do pino nº 0, e 1023 é esse valor. Na leitura, um valor analógico estará no intervalo [0, 1024].

Apresentamos os três comandos cl, pw e pr. Pode-se questionar por que não utilizamos campos mais explícitos nas sequências jSON, como “action” em vez de “ac”, “pinwrite” em vez de “pw”, “parametres” em vez de “pa”, etc. Um Arduino possui uma memória muito limitada. No entanto, as sequências jSON trocadas com o Arduino contribuem para o consumo de memória. Por isso, optamos por encurtá-las ao máximo.

Vejamos agora alguns casos de erros:

Comando
xx
Resposta
{"id":"","er":"100","et":{}}

Foi enviado um comando que não está no formato jSON. O Arduino retornou o código de erro 100.

Comando
{"id":"4","ac":"pr","pa":{"mod":"a"}}
Resposta
{"id":"4","er":"302","et":{}}

Foi enviado um comando pr sem o parâmetro pin. O Arduino retornou o código de erro 302.

Comando
{"id":"4","ac":"pinread","pa":{"pin":"0","mod":"a"}}
Resposta
{"id":"4","er":"104","et":{}}

Enviamos um comando pinread desconhecido (que é o pr). O Arduino retornou o código de erro 104.

Não continuaremos com os exemplos. A regra é simples. O Arduino não deve travar, independentemente do comando que lhe for enviado. Antes de executar um comando jSON, ele verifica se este está correto. Assim que surge um erro, o Arduino interrompe a execução do comando e retorna ao cliente a sequência de erro jSON. Mais uma vez, devido à limitação de espaço na memória, enviamos um código de erro em vez de uma mensagem completa.

O código do programa executado no Arduino é fornecido nos exemplos deste documento:

  

Para transferi-lo para o Arduino:

  • conecte-o ao seu PC;
  • no [1], abra o arquivo [arduino_uno.ino]. O Arduino IDE será iniciado e carregará o arquivo;

Observação: o código foi originalmente criado e testado com o IDE ARDUINO 1.5.x. Desde então, outras versões do IDE foram lançadas. O código não funcionou com o IDE ARDUINO 1.6.x. Parece haver um problema de compatibilidade com versões anteriores entre as versões 1.6 e 1.5.

  • No [2-4], especifique o tipo de Arduino utilizado;
  • no [5-7], indique em qual porta serial do PC ele está conectado;
  • no [8], faça o upload (=carregue) do programa [arduino_uno] para o Arduino;

O código do programa está bem comentado. O leitor interessado poderá consultá-lo. Destacamos apenas as linhas de código que permitem configurar a comunicação bidirecional cliente/servidor entre o Arduino e o PC:


#include <SPI.h>
#include <Ethernet.h>
#include <ajSON.h>

// ---------------------------------- CONFIGURATION DE O ARDUINO UNO
// endereço MAC do Arduino UNO
byte macArduino[] = { 
  0x90, 0xA2, 0xDA, 0x0D, 0xEE, 0xC7 };
char * strMacArduino="90:A2:DA:0D:EE:C7";
// o endereço IP do Arduino
IPAddress ipArduino(192,168,2,2);
// seu identificador
char * idArduino="cuisine";
// porta do servidor do Arduino
int portArduino=102;
// descrição do Arduino
char * descriptionArduino="contrôle domotique";
// o servidor do Arduino funcionará na porta 102
EthernetServer server(portArduino);
// IP do servidor de registro
IPAddress ipServeurEnregistrement(192,168,2,1); 
// porta do servidor de registro
int portServeurEnregistrement=100;
// o cliente Arduino do servidor de registro
EthernetClient clientArduino;
// o comando do cliente
char commande[100];
// a resposta do Arduino
char message[100];

// inicialização
void setup() {
  // O monitor serial permitirá acompanhar as trocas de dados
  Serial.begin(9600);
  // inicialização da conexão Ethernet
  Ethernet.begin(macArduino,ipArduino);  
  // memória disponível
  Serial.print(F("Memoire disponible : "));
  Serial.println(freeRam());
}

// loop infinito
void loop()
{
  ...
}
  • linha 8: o endereço MAC do Arduino. Isso não tem muita importância aqui, pois o Arduino estará em uma rede privada onde há um PC e um ou mais Arduinos. Basta que o endereço MAC seja único nessa rede privada. Normalmente, a placa de rede do Arduino possui um adesivo onde está indicado o endereço MAC da placa. Se esse adesivo estiver ausente e você não souber o endereço MAC da placa, pode inserir o que quiser na linha 8, desde que a regra de exclusividade do endereço MAC na rede privada seja respeitada;
  • linha 11: o endereço IP da placa. Novamente, pode-se inserir o que quiser, como [192.168.2.x], variando o valor de x para os diferentes Arduinos da rede privada;
  • linha 13: identificador do Arduino. Deve ser único entre os identificadores dos Arduinos de uma mesma rede privada;
  • linha 15: a porta de serviço do Arduino. Pode-se inserir o que quiser;
  • linha 17: a descrição da função do Arduino. Pode-se inserir o que se quiser. Cuidado com strings muito longas devido à memória limitada do Arduino;
  • linha 21: endereço IP do servidor de registro do Arduino no PC. Não deve ser alterado;
  • linha 23: porta desse serviço de registro. Não deve ser alterada;

5.4. O servidor web / jSON

5.4.1. Instalação

Image

O binário Java do servidor web / jSON é fornecido a você:

 

Abra uma janela de comando e digite o seguinte comando:

dos>java -jar arduinos-server-01-all-1.0.jar

Se o [java.exe] não estiver no PATH da janela de comandos, será necessário digitar o caminho completo do [java.exe] (geralmente C:\Program Files\java\...).

Uma janela DOS será aberta e exibirá os logs:


.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::             (v0.5.0.M6)

2014-01-06 11:11:35.550  INFO 8408 --- [           main] arduino.rest.metier.Application          : Starting Application on Gportpers3 with PID 8408 (C:\Users\SergeTahÚ\Desktop\part2\server.jar started by ST)
2014-01-06 11:11:35.587  INFO 8408 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@6a4ba620: startup date [Mon Jan 06 11:11:35 CET 2014]; root of context hierarchy
2014-01-06 11:11:36.765  INFO 8408 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2014-01-06 11:11:36.766  INFO 8408 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.42
2014-01-06 11:11:36.876  INFO 8408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-01-06 11:11:36.877  INFO 8408 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1293 ms
2014-01-06 11:11:37.084  INFO 8408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2014-01-06 11:11:37.084  INFO 8408 --- [ost-startStop-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2014-01-06 11:11:37.184  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.386  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.faireClignoterLed(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.388  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/commands/{idArduino}],methods=[POST],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.sendCommandesJson(java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.388  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.getArduinos(javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.389  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.pinRead(java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.390  INFO 8408 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String arduino.rest.metier.RestMetier.pinWrite(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,javax.servlet.http.HttpServletResponse)
2014-01-06 11:11:37.463  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] para o manipulador do tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.464  INFO 8408 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] para o manipulador do tipo [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-01-06 11:11:37.881  INFO 8408 --- [ost-startStop-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 796 ms
Serveur d'enregistrement lancÚ sur 192.168.2.1:100
2014-01-06 11:11:38.101  INFO 8408 --- [       Thread-4] arduino.dao.Recorder                  : Recorder : [11:11:38:101] : [Serveur d'enregistrement : attente d'un client]
2014-01-06 11:11:38.142  INFO 8408 --- [           main] arduino.rest.metier.Application : Started Application in 3.257 seconds
  • linha 11: um servidor Tomcat integrado é iniciado;
  • linha 15: o servlet [dispatcherServlet] do Spring MVC é carregado e executado;
  • linha 18: o Rest URL é detectado;
  • linha 19: o Rest URL [/arduinos/commands/{idArduino}] é detectado;
  • linha 20: o URL Rest [/arduinos/] é detectado;
  • linha 21: o URL Rest [/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}] é detectado;
  • linha 22: o URL Rest [/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}] é detectado;
  • linha 26: o servidor de registro dos Arduinos é iniciado;

Conecte seu Arduino ao PC, caso ainda não tenha feito isso. O firewall do PC deve estar desativado. Em seguida, usando um navegador, acesse o URL [http://localhost:8080/arduinos]:

Você deverá ver aparecer o identificador do Arduino conectado. Se nada aparecer, tente reiniciar o Arduino. Ele possui um botão para isso.

O servidor web / jSON já está instalado.

5.4.2. Os URL expostos pelo serviço web / jSON

Leitura recomendada: projeto [Exemple-15] (ver parágrafo 1.16.1);

O serviço web / jSON foi implementado com o Spring MVC e expõe as seguintes URL:


@Controller
public class WebController {

  // camada de negócios
  @Autowired
  private IMetier métier;

  // lista de Arduinos
  @RequestMapping(value = "/arduinos", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String getArduinos() throws JsonProcessingException {
    ...
  }

  // luz piscando
  @RequestMapping(value = "/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String faireClignoterLed(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("duree") int duree, @PathVariable("nombre") int nombre) throws JsonProcessingException {
...
  }

  // envio de comandos JSON
  @RequestMapping(value = "/arduinos/commands/{idArduino}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String sendCommandesJson(@PathVariable("idArduino") String idArduino, HttpServletRequest request) throws IOException {
    ...
  }

  // leitura de pino
  @RequestMapping(value = "/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String pinRead(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("mode") String mode) throws JsonProcessingException {
    ....
  }

  // gravação no pino
  @RequestMapping(value = "/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
  @ResponseBody
  public String pinWrite(@PathVariable("idCommande") String idCommande, @PathVariable("idArduino") String idArduino, @PathVariable("pin") int pin, @PathVariable("mode") String mode, @PathVariable("valeur") int valeur) throws JsonProcessingException {
  ...
  }
}

As respostas enviadas pelo servidor são representações jSON da seguinte classe [Response<T>]:


package client.android.dao.service;

import java.util.List;

public class Response<T> {

    // ----------------- propriedades
    // status da operação
    private int status;
    // eventuais mensagens de status
    private List<String> messages;
    // corpo da resposta
    private T body;

    // construtores
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getters e setters
...
}

O URL [/arduinos] envia uma resposta do tipo [Response<List<Arduino>>], em que [Arduino] é a seguinte classe:


package android.arduinos.entities;

import java.io.Serializable;

public class Arduino implements Serializable {
  // dados
  private String id;
  private String description;
  private String mac;
  private String ip;
  private int port;

// getters e setters
...
}
  • linha 7: [id] é o identificador do Arduino;
  • linha 8: sua descrição;
  • linha 9: seu endereço MAC;
  • linha 10: seu endereço IP;
  • linha 11: a porta na qual ele aguarda comandos;

Os URL:

  • [/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}];
  • [/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}];
  • [/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}];
  • [/arduinos/commands/{idArduino}];

enviam uma resposta do tipo [Response<ArduinoResponse>], em que a classe [ArduinoResponse] representa a resposta padrão de um Arduino:


public class ArduinoResponse implements Serializable {
  
  private String json;
  private String id;
  private String erreur;
  private Map<String, Object> etat;

  // getters e setters
...
}
  • [json]: a sequência jSON enviada por um Arduino e que não pôde ser decodificada (caso de erro); caso contrário, null;
  • [id]: o identificador do comando ao qual o Arduino responde;
  • [erreur]: um código de erro, 0 se for OK, outro valor caso contrário;
  • [etat]: um dicionário contendo a resposta específica ao comando. Na maioria das vezes, ele está vazio, a menos que o comando tenha solicitado a leitura de um valor do Arduino; nesse caso, esse valor será inserido nesse dicionário;

5.4.3. Testes do serviço web / jSON

Familiarize-se com o servidor web / jSON testando os seguintes URL:

URL
rôle
http://localhost:8080/arduinos/
rend la liste des Arduinos connectés
http://localhost:8080/arduinos/
blink/1/cuisine/8/100/20/
fait clignoter la led de la pin n° 8
 de l'Arduino identifié par cuisine,
 20 fois toutes les 100 ms.
http://localhost:8080/arduinos/
pinRead/1/cuisine/0/a/
lecture analogique de la pin n° 0 de
 l'Arduino identifié par cuisine
http://localhost:8080/arduinos/
pinRead/1/cuisine/5/b/
lecture binaire de la pin n° 5 de
 l'Arduino identifié par cuisine
http://localhost:8080/arduinos/
pinWrite/1/cuisine/8/b/1/
écriture binaire de la valeur 1 sur la pin n° 8 de l'Arduino identifié par
 cuisine
http://localhost:8080/arduinos/
pinWrite/1/cuisine/4/a/100/
écriture analogique de la valeur 100 sur la pin n° 4 de l'Arduino identifié
 par cuisine

Aqui estão algumas capturas de tela do resultado que você deve obter:

Obter a lista dos Arduinos conectados:

A sequência jSON recebida do servidor web / jSON é um objeto com os seguintes campos:

  • [status]: se for 0, indica que não houve erro; caso contrário, houve erro;
  • [messages]: uma lista de mensagens explicando o erro, caso tenha ocorrido um erro:
  • [body]: a lista dos Arduinos caso não tenha ocorrido erro. Cada Arduino é, então, descrito por um objeto com os seguintes campos:
    • [id]: identificador do Arduino. Dois Arduinos não podem ter o mesmo identificador;
    • [description]: breve descrição da funcionalidade do Arduino;
    • [mac]: endereço MAC do Arduino;
    • [ip]: endereço IP do Arduino;
    • [port]: porta na qual ele aguarda comandos;

Fazer o LED do pino nº 8 do Arduino identificado por [cuisine] piscar 20 vezes a cada 100 ms:

 

A string jSON recebida do servidor web / jSON é um objeto com os seguintes campos:

  • [status]: se for 0, indica que não houve erro; caso contrário, houve erro;
  • [messages]: uma lista de mensagens explicando o erro, caso tenha ocorrido algum erro:
  • [body]: a resposta do Arduino caso não tenha ocorrido erro:
    • [id]: identificador do comando. Esse identificador é o 1 em [/blink/1]. O Arduino repete esse identificador de comando em sua resposta;
    • [erreur]: um número de erro. Um valor diferente de 0 indica um erro;
    • [etat]: é usado apenas para a leitura de um pino. Nesse caso, seu valor é o valor do pino;
    • [json]: é utilizado apenas em caso de erro jSON entre o cliente e o servidor. Nesse caso, seu valor é a sequência de caracteres incorreta jSON enviada pelo Arduino;

Leitura analógica do pino nº 0 do Arduino identificado por [cuisine]:

 

A sequência jSON recebida do servidor web / jSON é análoga à anterior, com a única diferença no campo [etat], que representa o valor do pino nº 0.

Leitura binária do pino nº 5 do Arduino identificado por [cuisine]:

 

A sequência jSON recebida do servidor web / jSON é análoga à anterior.

Gravação binária do valor 1 no pino nº 8 do Arduino identificado por [cuisine]:

 

A sequência jSON recebida do servidor web / jSON é análoga à anterior.

O teste do URL [http://localhost:8080/arduinos/commands/cuisine] é mais complexo. O método do servidor web / jSON, que processa essa URL, aguarda uma solicitação POST que não pode ser simulada simplesmente com um navegador. Para testar este URL, é possível usar um navegador Chrome com a extensão [Advanced REST Client] (ver parágrafo 6.13):

 
  • em [1], o URL do método web / jSON a ser testado;
  • em [2], o método POST para enviar a solicitação;
  • em [3-4], o valor enviado é o de jSON;
  • em [5], a string jSON foi enviada. Observe bem os colchetes que iniciam e encerram a lista. Aqui, na lista, há apenas um comando jSON que faz o pino nº 8 piscar 10 vezes a cada 100 ms;
  • no [6], enviamos a solicitação;
 
  • no [7], a resposta jSON enviada pelo servidor. O objeto recebeu um objeto com os dois campos habituais [status, messages] e um campo [body] cujo valor é a lista das respostas do Arduino a cada um dos comandos jSON enviados.

Vamos ver o que acontece quando se envia um comando jSON com sintaxe incorreta para o Arduino:

Recebemos então a seguinte resposta:

 

Percebe-se que, na resposta do Arduino, o número do erro é [104], indicando que o comando [xx] não foi reconhecido.

5.5. Testes no cliente Android

Segue abaixo o arquivo executável do cliente Android finalizado:

  

Com o mouse, arraste o arquivo executável [app-debug.apk] acima para um emulador de tablet [GenyMotion]. Ele será então salvo e executado. Inicie também o servidor web / jSON, caso ainda não tenha feito isso. Conecte o Arduino ao PC com um LED acoplado a ele. O cliente Android permite gerenciar os Arduinos remotamente. Ele exibe as seguintes telas ao usuário.

A aba [CONFIG] permite conectar-se ao servidor e recuperar a lista dos Arduinos conectados:

Image

  • em [1], insira o endereço IP [192.168.2.1] fornecido ao seu PC (consulte o parágrafo 5.2).

A aba [PINWRITE] permite gravar um valor em um pino de um Arduino:

Image

Image

A guia [PINREAD] permite ler o valor de um pino de um Arduino:

Image

A guia [BLINK] permite fazer um LED de um Arduino piscar:

Image

A guia [COMMAND] permite enviar um comando jSON para um Arduino:

Image

5.6. O cliente Android do serviço web / jSON

Passaremos agora à programação do cliente Android.

5.6.1. A arquitetura do cliente

A arquitetura do cliente Android será a do projeto [Exemple-15] (ver parágrafo 1.16.2);

  • a camada [DAO] se comunica com o servidor web / jSON;

O cliente Android deve ser capaz de controlar vários Arduinos simultaneamente. Por exemplo, queremos poder fazer dois LEDs, instalados em dois Arduinos, piscarem ao mesmo tempo, e não um após o outro. Portanto, nosso cliente Android utilizará uma tarefa assíncrona por Arduino, e essas tarefas serão executadas em paralelo.

5.6.2. O projeto do Android Studio do cliente

Duplique o projeto [client-android-skel] (ver parágrafo 2) no projeto [client-arduinos-01] (se necessário, consulte novamente como duplicar um projeto Gradle no parágrafo 1.15):

Image

5.6.3. As cinco visualizações XML

  

Haverá cinco visualizações XML:

  • [blink]: para fazer um LED de um Arduino piscar. Ela está associada ao fragmento [BlinkFragment];
  • [commands]: para enviar um comando jSON a um Arduino. Está associada ao fragmento [CommandsFragment];
  • [config]: para configurar o URL do serviço web / jSON e obter a lista inicial de Arduinos conectados. Está associado ao fragmento [ConfigFragment];
  • [pinread]: para ler o valor binário ou analógico de um pino de um Arduino. Está associado ao fragmento [PinReadFragment];
  • [pinwrite]: para gravar um valor binário ou analógico em um pino de um Arduino. Está associada ao fragmento [PinWriteFragment];

Por enquanto, essas cinco visualizações XML terão todas o mesmo conteúdo vazio:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
  </RelativeLayout>
</ScrollView>
  • a visualização está em um contêiner [RelativeLayout] (linhas 7-10), que por sua vez está incluído em um contêiner [ScrollView] (linhas 2-11). Isso garante que possamos rolar a visualização caso ela ultrapasse o tamanho da tela de um tablet;

Tarefa: crie as cinco visualizações XML.


5.6.4. O menu dos fragmentos

Sabemos que os fragmentos de um projeto criado com [client-android-skel] devem estar associados a um menu, mesmo que vazio. Neste caso, o aplicativo não terá menu. O menu vazio já está no projeto;

  

5.6.5. Os cinco fragmentos do aplicativo

 

Tarefa: duplique o fragmento [DummyFragment] nos cinco fragmentos do aplicativo, conforme mostrado em [2].


O fragmento [ConfigFragment] tem a seguinte estrutura:


package client.android.fragments.behavior;

import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.fragments.state.DummyFragmentState;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;

@EFragment
@OptionsMenu(R.menu.menu_vide)
public class ConfigFragment extends AbstractFragment {

  // campos herdados da classe pai -------------------------------------------------------
...

Substitua a linha 10 pela seguinte:


@EFragment(R.layout.config)

Tarefa: faça o mesmo para os outros quatro fragmentos, adaptando o atributo [@EFragment] da classe.


Fragmento
Visão
ConfigFragment

R.layout.config
PinReadFragment

R.layout.pinread
PinWriteFragment

R.layout.pinwrite
CommandsFragment

R.layout.commands
BlinkFragment

R.layout.blink

5.6.6. Os estados dos fragmentos

Cada fragmento terá um estado.


Tarefa: duplique a classe [DummyFragmentState] cinco vezes para criar os cinco estados apresentados em [2].


5.6.7. Personalização do projeto

 

O pacote [architecture / custom] contém os elementos personalizáveis da arquitetura do aplicativo.

5.6.7.1. A interface [IMainActivity]

A interface [IMainActivity] define o que os fragmentos podem solicitar à atividade, bem como as constantes do aplicativo. Essa interface será a seguinte:


package client.android.architecture.custom;

import client.android.architecture.core.ISession;
import client.android.dao.service.IDao;

public interface IMainActivity extends IDao {

  // acesso à sessão
  ISession getSession();

  // mudança de visualização
  void navigateToView(int position, ISession.Action action);

  // gerenciamento de espera
  void beginWaiting();

  void cancelWaiting();

  // constantes da aplicação -------------------------------------

  // modo de depuração
  boolean IS_DEBUG_ENABLED = true;

  // tempo máximo de espera pela resposta do servidor
  int TIMEOUT = 1000;

  // tempo de espera antes da execução da solicitação do cliente
  int DELAY = 000;

  // autenticação básica
  boolean IS_BASIC_AUTHENTIFICATION_NEEDED = false;

  // adjacência dos fragmentos
  int OFF_SCREEN_PAGE_LIMIT = 1;

  // barra de abas
  boolean ARE_TABS_NEEDED = true;

  // imagem de espera
  boolean IS_WAITING_ICON_NEEDED = true;

  // número de fragmentos
  int FRAGMENTS_COUNT = 5;

  // número de visualizações
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}
  • linhas 25, 28, 31, 40: configuração da camada [DAO]. Este aplicativo consulta um servidor web / jSON;
  • linha 37: esta aplicação possui abas;
  • linha 43: este aplicativo possui cinco fragmentos;
  • linhas 46-50: os números dos cinco fragmentos;
  • linha 34: adjacência dos fragmentos. O desenvolvedor pode inserir aqui um valor no intervalo [1, FRAGMENTS_COUNT-1];

5.6.7.2. A classe [CoreState]

A classe [CoreState] é a classe pai dos estados dos fragmentos:


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ConfigFragmentState.class),
  @JsonSubTypes.Type(value = BlinkFragmentState.class),
  @JsonSubTypes.Type(value = PinReadFragmentState.class),
  @JsonSubTypes.Type(value = PinWriteFragmentState.class),
  @JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
  // fragmento visitado ou não
  protected boolean hasBeenVisited = false;
  // estado do eventual menu do fragmento
  protected MenuItemState[] menuOptionsState;

  // getters e setters
...
}
  • linhas 12-16: é necessário declarar aqui as classes dos estados dos cinco fragmentos;

5.6.8. A classe [MainActivity]

  

A classe [MainActivity] será a seguinte:


package client.android.activity;

import android.support.design.widget.TabLayout;
import android.util.Log;
import client.android.R;
import client.android.architecture.core.AbstractActivity;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.core.ISession;
import client.android.architecture.custom.IMainActivity;
import client.android.architecture.custom.Session;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.ArduinoCommand;
import client.android.dao.entities.ArduinoResponse;
import client.android.dao.service.Dao;
import client.android.dao.service.IDao;
import client.android.dao.service.Response;
import client.android.fragments.behavior.*;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.OptionsMenu;
import rx.Observable;

import java.util.List;
import java.util.Locale;

@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // camada [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessão
  private Session session;

  // métodos da classe pai -----------------------
  @Override
  protected void onCreateActivity() {
    // log
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sessão
    this.session = (Session) super.session;
    // criação das cinco abas
    for (int i = 0; i < 5; i++) {
      TabLayout.Tab newTab = tabLayout.newTab();
      newTab.setText(getFragmentTitle(i));
      tabLayout.addTab(newTab);
    }
  }

  @Override
  protected IDao getDao() {
    return dao;
  }

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new ConfigFragment_(), new BlinkFragment_(), new PinReadFragment_(), new PinWriteFragment_(), new CommandsFragment_()};
  }

  @Override
  protected CharSequence getFragmentTitle(int position) {
    Locale l = Locale.getDefault();
    switch (position) {
      case 0:
        return getString(R.string.config_titre).toUpperCase(l);
      case 1:
        return getString(R.string.blink_titre).toUpperCase(l);
      case 2:
        return getString(R.string.pinread_titre).toUpperCase(l);
      case 3:
        return getString(R.string.pinwrite_titre).toUpperCase(l);
      case 4:
        return getString(R.string.commands_titre).toUpperCase(l);
    }
    return null;
  }

  @Override
  protected void navigateOnTabSelected(int position) {
    // exibição do fragmento na posição n.º
    navigateToView(position, ISession.Action.NAVIGATION);
  }

  @Override
  protected int getFirstView() {
    return IMainActivity.VUE_CONFIG;
  }

  // implementação IDao -----------------------------------------
}
  • linhas 46-50: criação das cinco abas do aplicativo;
  • linha 48: os títulos das abas são fornecidos pelo método das linhas 63-79;
  • os cinco fragmentos são instanciados na linha 60. Devido às anotações AA, as classes dos fragmentos são as apresentadas anteriormente, com um sublinhado como sufixo;
  • linhas 63-79: define-se um título para cada um dos fragmentos. Esses títulos serão buscados no arquivo [res / values / strings.xml]
  

O conteúdo do arquivo [strings.xml] é o seguinte:


<?xml version="1.0" encoding="utf-8"?>
<resources>

  <!-- nome do aplicativo -->
  <string name="app_name">[arduinos-client-01]</string>
  <!-- Fragmentos e abas -->
  <string name="config_titre">[Config]</string>
  <string name="blink_titre">[Blink]</string>
  <string name="pinread_titre">[PinRead]</string>
  <string name="pinwrite_titre">[PinWrite]</string>
  <string name="commands_titre">[Commands]</string>

</resources>

Tarefa: crie os elementos acima e compile o projeto. Não deve haver erros.


Execute o projeto. Você deverá obter a seguinte visualização no emulador:

Image

Analise os logs gerados durante a exibição da primeira tela e acompanhe as diferentes etapas executadas. Navegue de uma aba para outra e continue acompanhando os logs.

5.6.9. A visualização XML [config]

A visualização XML [config] será a seguinte:

A visualização acima é obtida com o código XML a seguir:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

  <RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
      android:id="@+id/txt_TitreConfig"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentTop="true"
      android:layout_centerHorizontal="true"
      android:layout_marginTop="150dp"
      android:text="@string/txt_TitreConfig"
      android:textSize="@dimen/titre"/>

    <TextView
      android:id="@+id/txt_UrlServiceRest"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_TitreConfig"
      android:layout_marginTop="50dp"
      android:text="@string/txt_UrlServiceRest"
      android:textSize="20sp"/>

    <EditText
      android:id="@+id/edt_UrlServiceRest"
      android:layout_width="300dp"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_UrlServiceRest"
      android:layout_alignBottom="@+id/txt_UrlServiceRest"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_UrlServiceRest"
      android:ems="10"
      android:hint="@string/hint_UrlServiceRest"
      android:inputType="textUri">

      <requestFocus/>
    </EditText>

    <TextView
      android:id="@+id/txt_MsgErreurIpPort"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_UrlServiceRest"
      android:layout_marginTop="20dp"
      android:text="@string/txt_MsgErreurUrlServiceRest"
      android:textColor="@color/red"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/txt_arduinos"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_MsgErreurIpPort"
      android:layout_marginTop="40dp"
      android:text="@string/titre_list_arduinos"
      android:textColor="@color/blue"
      android:textSize="20sp"/>

    <Button
      android:id="@+id/btn_Rafraichir"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_arduinos"
      android:layout_alignBottom="@+id/txt_arduinos"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_arduinos"
      android:text="@string/btn_rafraichir"/>

    <Button
      android:id="@+id/btn_Annuler"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignBaseline="@+id/txt_arduinos"
      android:layout_alignBottom="@+id/txt_arduinos"
      android:layout_marginLeft="20dp"
      android:layout_toRightOf="@+id/txt_arduinos"
      android:text="@string/btn_annuler"
      android:visibility="invisible"/>

    <ListView
      android:id="@+id/ListViewArduinos"
      android:layout_width="match_parent"
      android:layout_height="200dp"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_arduinos"
      android:layout_marginTop="30dp"
      android:background="@color/wheat">
    </ListView>

  </RelativeLayout>
</ScrollView>

A visualização utiliza cadeias de caracteres (android:text nas linhas 15, 25, 37, 50, 61, 73) que estão definidas no arquivo [res / values / strings]:

  

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">android-domotique</string>

    <!-- Fragmentos e abas -->
    <string name="config_titre">[Config]</string>
    <string name="blink_titre">[Blink]</string>
    <string name="pinread_titre">[PinRead]</string>
    <string name="pinwrite_titre">[PinWrite]</string>
    <string name="commands_titre">[Commands]</string>

    <!-- Configuração -->
    <string name="txt_TitreConfig">Se connecter au serveur</string>
    <string name="txt_UrlServiceRest">Url du service web / jSON</string>
    <string name="txt_MsgErreurUrlServiceRest">L\'Url du service doit être entrée sous la forme Ip1.Ip2.Ip3.IP4:Port/contexte</string>
    <string name="hint_UrlServiceRest">ex (192.168.1.120:8080/rest)</string>
    <string name="btn_annuler">Annuler</string>
    <string name="btn_rafraichir">Rafraîchir</string>
    <string name="titre_list_arduinos">Liste des Arduinos connectés</string>
    
</resources>

A visualização utiliza cores (android:textColor nas linhas 51 e 62) definidas no arquivo [res / values / colors]:

  

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="colorPrimary">#3F51B5</color>
  <color name="colorPrimaryDark">#303F9F</color>
  <color name="colorAccent">#FF4081</color>
  <color name="floral_white">#FFFAF0</color>
  <!-- aplicativo -->
  <color name="red">#FF0000</color>
  <color name="blue">#0000FF</color>
  <color name="wheat">#FFEFD5</color>
</resources>

A vista utiliza dimensões (android:textSize na linha 16) que estão definidas no arquivo [res / values / dimens]:

  

<resources>
  <!-- Margens padrão da tela, de acordo com as diretrizes de design do Android. -->
  <dimen name="activity_horizontal_margin">16dp</dimen>
  <dimen name="activity_vertical_margin">16dp</dimen>
  <dimen name="fab_margin">16dp</dimen>
  <dimen name="appbar_padding_top">8dp</dimen>
  <!-- aplicativo -->
  <dimen name="titre">30dp</dimen>
</resources>

Essa técnica não foi utilizada para todas as dimensões. No entanto, é a recomendada. Ela permite alterar as dimensões em um único local.


Tarefa: crie os elementos acima.


Execute novamente seu projeto. Você deverá obter a seguinte visualização:

Image

5.6.10. O fragmento [ConfigFragment]

  

Para gerenciar a nova visualização [config], o código do fragmento [ConfigFragment] é alterado da seguinte forma:


package client.android.fragments.behavior;

import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import client.android.R;
import client.android.architecture.core.AbstractFragment;
import client.android.architecture.custom.CoreState;
import client.android.architecture.custom.IMainActivity;
import client.android.fragments.state.ConfigFragmentState;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EFragment;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.ViewById;

@EFragment(R.layout.config)
@OptionsMenu(R.menu.menu_vide)
public class ConfigFragment extends AbstractFragment {

  // elementos da interface visual
  @ViewById(R.id.btn_Rafraichir)
  protected Button btnRafraichir;
  @ViewById(R.id.btn_Annuler)
  protected Button btnAnnuler;
  @ViewById(R.id.edt_UrlServiceRest)
  protected EditText edtUrlServiceRest;
  @ViewById(R.id.txt_MsgErreurIpPort)
  protected TextView txtMsgErreurUrlServiceRest;
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;

  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
  }

  // Gerenciamento do ciclo de vida do fragmento -------------------------------------

  @Override
  public CoreState saveFragment() {
    return new ConfigFragmentState();
  }

  @Override
  protected int getNumView() {
    return IMainActivity.VUE_CONFIG;
  }

  @Override
  protected void initFragment(CoreState previousState) {

  }

  @Override
  protected void initView(CoreState previousState) {
    // Primeira visita?
    if(previousState==null){
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    }
  }

  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
  }

  @Override
  protected void notifyEndOfUpdates() {
    // botões
    initButtons();
  }

  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
  }

  // métodos privados --------------------------------------------

  private void initButtons() {
    // o botão [Exécuter] substitui o botão [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
}
  • linhas 23-32: os elementos da interface visual;
  • linhas 58-60: na primeira visita ao fragmento, a mensagem de erro é ocultada;
  • linhas 73-76: sempre que o fragmento for exibido, o botão [Annuler] será ocultado (linha 82) e o botão [Rafraîchir] será exibido (linhas 86-87). De fato, nesta aplicação, um fragmento não pode ser exibido enquanto uma operação assíncrona estiver em andamento e, portanto, o botão [Annuler] fica visível;

Tarefa: crie os elementos acima.


Execute esta nova versão. A primeira visualização deve agora ser a seguinte:

Image

5.6.10.1. O botão [Rafraîchir]

Por enquanto, vamos tratar o clique no botão [Rafraîchir] da seguinte maneira:


@Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // vamos iniciar uma tarefa — preparamos a espera
    beginWaiting(1);
  }

  @Click(R.id.btn_Annuler)
  protected void doAnnuler() {
    if (isDebugEnabled) {
      Log.d(className, "Annulation demandée");
    }
    // cancelamos as tarefas assíncronas
    cancelRunningTasks();
  }

  protected void beginWaiting(int numberOfRunningTasks) {
    // preparando a espera das tarefas
    beginRunningTasks(numberOfRunningTasks);
    // o botão [Annuler] substitui o botão [Rafraîchir]
    btnRafraichir.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
}
  // gerenciamento do ciclo de vida do fragmento -------------------------------------
...
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // botões em seu estado inicial
    initButtons();
  }

  // métodos privados --------------------------------------------

  private void initButtons() {
    // o botão [Exécuter] substitui o botão [Annuler]
    btnAnnuler.setVisibility(View.INVISIBLE);
    btnRafraichir.setVisibility(View.VISIBLE);
  }
  • linhas 1-5: o método executado ao clicar no botão [Rafraîchir];
  • linha 4: iniciamos a espera;
  • linha 18: passamos para a classe pai o número de tarefas assíncronas que vamos iniciar. A imagem de espera será exibida;
  • linhas 20-21: essa espera resultará no aparecimento do botão [Annuler], no desaparecimento do botão [Rafraîchir] e no aparecimento da imagem de espera. Nada mais acontece. O usuário pode, no entanto, clicar no botão [Annuler]. O método das linhas 7-14 será então executado;
  • linha 13: solicita-se à classe pai que cancele todas as tarefas. A classe fará isso e, em seguida, chamará o método das linhas 25 a 29 para sinalizar que todas as tarefas foram concluídas. O parâmetro [runningTasksHaveBeenCanceled] terá o valor true para indicar que houve cancelamento das tarefas;
  • linhas 35-36: o botão [Annuler] desaparecerá, enquanto o botão [Rafraîchir] reaparecerá.

Tarefa: Faça essas alterações e, em seguida, execute o projeto. Verifique se o botão [Rafraîchir] inicia a espera e se o botão [Annuler] a interrompe. Observe os logs.


5.6.10.2. Verificação das entradas

Na versão anterior, não verificávamos a validade da entrada URL. Para verificá-la, adicionamos o seguinte código em [ConfigFragment]:


// os valores inseridos
  private String urlServiceRest;

  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // verifica-se as entradas
    if (!pageValid()) {
      return;
    }
    // vamos iniciar uma tarefa — preparamos a espera
    beginWaiting(1);
  }

  // verificação dos dados inseridos
  private boolean pageValid() {
    // inicialmente, nenhuma mensagem de erro
    txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    // recuperando o IP e a porta do servidor
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    // verificando a validade
    try {
      URI uri = new URI(urlServiceRest);
      String host = uri.getHost();
      int port = uri.getPort();
      if (host == null || port == -1) {
        throw new Exception();
      }
    } catch (Exception ex) {
      // exibição da mensagem de erro
      txtMsgErreurUrlServiceRest.setVisibility(View.VISIBLE);
      // retorno para o UI
      return false;
    }
    // Tudo certo
    return true;
  }
  • linha 2: a entrada URL;
  • linhas 7-9: antes de fazer qualquer coisa, verificamos a validade dos dados inseridos;
  • linha 19: recuperamos o URL inserido e adicionamos a ele o prefixo [http://];
  • linha 22: tenta-se construir um objeto URI (Uniform Resource Identifier) com ele. Se o URL inserido estiver sintaticamente incorreto, ocorrerá uma exceção;
  • linhas 23-27: gera-se uma exceção se o URI estiver correto, mas, no entanto, houver [host==null] e [port==-1]. Esse é um caso possível;
  • linha 30: ocorreu uma exceção. Exibe-se a mensagem de erro;
  • linha 32: retorna-se [false] para indicar que a página é inválida;
  • linha 35: não ocorreram erros. Retornamos [true] para indicar que a página é válida;

Tarefa: crie os elementos acima.


Teste essa nova versão e verifique se os URL inválidos são devidamente sinalizados.

5.6.10.3. Exibição da lista de Arduinos

  

As diferentes visualizações precisarão exibir a lista dos Arduinos conectados. Para isso, definiremos diferentes classes e uma visualização XML:

  • um Arduino será representado pela classe [Arduino] [1];
  • a classe [CheckedArduino] [1] herda da classe [Arduino], à qual foi adicionada uma variável booleana para indicar se o Arduino foi selecionado ou não em uma lista;

A classe [Arduino] é a que já é utilizada pelo servidor e apresentada no parágrafo 5.4.2. É a seguinte:


package android.arduinos.entities;

import java.io.Serializable;

public class Arduino implements Serializable {
  // dados
  private String id;
  private String description;
  private String mac;
  private String ip;
  private int port;

// getters e setters
...
}
  • linha 7: [id] é o identificador do Arduino;
  • linha 8: sua descrição;
  • linha 9: seu endereço MAC;
  • linha 10: seu endereço IP;
  • linha 11: a porta na qual ele aguarda comandos;

Essa classe corresponde à sequência jSON recebida do servidor quando se solicita a lista dos Arduinos conectados:

A classe [CheckedArduino] herda da classe [Arduino]:


package android.arduinos.entities;

public class CheckedArduino extends Arduino {
    private static final long serialVersionUID = 1L;
    // é possível selecionar um Arduino
    private boolean isChecked;

    // construtor
    public CheckedArduino(Arduino arduino, boolean isChecked) {
        // pai
        super(arduino.getId(), arduino.getDescription(), arduino.getMac(), arduino.getIp(), arduino.getPort());
        // local
        this.isChecked = isChecked;
    }

    // getters e setters
    public boolean isChecked() {
        return isChecked;
    }

    public void setChecked(boolean isChecked) {
        this.isChecked = isChecked;
    }

}
  • linha 3: a classe [CheckedArduino] herda da classe [Arduino];
  • linha 6: adicionamos a ela um booleano que nos servirá para saber se, na lista de Arduinos exibida, um Arduino foi selecionado ou não;

Na classe [ConfigFragment], vamos simular a obtenção da lista de Arduinos conectados.

  

  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;
..
  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // verifica-se as entradas
    if (!pageValid()) {
      return;
    }
    // vamos iniciar uma tarefa — preparando a espera
    beginWaiting(1);
    // limpa-se a lista de Arduinos
    clearArduinos();
    // solicita-se a lista de Arduinos em segundo plano
    getArduinosInBackground();
  }

  private void getArduinosInBackground() {
   ...
  }

  // zerando a lista de Arduinos
  private void clearArduinos() {
    // criando uma lista vazia
    List<String> strings = new ArrayList<>();
    // exibe a lista
    listArduinos.setAdapter(new ArrayAdapter<String>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}
  • linha 2: o ListView que exibe os Arduinos conectados ao servidor;
  • linha 5: o método que solicita a lista dos Arduinos conectados;
  • linha 11: informamos à classe pai que vamos iniciar uma tarefa assíncrona;
  • linha 12: apaga-se a lista de Arduinos atualmente exibida;
  • linha 15: solicita-se, em segundo plano, a lista dos Arduinos conectados;
  • linhas 23-28: o método que limpa a lista de Arduinos atualmente exibida;

O método [getArduinosInBackground] é o seguinte:


  private void getArduinosInBackground() {
    // cria-se uma lista fictícia de Arduinos
    List<Arduino> arduinos = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
      arduinos.add(new Arduino("id" + i, "desc" + i, "mac" + i, "ip" + i, i));
    }
    // simula-se uma resposta do servidor
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // cancela-se a espera
    cancelWaitingTasks();
    // alteramos os botões
    initButtons();
    // processa-se a resposta
    consumeArduinosResponse(response);
}
  • linhas 3-6: cria-se uma lista de 20 Arduinos;
  • linhas 8-9: constrói-se a resposta do tipo [Response<List<Arduino>>] (parágrafo 5.4.2) que irá encapsular a lista de Arduinos criada;
  • linha 11: cancela-se a espera;
  • linha 13: os botões são repostos em seu estado inicial;
  • linha 15: a resposta é processada;

O método [consumeArduinosResponse] é o seguinte:


  // exibição da resposta
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // erro?
    if (response.getStatus() != 0) {
      // exibição
      showAlert(response.getMessages());
      // retorno à interface do usuário
      return;
    }
    // criando uma lista de [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // exibindo-os
    showArduinos(checkedArduinos);
}
  • linhas 4-11: verifica-se o código de erro da resposta enviada pelo servidor:
  • linha 4: se o código de erro for diferente de zero;
  • linha 6: exibe-se as mensagens armazenadas pelo servidor no campo [messages] da resposta;
  • linha 8: retorna-se à interface do usuário;
  • linhas 11-16: se não houver erros, exibe-se a lista de Arduinos recebida, após convertê-la para o tipo List<CheckedArduino>;

O método [showArduinos] é o seguinte:


  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // cria-se uma lista de strings a partir da lista de Arduinos
    List<String> strings = new ArrayList<>();
    for (CheckedArduino checkedArduino : checkedArduinos) {
      strings.add(checkedArduino.toString());
    }
    // ela é exibida
    listArduinos.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}

Tarefa: faça as alterações acima e execute seu projeto.


Você deve obter a seguinte visualização ao clicar no botão [Rafraîchir]:

Image

A entrada em [1] não é utilizada. Portanto, você pode inserir qualquer valor, desde que respeite o formato esperado.

5.6.10.4. Um modelo para exibir um Arduino

Por enquanto, os Arduinos conectados são exibidos na visualização [Config] da seguinte maneira:

Image

Agora, queremos exibi-los da seguinte forma:

Image

  • em [1], uma caixa de seleção que permitirá selecionar um Arduino. Essa caixa de seleção ficará oculta quando quisermos apresentar uma lista de Arduinos não selecionáveis;
  • em [2], o identificador do Arduino;
  • em [3], sua descrição;

O que se segue retoma conceitos desenvolvidos nos projetos [exemple-19] e [exemple-19B] do parágrafo 1.20. Revise-os, se necessário.

Primeiramente, criamos a visualização que exibirá um elemento da lista de Arduinos:

 

O código da visualização [listarduinos_item] acima é o seguinte:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/RelativeLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/wheat"
    android:orientation="vertical" >

    <CheckBox
        android:id="@+id/checkBoxArduino"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/txt_arduino_description" />

    <TextView
        android:id="@+id/TextView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_marginLeft="40dp"
        android:text="@string/txt_arduino_id" />

    <TextView
        android:id="@+id/txt_arduino_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/TextView1"
        android:text="@string/dummy"
        android:textColor="@color/blue" />

    <TextView
        android:id="@+id/TextView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignParentTop="true"
        android:layout_marginLeft="20dp"
        android:layout_toRightOf="@+id/txt_arduino_id"
        android:text="@string/txt_arduino_description" />

    <TextView
        android:id="@+id/txt_arduino_description"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/checkBoxArduino"
        android:layout_alignTop="@+id/TextView2"
        android:layout_toRightOf="@+id/TextView2"
        android:text="@string/dummy"
        android:textColor="@color/blue" />

</RelativeLayout>
  • linhas 9-15: a caixa de seleção;
  • linhas 17-23: o texto [Id : ];
  • linhas 25-33: o ID do Arduino será inserido aqui;
  • linhas 35-43: o texto [Description : ];
  • linhas 45-53: a descrição do Arduino será inserida aqui;

Esta visualização utiliza textos (linhas 23, 32, 43) definidos em [res / values / strings.xml]:


    <string name="dummy">XXXXX</string>

    <!--  listarduinos_item -->
    <string name="txt_arduino_id">Id : </string>
<string name="txt_arduino_description">Description : </string>

A visualização também utiliza uma cor (linhas 33, 53) definida em [res / values / colors.xml]:


<?xml version="1.0" encoding="utf-8"?>
<resources>

    <color name="red">#FF0000</color>
    <color name="blue">#0000FF</color>
    <color name="wheat">#FFEFD5</color>
    <color name="floral_white">#FFFAF0</color>

</resources>

O gerenciador de exibição de um item da lista de Arduinos

  

A classe [ListArduinosAdapter] é a classe chamada pela [ListView] para exibir cada um dos itens da lista de Arduinos. Seu código é o seguinte:


package istia.st.android.vues;

import istia.st.android.R;
...

public class ListArduinosAdapter extends ArrayAdapter<CheckedArduino> {

    // a tabela dos Arduinos
    private List<CheckedArduino> arduinos;
    // o contexto de execução
    private Context context;
    // o ID do layout de exibição de uma linha da lista de Arduinos
    private int layoutResourceId;
    // a linha contém ou não uma caixa de seleção
    private Boolean selectable;

    // construtor
    public ListArduinosAdapter(Context context, int layoutResourceId, List<CheckedArduino> arduinos, Boolean selectable) {
        // pai
        super(context, layoutResourceId, arduinos);
        // as informações são armazenadas
        this.arduinos = arduinos;
        this.context = context;
        this.layoutResourceId = layoutResourceId;
        this.selectable = selectable;
    }

    @Override
    public View getView(final int position, View convertView, ViewGroup parent) {
...
    }
}
  • linha 18: o construtor da classe aceita quatro parâmetros: a atividade em execução, o identificador da visualização a ser exibida para cada elemento da fonte de dados, a fonte de dados que alimenta a lista e um valor booleano que indica se a caixa de seleção associada a cada Arduino deve ser exibida ou não;
  • linhas 8-15: essas quatro informações são armazenadas localmente;

Linha 29: o método [getView] é responsável por gerar a visualização nº [position] no [ListView] e por gerenciar seus eventos. Seu código é o seguinte:


@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // o Arduino atual
        final CheckedArduino arduino = arduinos.get(position);
        // cria-se a linha atual
        View row = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, parent, false);
        // recuperam-se as referências nos [TextView]
        TextView txtArduinoId = (TextView) row.findViewById(R.id.txt_arduino_id);
        TextView txtArduinoDesc = (TextView) row.findViewById(R.id.txt_arduino_description);
        // preenche-se a linha
        txtArduinoId.setText(arduino.getId());
        txtArduinoDesc.setText(arduino.getDescription());
        // o CheckBox nem sempre está visível
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // atribui-se o valor a ele
            ck.setChecked(arduino.isChecked());
            // gerencia-se o clique
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
        }
        // retornamos a linha
        return row;
    }
  • linha 2: o primeiro parâmetro é a posição no [ListView] da linha a ser criada. É também a posição na lista de Arduinos armazenada localmente;
  • linha 4: obtém-se uma referência ao Arduino que será associado à linha criada;
  • linha 6: a linha atual é criada a partir da visualização [listarduinos_item.xml];
  • linhas 8-9: as referências aos dois [TextView] são recuperadas;
  • linhas 11-12: os dois [TextView] recebem seus valores;
  • linha 14: recupera-se uma referência à caixa de seleção;
  • linha 15: ela é tornada visível ou não, dependendo do valor [selectable] passado inicialmente ao construtor;
  • linha 16: se a caixa de seleção estiver presente;
  • linha 18: atribui-se a ela o valor [isChecked] do Arduino atual;
  • linhas 20-26: gerencia-se o clique na caixa de seleção;
  • linha 23: o valor da caixa de seleção é armazenado no Arduino atual;

Gerenciamento da lista de Arduinos

A exibição da lista de Arduinos é, por enquanto, gerenciada por dois métodos da classe [ConfigFragment]:

  • [clearArduinos]: que exibe uma lista vazia;
  • [showArduinos]: que exibe a lista retornada pelo servidor;

Esses dois métodos evoluem da seguinte forma:


  // a lista de Arduinos é zerada
  private void clearArduinos() {
    // exibe-se uma lista vazia
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }

  // exibição da lista de Arduinos
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // exibe os Arduinos
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
}

Tarefa: Faça essas alterações e teste o novo aplicativo.


Image

5.6.10.5. A sessão

A sessão é onde colocamos as informações compartilhadas pelos fragmentos e pela atividade. Todos os fragmentos precisam exibir a lista de Arduinos conectados. Portanto, uma primeira versão da sessão será a seguinte:


package client.android.architecture.custom;

import client.android.activity.CheckedArduino;
import client.android.architecture.core.AbstractSession;

import java.util.ArrayList;
import java.util.List;

public class Session extends AbstractSession {
  // dados a serem compartilhados entre os próprios fragmentos e entre fragmentos e a atividade
  // os elementos que não podem ser serializados em jSON devem ter a anotação @JsonIgnore
  // não se esqueça dos getters e setters necessários para a serialização/desserialização em jSON

  // a lista de Arduinos
  private List<CheckedArduino> checkedArduinos = new ArrayList<>();

  // getters e setters
...
}

Tarefa: crie a classe [Session] mencionada anteriormente.


A criação dessa sessão nos leva a modificar o código já escrito da seguinte maneira:


  // exibição da resposta
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // erro?
    if (response.getStatus() != 0) {
      // exibição
      showAlert(response.getMessages());
      // cancelar
      doAnnuler();
      // retorno à interface do usuário
      return;
    }
    // criamos uma lista de [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // ela é carregada na sessão
    session.setCheckedArduinos(checkedArduinos);
    // exibimos os dados
    showArduinos(checkedArduinos);
    // cancela-se a espera
    cancelWaitingTasks();
}
  • linha 18: a lista de Arduinos criada pelas linhas anteriores é inserida na sessão;

5.6.10.6. Gerenciamento do estado do fragmento

Durante uma rotação do dispositivo, os componentes visuais da visualização são exibidos (por padrão) no estado em que se encontravam no momento do projeto da visualização:

  • o [ListView] contém os elementos que o designer inseriu nele;
  • a mensagem de erro está no estado visível ou não visível em que o designer a colocou;

Os estados dos componentes visuais no momento do projeto podem ser adequados ou não ao restaurar um fragmento. O que ocorre neste caso?

  • o [ListView] deve exibir a lista dos Arduinos conectados. O valor do [ListView] no momento da concepção, portanto, não pode ser utilizado;
  • o [TextView] da mensagem de erro deve ser restaurado no estado em que se encontrava no momento do salvamento, independentemente de estar visível ou não. Seu valor no projeto pode não ser adequado para esses dois casos;

Portanto, precisamos salvar o estado desses dois componentes ao salvar o estado do fragmento:

  • a lista dos Arduinos conectados;
  • a visibilidade (exibida/oculta) da mensagem de erro ao inserir o URL do serviço web / jSON;

Como a lista de Arduinos está presente na sessão, ela será salva automaticamente. A visibilidade da mensagem de erro será armazenada na seguinte classe [ConfigFragmentState]:

  

package client.android.fragments.state;

import client.android.architecture.custom.CoreState;

public class ConfigFragmentState extends CoreState {

  // visibilidade da mensagem de erro
  private boolean txtMsgErreurUrlServiceRestVisible;

  // getters e setters
...
}

Tarefa: crie a classe [ConfigFragmentState] acima.


Para reproduzir corretamente os estados dos fragmentos, é necessário que seus métodos [getNumView] e [saveFragment] sejam modificados. Por exemplo, o método do fragmento [BlinkFragment] é atualmente o seguinte:


  @Override
  public CoreState saveFragment() {
    // é preciso salvar o fragmento
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // senão houver nada para salvar, execute [return new CoreState();] e exclua a classe [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // é necessário retornar o número do fragmento na tabela de fragmentos gerenciados pela atividade (cf. MainActivity)
    return 0;
}

Se nada for feito, o relatório gerado na linha 6 será salvo no elemento 0 (linha 13) da matriz CoreState[] coreStates da classe [AbstractSession] (linha 5 abaixo):


public class AbstractSession implements ISession {
  ...

  // estado das visualizações
  private CoreState[] coreStates = new CoreState[0];
...

No entanto, ele deve ser gravado no elemento correspondente ao número do fragmento [BlinkFragment] na tabela de fragmentos definida na classe [MainActivity] (linha 9 abaixo):


@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  ...

  @Override
  protected AbstractFragment[] getFragments() {
    return new AbstractFragment[]{new ConfigFragment_(), new BlinkFragment_(), new PinReadFragment_(), new PinWriteFragment_(), new CommandsFragment_()};
  }


Os números dos fragmentos foram definidos na interface [IMainActivity]:


public interface IMainActivity extends IDao {

  ...

  // números das visualizações
  int VUE_CONFIG = 0;
  int VUE_BLINK = 1;
  int VUE_PINREAD = 2;
  int VUE_PINWRITE = 3;
  int VUE_COMMANDS = 4;
}

Por fim, o estado do fragmento [BlinkFragment] será gerenciado corretamente se for escrito:


  @Override
  public CoreState saveFragment() {
    // é necessário salvar o fragmento
    DummyFragmentState state=new DummyFragmentState();
    // ...
    return state;
    // senão houver nada para salvar, execute [return new CoreState();] e exclua a classe [DummyFragmentState]
  }

  @Override
  protected int getNumView() {
    // é necessário retornar o número do fragmento na tabela de fragmentos gerenciados pela atividade (cf. MainActivity)
    return IMainActivity.VUE_BLINK;
}
  • linha 14: retorna-se o número do fragmento [BlinkFragment] na tabela de fragmentos gerenciados pela atividade;

Além disso, a classe [CoreState], que é a classe-pai dos estados dos fragmentos, é, por enquanto, a seguinte (ver parágrafo 5.6.7.2):


package client.android.architecture.custom;

import client.android.architecture.core.MenuItemState;
import client.android.fragments.state.*;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes({
  @JsonSubTypes.Type(value = ConfigFragmentState.class),
  @JsonSubTypes.Type(value = BlinkFragmentState.class),
  @JsonSubTypes.Type(value = PinReadFragmentState.class),
  @JsonSubTypes.Type(value = PinWriteFragmentState.class),
  @JsonSubTypes.Type(value = CommandsFragmentState.class)}
)
public class CoreState {
  // fragmento visitado ou não
  protected boolean hasBeenVisited = false;
  // estado do eventual menu do fragmento
  protected MenuItemState[] menuOptionsState;

  // getters e setters
....
}
  • linhas 12-16: a classe [DummyFragmentState] não consta na lista de classes filhas da classe [CoreState]. No entanto, o método [saveFragment] da classe [BlinkFragment] atualmente retorna um tipo [ DummyFragmentState]. Se deixarmos as coisas como estão, a serialização/desserialização da sessão falhará e a sessão não será restaurada, levando a uma falha no aplicativo;

O método [saveFragment] do fragmento [BlinkFragment] deve ser reescrito da seguinte forma:


  @Override
  public CoreState saveFragment() {
    // é necessário salvar o fragmento
    BlinkFragmentState state=new BlinkFragmentState();
    // ...
    return state;
    // senão houver nada para salvar, execute [return new CoreState();] e exclua a classe [DummyFragmentState]
}

Tarefa: em cada um dos fragmentos, modifique o método [getNumView] para que ele retorne o número do fragmento e o método [saveFragment] para que ele retorne uma instância da classe de estado do fragmento (conforme descrito acima).


5.6.10.7. Gerenciamento do ciclo de vida do fragmento

Aqui, estamos interessados no ciclo de vida do fragmento [ConfigFragment], especialmente nos quatro métodos:

  • [saveFragment]: deve salvar o estado do fragmento para que ele possa ser restaurado posteriormente;
  • [initFragment]: que deve inicializar determinados campos do fragmento, se necessário. Esse método é chamado no início da aplicação e sempre que ocorre uma rotação do dispositivo. Mais precisamente, ele é chamado quando o fragmento se torna visível após um dos dois eventos anteriores;
  • [initView]: que deve inicializar certos componentes da visualização, se necessário. Esse método é chamado sempre que [initFragment] for chamado e quando a visualização precisar ser regenerada porque o fragmento, em determinado momento, saiu da adjacência do fragmento exibido. Assim como anteriormente, ele é chamado quando o fragmento se torna visível após um desses eventos;
  • [updateOnRestore]: que é executado após os dois métodos anteriores quando ocorre uma rotação do dispositivo, mas também quando há navegação. Sua função é restaurar o estado anterior do fragmento;

Esses métodos serão os seguintes:


// adaptador da lista de Arduinos
  private ListArduinosAdapter adapterListArduinos;

...
  // gerenciamento do ciclo de vida do fragmento -------------------------------------

  @Override
  public CoreState saveFragment() {
    ConfigFragmentState state = new ConfigFragmentState();
    state.setTxtMsgErreurUrlServiceRestVisible(txtMsgErreurUrlServiceRest.getVisibility() == View.VISIBLE);
    return state;
  }

  @Override
  protected void initFragment(CoreState previousState) {
    // adaptador listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);

  }

  @Override
  protected void initView(CoreState previousState) {
    // ligação entre listview e adaptador
    listArduinos.setAdapter(adapterListArduinos);
    // Primeira visita?
    if (previousState == null) {
      // ListView vazio — criado por [initFragment]
      // mensagem de erro oculta
      txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    } else {
      // a mensagem de erro volta a ficar visível
      ConfigFragmentState state = (ConfigFragmentState) previousState;
      txtMsgErreurUrlServiceRest.setVisibility(state.isTxtMsgErreurUrlServiceRestVisible() ? View.VISIBLE : View.INVISIBLE);
    }
  }


  @Override
  protected void updateOnSubmit(CoreState previousState) {

  }

  @Override
  protected void updateOnRestore(CoreState previousState) {
  }


  @Override
  protected void notifyEndOfUpdates() {
    // botões
    initButtons();
}
  • linha 2: o adaptador do ListView dos Arduinos. É uma variável global, pois é utilizada em diferentes métodos;
  • linhas 7-12: o método [saveFragment] salva, em um tipo [ConfigFragmentState], a visibilidade do TextView txtMsgErreurUrlServiceRestVisible (linha 10);
  • linhas 14-19: o método [initFragment] inicializa o adaptador da linha 2 com a lista de Arduinos presentes na sessão (linha 17). Vale lembrar que a função do [initFragment] é inicializar os campos do fragmento. Aqui, essa inicialização deve ser feita em todos os casos, seja na primeira visita (previousState==null) ou não;
  • linha 17: vemos que o adaptador está vinculado à fonte de dados [session.getCheckedArduinos]. Esta não deve ter o valor null. Por esse motivo, o campo [session.checkedArduinos] é inicializado com uma lista vazia na sessão:

  // lista de Arduinos
private List<CheckedArduino> checkedArduinos = new ArrayList<>();
  • linhas 21-35: o método [initView] tem a função de inicializar certos componentes da interface visual, especialmente aqueles cujo valor não é mantido durante a rotação do dispositivo;
  • linha 24: o ListView dos Arduinos está associado ao adaptador da linha 2;
  • linhas 28-32: distingue-se a primeira visita das demais;
  • linha 29: na primeira visita, deve-se exibir um [ListView] vazio. É o que ocorre, pois, na primeira visita, o adaptador do [ListView] foi associado a uma lista vazia (linha 17);
  • linha 31: a mensagem de erro está oculta;
  • linhas 32-36: o caso em que não se trata da primeira visita;
  • o [ListView] já está no estado correto desde a linha 24. Não há mais nada a fazer;
  • linhas 34-35: restaura-se a mensagem de erro ao estado em que se encontrava no momento do último salvamento do fragmento;
  • linhas 31-36: o método [updateOnRestore] deve restaurar o fragmento ao seu estado inicial. Chega-se ao método [updateOnRestore] de duas maneiras:
    • ou porque houve rotação do dispositivo. Nesse caso, todas as inicializações necessárias já foram realizadas no [initView];
    • ou porque se está navegando de uma aba para a aba [Config]. Se o fragmento [Config] saiu da vizinhança dos fragmentos exibidos desde que foi abandonado, o método [initView] foi executado e o fragmento já está no estado desejado. Se o fragmento [Config] não saiu da vizinhança dos fragmentos exibidos desde que foi abandonado, seus componentes visuais não alteraram de estado e não há nada a ser feito;

Vemos que o método [updateOnRestore] não tem nada a fazer. Às vezes é assim, outras vezes não. A diferença está no método [updateOnSubmit]: se esse método realizar alguma ação que torne desnecessárias certas inicializações feitas no [initView], então essas inicializações deveriam ser feitas no método [updateOnRestore]. Tomemos o exemplo de um botão de opção com três valores: V1, V2 e V3. Talvez, no caso de uma navegação associada a uma ação [SUBMIT], o botão de opção marcado deva ser sempre aquele com o valor V1. Nesse caso, restaurar o valor do botão de opção no método [initView] é desnecessário, pois, no caso de um [SUBMIT], esse valor será substituído pelo fornecido pelo método [updateOnSubmit]. Portanto, é preferível transferir essa restauração para o método [updateOnRestore], a fim de evitar a execução de uma operação desnecessária em certos casos.

  • linhas 48-52: o método [notifyEndOfUpdates] é executado após todos os anteriores;
  • linha 51: os botões são colocados em seu estado inicial: botão [Rafraîchir] exibido, botão [Annuler] oculto:

Tarefa: adicione o código anterior ao [ConfigFragment] e, em seguida, execute o aplicativo. Observe que, ao girar o dispositivo, a aba [Config] mantém seu estado (mensagem de erro, lista de Arduinos). Verifique se o mesmo ocorre ao navegar simplesmente da aba [config] para a aba [Commands] e, em seguida, para a aba [Config]. Nesse último caso, se você tiver mantido no [IMainActivity] uma adjacência de fragmentos igual a 1, então a exibição do fragmento [ConfigFragment] é destruída ao passar para a aba [Commands] e, em seguida, recriada ao retornar à aba [Config]. Durante os testes, examine os logs.


5.6.10.8. Melhoria do código

O código do fragmento [ConfigFragment] pode ser aprimorado. Por exemplo, escrevemos:


// adaptador da lista de Arduinos
  private ListArduinosAdapter adapterListArduinos;

...

  // exibição da lista de Arduinos
  private void showArduinos(List<CheckedArduino> checkedArduinos) {
    // exibindo os Arduinos
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, checkedArduinos, false);
    listArduinos.setAdapter(adapter);
  }

  // zerar a lista de Arduinos
  private void clearArduinos() {
    // exibe uma lista vazia
    ListArduinosAdapter adapter = new ListArduinosAdapter(getActivity(), R.layout.listarduinos_item, new ArrayList<CheckedArduino>(), false);
    listArduinos.setAdapter(adapter);
  }
  • percebe-se que, nas linhas 9 e 16, utilizamos uma variável local desvinculada do campo da linha 2, embora seja exatamente a mesma entidade que queremos manipular;

Modificamos o código da seguinte maneira:


  // adaptador da lista de Arduinos
  private ListArduinosAdapter adapterListArduinos;

  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
  ...
  }

  private void getArduinosInBackground() {
 ...
    // a lista é consumida
    consumeArduinosResponse(response);
  }

  // exibição da resposta
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // erro?
    if (response.getStatus() != 0) {
      // exibição
      showAlert(response.getMessages());
      // cancelar
      doAnnuler();
      // retorno à interface do usuário
      return;
    }
    // cria-se uma lista de [CheckedArduino]
    List<CheckedArduino> checkedArduinos = session.getCheckedArduinos();
    checkedArduinos.clear();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // exibindo-as
    adapterListArduinos.notifyDataSetChanged();
    // cancela-se a espera
    cancelWaitingTasks();
}
  
  @Override
  protected void initFragment(CoreState previousState) {
    // adaptador listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), false);

  }

  @Override
  protected void initView(CoreState previousState) {
    // ligação entre a lista de visualização e o adaptador
    listArduinos.setAdapter(adapterListArduinos);
    ...
}
  • quando o método da linha 5 é executado, o ciclo de vida do fragmento já foi executado. Portanto:
    • o adaptador da linha 2 foi associado à sua fonte de dados (linha 41);
    • o [ListView] dos Arduinos conectados foi vinculado a esse adaptador (linha 48);

Quando quisermos alterar a exibição do [ListView], é preciso fazer duas coisas:

  • alterar o conteúdo da fonte de dados [session.checkedArduinos];
  • notificar essa alteração ao adaptador por meio da instrução [adapterListArduinos.notifyDataSetChanged()];

Trata-se, de fato, de alterar o conteúdo da fonte de dados e não a própria fonte de dados. Se alterarmos a própria fonte de dados, a operação [adapterListArduinos.notifyDataSetChanged()] continuará exibindo a fonte de dados antiga. Seria então necessário associar o adaptador à nova fonte de dados.

O código é o seguinte:

  • linha 27: recuperamos a fonte de dados;
  • linha 28: esvaziamos a fonte de dados. Por esse motivo, removemos o método [clearArduinos];
  • linhas 29-31: nessa lista, agora vazia, adicionamos novos elementos;
  • linha 33: instruímos o adaptador a atualizar-se. Isso atualizará a exibição do [ListView] associado;

Tarefa: faça essas alterações e verifique se seu aplicativo continua funcionando.


5.6.11. Comunicação entre visualizações

Para verificar a comunicação entre as visualizações, faremos com que todas as outras visualizações exibam a lista de Arduinos obtida pela visualização [Config]. Vamos começar pela visualização [blink.xml]. Enquanto antes ela não exibia nada, agora ela exibirá a lista de Arduinos conectados:

Image

 

O código XML da visualização [blink.xml] será o seguinte:


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:id="@+id/scrollView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content">

  <?xml version="1.0" encoding="utf-8"?>
  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:tools="http://schemas.android.com/tools"
                  android:id="@+id/RelativeLayout1"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">

    <TextView
      android:id="@+id/txt_arduinos"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_marginTop="150dp"
      android:text="@string/titre_list_arduinos"
      android:textColor="@color/blue"
      android:textSize="20sp" />

    <ListView
      android:id="@+id/ListViewArduinos"
      android:layout_width="match_parent"
      android:layout_height="200dp"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/txt_arduinos"
      android:layout_marginTop="30dp"
      android:background="@color/wheat">
    </ListView>

  </RelativeLayout>
</ScrollView>

Esse código foi copiado diretamente da visualização [config.xml]. Apenas a margem superior da linha 19 foi alterada.


Tarefa: duplique este código nas visualizações [commands.xml, pinread.xml, pinwrite.xml].


O código do fragmento [BlinkFragment] associado à visualização [blink.xml] também sofre alterações:

  

  // componentes visuais
  @ViewById(R.id.ListViewArduinos)
  protected ListView listArduinos;

  // adaptador da lista de Arduinos
  private ListArduinosAdapter adapterListArduinos;
...

  // métodos impostos pela classe pai -------------------------------------------------------

...
  @Override
  protected void initFragment(CoreState previousState) {
    // adaptador listArduinos
    adapterListArduinos = new ListArduinosAdapter(activity, R.layout.listarduinos_item, session.getCheckedArduinos(), true);

  }

  @Override
  protected void initView(CoreState previousState) {
    // ligação entre listview e adaptador
    listArduinos.setAdapter(adapterListArduinos);
  }
...
  • linhas 2-3: o componente [ListView] dos Arduinos conectados;
  • linha 6: o adaptador desse [ListView];
  • linhas 12-23: o código dos métodos [initFragment] e [initView] é o mesmo já utilizado para o fragmento [ConfigFragment];
  • linha 15: quando o fragmento precisa ser reinicializado, reinicializa-se o adaptador da linha 2, associando-o à lista de Arduinos armazenada na sessão. O último parâmetro [true] do construtor [ListArduinosAdapter] significa que se deseja que apareça uma caixa de seleção ao lado de cada Arduino;
  • linha 22: quando a exibição do fragmento precisa ser reinicializada, associamos o [ListView] dos Arduinos conectados ao adaptador da linha 6;

Tarefa: Duplique esse código nos outros fragmentos [CommandsFragment, PinReadFragment, PinWriteFragment]. Execute o aplicativo e observe agora que cada guia exibe a lista dos Arduinos conectados. Observe também que, se você marcar os Arduinos em uma guia e navegar para outra guia, eles continuarão marcados nessa última.


Observação: A explicação para a manutenção dos Arduinos marcados é a seguinte. A classe [ListArduinosAdapter] foi apresentada no parágrafo 5.6.10.4. O código relacionado à caixa de seleção é o seguinte:


        // o Arduino atual
        final CheckedArduino arduino = arduinos.get(position);
...
        // o CheckBox nem sempre está visível
        CheckBox ck = (CheckBox) row.findViewById(R.id.checkBoxArduino);
        ck.setVisibility(selectable ? View.VISIBLE : View.INVISIBLE);
        if (selectable) {
            // atribui-se um valor a ele
            ck.setChecked(arduino.isChecked());
            // gerenciamos o clique
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
}
  • linhas 11-15: se, na aba X, for marcada uma caixa de seleção, a propriedade [checked] do Arduino da linha 2 é alterada para true (linha 14);
  • ao passar para a aba Y, o valor [ListView] dos Arduinos dessa aba é exibido. Na linha 9, vemos que, se a propriedade [checked] do Arduino da linha 2 for alterada para true, a caixa de seleção [ck] da linha 5 será marcada;

5.6.12. A camada [DAO]

Observação: para esta parte, revise a implementação da camada [DAO] no projeto [exemple-16B] (ver parágrafo 2.8.3).

Por enquanto, geramos manualmente a lista dos Arduinos conectados. Agora, vamos solicitá-la ao servidor web / jSON. Para isso, vamos construir a camada [DAO]:

  

5.6.12.1. A interface IDao

A interface [IDao] da camada [DAO] será a seguinte:


package client.android.dao.service;

import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import rx.Observable;

import java.util.List;

public interface IDao {
  // URL do serviço web
  void setUrlServiceWebJson(String url);

  // usuário
  void setUser(String user, String mdp);

  // tempo limite do cliente
  void setTimeout(int timeout);

  // autenticação básica
  void setBasicAuthentification(boolean isBasicAuthentificationNeeded);

  // modo de depuração
  void setDebugMode(boolean isDebugEnabled);

  // tempo de espera do cliente, em milissegundos, antes da solicitação
  void setDelay(int delay);

  // específico ----------------------------------------
  // lista de Arduinos
  Observable<Response<List<Arduino>>> getArduinos();
}
  • linhas 11-26: essas linhas já estão presentes na interface [IDao] do projeto modelo [client-android-skel];
  • linha 30: o método [getArduinos] permite obter a lista dos Arduinos conectados na forma de um observável do tipo Observable<[Response<List<Arduino>>>];

Vale lembrar que [Response<T>] é o tipo de todas as respostas enviadas pelo servidor na forma de uma string jSON:


package client.android.dao.entities;

import java.util.List;

public class Response<T> {

    // ----------------- propriedades
    // status da operação
    private int status;
    // eventuais mensagens de erro
    private List<String> messages;
    // o corpo da resposta
    private T body;

    // construtores
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // getters e setters
...
}

5.6.12.2. A interface [WebClient]

  

A interface [WebClient] é uma interface cuja implementação é fornecida pela biblioteca AA. Essa interface será a seguinte:


package client.android.dao.service;

import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import org.androidannotations.rest.spring.annotations.Get;
import org.androidannotations.rest.spring.annotations.Path;
import org.androidannotations.rest.spring.annotations.Rest;
import org.androidannotations.rest.spring.api.RestClientRootUrl;
import org.androidannotations.rest.spring.api.RestClientSupport;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Rest(converters = {MappingJackson2HttpMessageConverter.class})
public interface WebClient extends RestClientRootUrl, RestClientSupport {

  // RestTemplate
  void setRestTemplate(RestTemplate restTemplate);

  // específico --------------------------------------
  // lista de Arduinos
  @Get("/arduinos")
  Response<List<Arduino>> getArduinos();
}
  • linhas 15-19: essas linhas já estão presentes por padrão na interface [WebClient] do projeto modelo [client-android-skel];
  • linha 23: o URL do servidor que permite obter a lista de Arduinos por meio de uma operação GET. Vale lembrar que este URL é medido em relação ao URL raiz [RestClientRootUrl] da linha 16;
  • linha 24: o servidor retorna a cadeia jSON do tipo [Response<List<Arduino>>]. Essa cadeia jSON é automaticamente deserializada para o tipo [Response<List<Arduino>>] por meio do conversor jSON [MappingJackson2HttpMessageConverter] da linha 15;

5.6.12.3. A classe [Dao]

A classe [Dao] implementa a interface [IDao] da seguinte maneira:


package client.android.dao.service;

import android.util.Log;
import client.android.dao.entities.Arduino;
import client.android.dao.entities.Response;
import org.androidannotations.annotations.AfterInject;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.EBean;
import org.androidannotations.rest.spring.annotations.RestService;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

@EBean(scope = EBean.Scope.Singleton)
public class Dao extends AbstractDao implements IDao {

  // cliente do serviço web
  @RestService
  protected WebClient webClient;
  // segurança
  @Bean
  protected MyAuthInterceptor authInterceptor;
  // o RestTemplate
  private RestTemplate restTemplate;
  // fábrica do RestTemplate
  private SimpleClientHttpRequestFactory factory;

  @AfterInject
  public void afterInject() {
    // registro
    Log.d(className, "afterInject");
    // é construído o restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // configura-se o conversor jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // configura-se o restTemplate do cliente web
    webClient.setRestTemplate(restTemplate);
  }

  @Override
  public void setUrlServiceWebJson(String url) {
    // define-se o URL do serviço web
    webClient.setRootUrl(url);
  }

  @Override
  public void setUser(String user, String mdp) {
    // registra-se o usuário no interceptador
    authInterceptor.setUser(user, mdp);
  }

  @Override
  public void setTimeout(int timeout) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setTimeout thread=%s, timeout=%s", Thread.currentThread().getName(), timeout));
    }
    // configuração de fábrica
    factory.setReadTimeout(timeout);
    factory.setConnectTimeout(timeout);
  }

  @Override
  public void setBasicAuthentification(boolean isBasicAuthentificationNeeded) {
    if (isDebugEnabled) {
      Log.d(className, String.format("setBasicAuthentification thread=%s, isBasicAuthentificationNeeded=%s", Thread.currentThread().getName(), isBasicAuthentificationNeeded));
    }
    // interceptador de autenticação?
    if (isBasicAuthentificationNeeded) {
      // adiciona-se o interceptador de autenticação
      List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();
      interceptors.add(authInterceptor);
      restTemplate.setInterceptors(interceptors);
    }
  }

  // métodos privados -------------------------------------------------
  private void log(String message) {
    if (isDebugEnabled) {
      Log.d(className, message);
    }
  }

  // implementação específica IDao -----------------------------------------------

  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    // execução no cliente web
    return getResponse(new IRequest<Response<List<Arduino>>>() {
      @Override
      public Response<List<Arduino>> getResponse() {
        return webClient.getArduinos();
      }
    });
  }
}
  • linhas 19-87: essas linhas são básicas na classe [Dao] do projeto [client-android-skel];
  • linhas 91-100: implementação do método [getArduinos];
  • linha 94: chama-se o método [getResponse] da classe pai. O único parâmetro desse método é uma instância da interface [IRequest<T>];
  • linhas 95-99: o único método da interface [IRequest<T>] é o método [T getResponse()];
  • linha 94: o tipo T de [IRequest<T>] deve ser o tipo T do resultado Observable<T> do método da linha 92; portanto, neste caso, um tipo [Response<List<Arduino>>];
  • linha 97: o método [IRequest.getResponse()] delega a tarefa ao método [webClient.getArduinos()] que apresentamos. [webClient], definido na linha 24, é instanciado pela biblioteca AA e é uma instância da interface [WebClient] que apresentamos;

5.6.13. A atividade [MainActivity]

  

Já apresentamos a atividade [MainActivity] no parágrafo 5.6.8. Ela estende a classe [AbstractActivity] e, por isso, implementa a interface [IMainActivity], que, por sua vez, estende a interface [IDao]. Sempre que se adiciona um método à interface [IDao], é necessário implementá-lo na classe [MainActivity]. O método [IDao.getArduinos] adicionado à interface [IDao] será implementado da seguinte maneira na classe [MainActivity]:


...
@EActivity
@OptionsMenu(R.menu.menu_main)
public class MainActivity extends AbstractActivity {

  // camada [DAO]
  @Bean(Dao.class)
  protected IDao dao;
  // sessão
  private Session session;

...

  // implementação IDao -----------------------------------------
  @Override
  public Observable<Response<List<Arduino>>> getArduinos() {
    return dao.getArduinos();
  }
}
  • linhas 15-18: o método [getArduinos] é implementado delegando a tarefa à classe [Dao], que acabamos de apresentar e à qual há uma referência na linha 8;

5.6.14. O fragmento [ConfigFragment] revisitado

Na classe [ConfigFragment], o código executado ao clicar no botão [Rafraîchir] é, por enquanto, o seguinte:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    ...
    // solicita-se a lista de Arduinos em segundo plano
    getArduinosInBackground();
  }

  private void getArduinosInBackground() {
    // cria-se uma lista fictícia de Arduinos
    List<Arduino> arduinos = new ArrayList<>();
    for (int i = 0; i < 20; i++) {
      arduinos.add(new Arduino("id" + i, "desc" + i, "mac" + i, "ip" + i, i));
    }
    // simula-se uma resposta do servidor
    Response<List<Arduino>> response = new Response<>();
    response.setBody(arduinos);
    // ela é processada
    consumeArduinosResponse(response);
  }

  // exibição da resposta
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    ...
}

Precisamos reescrever as linhas 10 a 16, que geravam diretamente uma resposta do tipo [Response<List<Arduino>>]. Agora, precisamos solicitar essa lista à camada [DAO] por meio da atividade. O código passa a ser o seguinte:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // verifica-se os dados inseridos
    if (!pageValid()) {
      return;
    }
    // armazenamos a entrada
    mainActivity.setUrlServiceWebJson(urlServiceRest);
    // prepara-se a espera
    beginWaiting(1);
    // executa-se a tarefa assíncrona
    executeInBackground(mainActivity.getArduinos(), new Action1<Response<List<Arduino>>>() {

      @Override
      public void call(Response<List<Arduino>> response) {
        // processa-se a resposta
        consumeArduinosResponse(response);
      }
    });
}
  • linha 8: o URL, raiz do serviço web / jSON inserido pelo usuário, é passado para a camada [DAO] por meio da atividade. Essa será a raiz URL da interface [WebClient] (ver parágrafo 5.6.12.2);
  • linha 10: avisa-se a classe pai de que será iniciada uma tarefa assíncrona;
  • linhas 12-19: inicialização da tarefa assíncrona que retornará a lista dos Arduinos conectados ao servidor;
  • linha 12: chamada do método [executeInBackground] da classe pai. Esse método espera dois parâmetros:
    • linha 12: o processo a ser observado. Esse processo é fornecido aqui pelo método [mainActivity.getArduinos()];
    • linhas 12-19: uma instância da interface [Action1<T>], em que o tipo T é o tipo fornecido pelo processo, neste caso um tipo [Response<List<Arduino>>];
  • linhas 14-18: o método chamado quando a tarefa assíncrona retorna seu resultado do tipo [Response<List<Arduino>>];
  • linha 17: a resposta recebida é passada para o método [consumeArduinosResponse] já definido;

Tarefa: Inicie o servidor conforme indicado no parágrafo 5.4. Conecte um ou mais Arduinos ao PC no qual o servidor foi iniciado. Em seguida, inicie o cliente Android e verifique se consegue obter a lista dos Arduinos conectados. Observe os logs.


Image

  • digite o endereço URL indicado em [1]. Esse é um dos endereços IP do seu servidor;
  • clique no botão [2];
  • você deverá obter a lista dos Arduinos conectados em [3];

Verifique se essa lista também aparece nas outras abas.

5.7. Tarefa a ser realizada


Seguindo o mesmo procedimento que acabou de ser feito para a visualização [Config], crie e teste sucessivamente as outras quatro visualizações do aplicativo: [Blink], [PinRead], [PinWrite] e [Commands].


As visualizações a serem criadas foram apresentadas no parágrafo 5.5.

Para cada vista, é necessário:

  • desenhar a vista XML (ver parágrafo 5.6.9);
  • construir o fragmento associado (ver parágrafo 5.6.10);
  • adicionar um método à interface [WebClient] (ver parágrafo 5.6.12.2);
  • adicionar um método à interface [IDao] (ver parágrafo 5.6.12.2);
  • adicionar um método à classe [Dao] (ver parágrafo 5.6.12.3);
  • adicionar um método à atividade [MainActivity] (ver parágrafo 5.6.13);
  • escrever os manipuladores de eventos do fragmento (ver parágrafo 5.6.14);
  • testar e observar os logs;

Nota 1: o exemplo a ser seguido é o projeto [Exemple-16B] do curso (ver parágrafo 2.8.3).

Nota 2: os URL a serem consultados e o tipo de suas respostas foram apresentados no parágrafo 5.4.2.

Nota 3:

A classe [CommandsFragment] envia uma lista contendo um único comando a ser executado por um ou mais Arduinos. Esse comando será encapsulado na seguinte classe [ArduinoCommand]:


package android.arduinos.dao;

import java.util.Map;

public class ArduinoCommand {

  // dados
  private String id;
  private String ac;
  private Map<String, Object> pa;

  // construtores
  public ArduinoCommand() {

  }

  public ArduinoCommand(String id, String ac, Map<String, Object> pa) {
    this.id = id;
    this.ac = ac;
    this.pa = pa;
  }

  // getters e setters
...
}

Na interface [WebClient], o método para executar essa lista de um comando será o seguinte:


  // envio de comandos JSON
  @Post("/arduinos/commands/{idArduino}")
Response<List<ArduinoResponse>> sendCommands(@Body List<ArduinoCommand> commands, @Path String idArduino);
  • linha 2: o URL é solicitado com uma ordem HTTP POST;
  • linha 3: o valor lançado deve conter a anotação [@Body];

Nota 4: recomenda-se realizar este trabalho da seguinte maneira:

  • só passe para a próxima tela quando a tela atual tiver sido criada e testada;
  • gerencie o estado das visualizações somente após obter um aplicativo funcional em condições normais. Em seguida, para cada visualização, execute o dispositivo em diferentes estados da visualização e anote as informações perdidas. São essas informações que devem ser salvas e, posteriormente, restauradas. Em seguida, verifique a navegação: ao sair de uma aba e retornar a ela posteriormente, deve-se encontrá-la no estado em que foi deixada;