Skip to content

5. TP 2 - Controlar Arduinos com um tablet Android

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

5.1. Arquitetura do projeto

O projeto na sua totalidade terá a seguinte arquitetura:

  • o bloco [1], servidor web / jSON e os Arduinos serão fornecidos;
  • terão de construir o bloco [2] e programar o tablet Android para comunicar com o servidor web / jSON.

5.2. O material

Têm à vossa disposição os seguintes elementos:

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

5.2.1. O Arduino

Eis como proceder para ligar os diferentes elementos entre si:

  • retire o cabo de rede do seu PC;
  • ligue o seu PC ao Arduino através de um cabo de rede;
  • o Arduino que terá à sua disposição já estará programado. O seu endereço IP será [192.168.2.2]. Para que o seu PC detecte o Arduino, é necessário atribuir-lhe o endereço IP na rede [192.168.2]. Os Arduinos foram programados para comunicar com um PC com o endereço IP [192.168.2.1]. Eis como proceder:

Aceda ao [Panneau de configuration\Réseau et Internet\Centre Réseau et partage]:

 
  • em [1], clique na ligação [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 cartão o endereço IP [192.168.2.1] e a máscara de sub-rede [255.255.255.0];
  • em [5], clique em [OK] tantas vezes quantas forem necessárias para sair do assistente.

5.2.2. O tablet

  • utilizando a sua chave Wi-Fi, ligue o seu computador à rede Wi-Fi que lhe for indicada. Faça o mesmo com o seu tablet;
  • Verifique o endereço Wi-Fi IP do seu PC, digitando [ipconfig] numa janela DOS. Irá 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 IP do seu tablet. Pergunte ao seu orientador como fazê-lo, caso não saiba. Irá encontrar um endereço do tipo [192.168.x.z];
  • desative o firewall do seu PC, caso esteja ativo [Panneau de configuration\Système et sécurité\Pare-feu Windows];
  • numa janela do DOS, verifique se o PC e o tablet conseguem comunicar, digitando o comando [ping 192.168.x.z], em que [192.168.x.z] é o endereço IP do seu tablet. O tablet deverá 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 está agora pronta.

5.2.3. O emulador [Genymotion]

O emulador [Genymotion] (ver parágrafo 6.9) substitui vantajosamente o tablet. É praticamente tão rápido e não requer rede Wi-Fi. Recomenda-se a utilização deste método. Poderá utilizar o tablet para a verificação final da sua aplicação.

5.3. Programação dos Arduinos

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

A ler

  • instalação do ambiente de desenvolvimento Arduino (ver parágrafo 6.1);
  • utilização 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 ligados a hardware. Estes pinos são entradas ou saídas. O seu valor é binário ou analógico. Para controlar o Arduino, existem duas operações básicas:

  • escrever um valor binário/analógico num pino designado pelo seu número;
  • ler um valor binário/analógico num pino designado pelo seu número;

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

  • fazer um LED piscar durante um determinado período de tempo e com uma determinada frequência. Esta 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. Não é, portanto, possível fazer um LED piscar a cada 100 milissegundos, por exemplo. Por isso, vamos implementar esta 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 realizam-se através de uma rede TCP-IP, por meio de trocas de linhas de texto no formato jSON (JavaScript Object Notation);
  • ao arrancar, o Arduino liga-se à porta 100 de um servidor de registo presente na camada [DAO]. 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 cadeia jSON que identifica o Arduino que se está a ligar:

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

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

  • Assim que o Arduino se registar no servidor de registo, fica à escuta na porta que indicou ao servidor (102, acima). Aguarda comandos jSON com o seguinte formato:
{"id":"identifiant","ac":"une_action","pa":{"param1":"valeur1","param2":"valeur2",...}}

Trata-se de uma cadeia 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 escrever um valor num 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. Dependem da ação.
  • O Arduino envia sistematicamente uma resposta ao seu cliente. Esta resposta é uma cadeia 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 no caso do comando de leitura pr. Nesse caso, o dicionário contém o valor do pino n.º x solicitado.

Eis alguns exemplos destinados a esclarecer as especificações anteriores:

Fazer piscar o LED n.º 8 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 piscares e o n.º do pino do LED.

Escrever 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 gravar e o n.º 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, o número do pino. Se não houver erros, o Arduino insere no dicionário «et» da 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].

Apresentámos os três comandos cl, pw e pr. Podemos questionar-nos por que razão não utilizámos campos mais explícitos nas cadeias jSON, como «action» em vez de «ac», «pinwrite» em vez de «pw», «parametres» em vez de «pa», etc. Um Arduino tem uma memória muito reduzida. No entanto, as cadeias de caracteres jSON trocadas com o Arduino contribuem para a ocupação de memória. Por isso, optou-se por encurtá-las ao máximo.

Vejamos agora alguns casos de erro:

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

Foi enviado um comando que não está no formato jSON. O Arduino devolveu 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 indicar o parâmetro pin. O Arduino devolveu o código de erro 302.

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

Enviámos um comando pinread desconhecido (é o pr). O Arduino devolveu o código de erro 104.

Não vamos continuar com os exemplos. A regra é simples. O Arduino não deve bloquear, independentemente do comando que lhe seja 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 devolve ao seu cliente a cadeia de erro jSON. Mais uma vez, devido às limitações de espaço de memória, é devolvido 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 o transferir para o Arduino:

  • ligue-o ao seu PC;
  • no [1], abra o ficheiro [arduino_uno.ino]. O Arduino IDE irá iniciar-se e carregar o ficheiro;

Nota: o código foi originalmente criado e testado com o IDE ARDUINO 1.5.x. Desde então, foram lançadas outras versões do IDE. 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], indique o tipo de Arduino utilizado;
  • no [5-7], indique em que porta série do PC se encontra;
  • no [8], carregue o programa [arduino_uno] no Arduino;

O código do programa está muito bem comentado. O leitor interessado poderá consultá-lo. Assinalamos 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";
// a morada IP do Arduino
IPAddress ipArduino(192,168,2,2);
// o seu identificador
char * idArduino="cuisine";
// porta do servidor Arduino
int portArduino=102;
// descrição do Arduino
char * descriptionArduino="contrôle domotique";
// o servidor Arduino funcionará na porta 102
EthernetServer server(portArduino);
// IP do servidor de registo
IPAddress ipServeurEnregistrement(192,168,2,1); 
// porta do servidor de registo
int portServeurEnregistrement=100;
// o cliente Arduino do servidor de registo
EthernetClient clientArduino;
// o comando do cliente
char commande[100];
// a resposta do Arduino
char message[100];

// inicialização
void setup() {
  // O monitor de série permitirá acompanhar as trocas de dados
  Serial.begin(9600);
  // início da ligaçã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. Não tem grande importância neste contexto, uma vez que o Arduino estará numa rede privada onde existe 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 tem um autocolante onde está indicado o endereço MAC da placa. Se esse autocolante não estiver presente e se não souber o endereço MAC da placa, pode colocar 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. Mais uma vez, pode-se colocar o que se quiser do tipo [192.168.2.x] e variar 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 se quiser;
  • linha 17: a descrição da função do Arduino. Pode-se definir o que se quiser. Tenha cuidado com cadeias de caracteres longas devido à memória limitada do Arduino;
  • linha 21: endereço IP do servidor de registo do Arduino no PC. Não deve ser alterado;
  • linha 23: porta deste serviço de registo. Não deve ser alterada;

5.4. O servidor web / jSON

5.4.1. Instalação

Image

O ficheiro binário Java do servidor web / jSON é-lhe fornecido:

 

Abra uma janela de comandos e introduza 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 introduzir o caminho completo do [java.exe] (normalmente C:\Program Files\java\...).

Será aberta uma janela DOS que exibirá os registos:


.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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: é iniciado um servidor Tomcat incorporado;
  • linha 15: a servlet [dispatcherServlet] do Spring MVC é carregada e executada;
  • linha 18: o Rest URL [/arduinos/blink/{idCommande}/{idArduino}/{pin}/{duree}/{nombre}] é detetado;
  • linha 19: é detetado o Rest URL [/arduinos/commands/{idArduino}];
  • linha 20: é detetado o URL Rest [/arduinos/];
  • linha 21: é detetado o URL Rest [/arduinos/pinRead/{idCommande}/{idArduino}/{pin}/{mode}];
  • linha 22: o URL Rest [/arduinos/pinWrite/{idCommande}/{idArduino}/{pin}/{mode}/{valeur}] é detetado;
  • linha 26: o servidor de registo dos Arduinos é iniciado;

Ligue o seu Arduino ao PC, caso ainda não o tenha feito. O firewall do PC deve estar desativado. Em seguida, num navegador, aceda ao URL [http://localhost:8080/arduinos]:

Deve aparecer o identificador do Arduino ligado. Se não aparecer nada, lembre-se de reiniciar o Arduino. Este possui um botão para esse efeito.

O servidor web / jSON está agora instalado.

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

A ler: 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 intermitente
  @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 do 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
    // estado da operação
    private int status;
    // eventuais mensagens de estado
    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: a sua descrição;
  • linha 9: o seu endereço MAC;
  • linha 10: o seu endereço IP;
  • linha 11: a porta na qual 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 cadeia jSON enviada por um Arduino e que não pôde ser descodificada (caso de erro), null caso contrário;
  • [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 que contém a resposta específica ao comando. Na maioria das vezes, está vazio, a menos que o comando tenha solicitado a leitura de um valor do Arduino; nesse caso, esse valor será colocado neste 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

Eis algumas capturas de ecrã do resultado que deve obter:

Obter a lista dos Arduinos ligados:

A cadeia 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 um erro;
  • [messages]: uma lista de mensagens que explicam o erro, caso tenha ocorrido algum:
  • [body]: a lista de Arduinos, caso não tenha ocorrido nenhum 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 aguarda comandos;

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

 

A cadeia 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 um erro;
  • [messages]: uma lista de mensagens que explicam o erro, caso tenha ocorrido algum erro:
  • [body]: a resposta do Arduino caso não tenha ocorrido qualquer erro:
    • [id]: identificador do comando. Este identificador é o 1 em [/blink/1]. O Arduino inclui este identificador de comando na sua resposta;
    • [erreur]: um número de erro. Um valor diferente de 0 indica um erro;
    • [etat]: é utilizado apenas para a leitura de um pino. Nesse caso, tem como valor o valor do pino;
    • [json]: é utilizado apenas em caso de erro jSON entre o cliente e o servidor. Tem então como valor a cadeia de caracteres errada jSON enviada pelo Arduino;

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

 

A cadeia 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 cadeia 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 cadeia 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 este URL, aguarda um pedido POST que não é possível simular simplesmente com um navegador. Para testar este URL, pode-se utilizar 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 testar;
  • em [2], o método POST para enviar o pedido;
  • em [3-4], o valor enviado é o de jSON;
  • em [5], a cadeia jSON foi enviada. É importante reparar nos parênteses retos que iniciam e terminam a lista. Aqui, na lista, existe apenas um comando jSON que faz piscar o pino n.º 8, 10 vezes a cada 100 ms;
  • em [6], envia-se o pedido;
 
  • em [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:

 

Vemos que, na resposta do Arduino, o número de erro é [104], indicando assim que o comando [xx] não foi reconhecido.

5.5. Testes no cliente Android

Apresenta-se o ficheiro executável do cliente Android já concluído:

  

Com o rato, arraste o ficheiro executável [app-debug.apk] acima para um emulador de tablet [GenyMotion]. Este será então guardado e, em seguida, executado. Inicie também o servidor web / jSON, caso ainda não o tenha feito. Ligue o Arduino ao PC com um LED acoplado. O cliente Android permite gerir os Arduinos à distância. Apresenta ao utilizador os seguintes ecrãs.

O separador [CONFIG] permite ligar-se ao servidor e obter a lista dos Arduinos ligados:

Image

  • em [1], introduza o endereço IP [192.168.2.1] atribuído ao seu PC (ver parágrafo 5.2).

O separador [PINWRITE] permite escrever um valor num pino de um Arduino:

Image

Image

O separador [PINREAD] permite ler o valor de um pino de um Arduino:

Image

O separador [BLINK] permite fazer piscar um LED de um Arduino:

Image

O separador [COMMAND] permite enviar um comando jSON para um Arduino:

Image

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

Passamos 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] comunica com o servidor web / jSON;

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

5.6.2. O projeto do cliente no Android Studio

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 vistas XML

  

Haverá cinco vistas XML:

  • [blink]: para fazer piscar um LED de um Arduino. 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 ligados. 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 escrever um valor binário ou analógico num pino de um Arduino. Está associada ao fragmento [PinWriteFragment];

Por enquanto, estas cinco vistas 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 vista encontra-se num contentor [RelativeLayout] (linhas 7-10), que por sua vez está incluído num contentor [ScrollView] (linhas 2-11). Isto garante que possamos «deslizar» a vista caso esta exceda o tamanho do ecrã de um tablet;

Tarefa: crie as cinco vistas XML.


5.6.4. O menu dos fragmentos

Sabemos que os fragmentos de um projeto construído com [client-android-skel] têm de estar associados a um menu, mesmo que este esteja vazio. Neste caso, a aplicação não terá menu. O menu vazio já se encontra no projeto;

  

5.6.5. Os cinco fragmentos da aplicação

 

Tarefa: duplique o fragmento [DummyFragment] nos cinco fragmentos da aplicação, tal como 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 linha:


@EFragment(R.layout.config)

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


Fragmento
Vista
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 da aplicação.

5.6.7.1. A interface [IMainActivity]

A interface [IMainActivity] define o que os fragmentos podem solicitar à atividade, bem como as constantes da aplicação. Esta interface será, neste caso, 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 vista
  void navigateToView(int position, ISession.Action action);

  // gestão da 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 do pedido 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 separadores
  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]. Esta aplicação consulta um servidor web / jSON;
  • linha 37: esta aplicação possui separadores;
  • linha 43: esta aplicação tem cinco fragmentos;
  • linhas 46-50: os números dos cinco fragmentos;
  • linha 34: adjacência dos fragmentos. O programador pode introduzir 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() {
    // registo
    if (IS_DEBUG_ENABLED) {
      Log.d(className, "onCreateActivity");
    }
    // sessão
    this.session = (Session) super.session;
    // criação das cinco separadores
    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) {
    // exibe-se o 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 dos cinco separadores da aplicação;
  • linha 48: os títulos dos separadores 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. Estes títulos serão procurados no ficheiro [res / values / strings.xml]
  

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


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

  <!-- nome da aplicação -->
  <string name="app_name">[arduinos-client-01]</string>
  <!-- Fragmentos e separadores -->
  <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 anteriores e compile o projeto. Não deve haver erros.


Execute o projeto. Deverá obter a seguinte visualização no emulador:

Image

Analise os registos que acompanharam a exibição da primeira vista e acompanhe as diferentes etapas executadas. Navegue de um separador para outro e continue a acompanhar os registos.

5.6.9. A vista XML [config]

A vista XML [config] será a seguinte:

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


<?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 vista utiliza cadeias de caracteres (android:text nas linhas 15, 25, 37, 50, 61, 73) que estão definidas no ficheiro [res / values / strings]:

  

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

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

    <!-- Fragmentos e separadores -->
    <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 vista utiliza cores (android:textColor nas linhas 51 e 62) definidas no ficheiro [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>
  <!-- aplicação -->
  <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 ficheiro [res / values / dimens]:

  

<resources>
  <!-- Margens de ecrã predefinidas, 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>
  <!-- aplicação -->
  <dimen name="titre">30dp</dimen>
</resources>

Esta técnica não foi utilizada para todas as dimensões. No entanto, é a técnica recomendada. Permite alterar as dimensões num único local.


Tarefa: crie os elementos acima referidos.


Execute novamente o seu projeto. Deverá obter a seguinte visualização:

Image

5.6.10. O fragmento [ConfigFragment]

  

Para gerir a nova vista [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() {
  }

  // gestão 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 apresentado, o botão [Annuler] será ocultado (linha 82) e o botão [Rafraîchir] será apresentado (linhas 86-87). Com efeito, nesta aplicação, um fragmento não pode ser apresentado enquanto estiver em curso uma operação assíncrona e, por isso, o botão [Annuler] fica visível;

Tarefa: crie os elementos acima referidos.


Execute esta nova versão. A primeira vista 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 forma:


@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) {
    // prepara-se a espera das tarefas
    beginRunningTasks(numberOfRunningTasks);
    // o botão [Annuler] substitui o botão [Rafraîchir]
    btnRafraichir.setVisibility(View.INVISIBLE);
    btnAnnuler.setVisibility(View.VISIBLE);
}
  // gestão do ciclo de vida do fragmento -------------------------------------
...
  @Override
  protected void notifyEndOfTasks(boolean runningTasksHaveBeenCanceled) {
    // botões no 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: inicia-se a espera;
  • linha 18: passamos para a classe pai o número de tarefas assíncronas que vamos iniciar. A imagem de espera irá aparecer;
  • linhas 20-21: esta espera resultará no aparecimento do botão [Annuler], no desaparecimento do botão [Rafraîchir] e no aparecimento da imagem de espera. Não acontece mais nada. No entanto, o utilizador pode 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 irá fazê-lo e, em resposta, chamará o método das linhas 25-29 para indicar que todas as tarefas estão concluídas. O parâmetro [runningTasksHaveBeenCanceled] assumirá o valor true para indicar que as tarefas foram canceladas;
  • linhas 35-36: o botão [Annuler] desaparecerá, enquanto o botão [Rafraîchir] reaparecerá.

Tarefa: Efetue estas 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 registos.


5.6.10.2. Verificação dos dados introduzidos

Na versão anterior, não verificávamos a validade do valor introduzido em URL. Para o verificar, adicionamos o seguinte código em [ConfigFragment]:


// os valores introduzidos
  private String urlServiceRest;

  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // verificam-se os dados introduzidos
    if (!pageValid()) {
      return;
    }
    // vai-se iniciar uma tarefa — prepara-se a espera
    beginWaiting(1);
  }

  // verificação dos dados introduzidos
  private boolean pageValid() {
    // inicialmente, nenhuma mensagem de erro
    txtMsgErreurUrlServiceRest.setVisibility(View.INVISIBLE);
    // recuperam-se o IP e a porta do servidor
    urlServiceRest = String.format("http://%s", edtUrlServiceRest.getText().toString().trim());
    // verifica-se a sua 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 de mensagem de erro
      txtMsgErreurUrlServiceRest.setVisibility(View.VISIBLE);
      // regresso ao UI
      return false;
    }
    // Está tudo bem
    return true;
  }
  • linha 2: a entrada URL;
  • linhas 7-9: antes de fazer qualquer coisa, verificamos a validade dos dados introduzidos;
  • linha 19: recuperamos o URL introduzido e adicionamos-lhe o prefixo [http://];
  • linha 22: tenta-se construir um objeto URI (Uniform Resource Identifier) com ele. Se o URL introduzido estiver sintaticamente incorreto, ocorrerá uma exceção;
  • linhas 23-27: é gerada uma exceção se o URI estiver correto, mas existirem, no entanto, os valores [host==null] e [port==-1]. Este é um caso possível;
  • linha 30: ocorreu uma exceção. É apresentada a mensagem de erro;
  • linha 32: devolve-se [false] para indicar que a página é inválida;
  • linha 35: não ocorreram erros. Devolvemos [true] para indicar que a página é válida;

Tarefa: crie os elementos acima referidos.


Teste esta 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 vistas vão precisar de apresentar a lista dos Arduinos ligados. Para tal, vamos definir várias classes e uma vista XML:

  • um Arduino será representado pela classe [Arduino] [1];
  • a classe [CheckedArduino] [1] herda da classe [Arduino], à qual foi adicionado um valor booleano para indicar se o Arduino foi ou não selecionado numa 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: a sua descrição;
  • linha 9: o seu endereço MAC;
  • linha 10: o seu endereço IP;
  • linha 11: a porta na qual aguarda comandos;

Esta classe corresponde à cadeia jSON recebida do servidor quando lhe é solicitada a lista dos Arduinos ligados:

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: adiciona-se-lhe um valor booleano que nos servirá para saber se, na lista de Arduinos apresentada, foi ou não selecionado um Arduino;

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

  

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

  private void getArduinosInBackground() {
   ...
  }

  // zerar a lista de Arduinos
  private void clearArduinos() {
    // criamos uma lista vazia
    List<String> strings = new ArrayList<>();
    // exibe-se 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 apresenta os Arduinos ligados ao servidor;
  • linha 5: o método que solicita a lista dos Arduinos ligados;
  • linha 11: indica-se à classe pai que se vai iniciar uma tarefa assíncrona;
  • linha 12: apaga-se a lista de Arduinos atualmente apresentada;
  • linha 15: solicita-se, em segundo plano, a lista dos Arduinos ligados;
  • linhas 23-28: o método que limpa a lista de Arduinos atualmente apresentada;

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: repõe-se os botões no seu estado inicial;
  • linha 15: processa-se a resposta;

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());
      // regresso à interface do utilizador
      return;
    }
    // criamos uma lista de [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // exibem-se
    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: regressa-se à interface do utilizador;
  • linhas 11-16: se não tiverem ocorrido erros, exibe-se a lista de Arduinos recebida, após a ter convertido num 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());
    }
    // exibe-se
    listArduinos.setAdapter(new ArrayAdapter<>(activity, android.R.layout.simple_list_item_1, android.R.id.text1, strings));
}

Tarefa: efetue as alterações anteriores e execute o seu projeto.


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

Image

A entrada em [1] não é utilizada. Por isso, pode introduzir qualquer valor, desde que respeite o formato esperado.

5.6.10.4. Um modelo para apresentar um Arduino

Por enquanto, os Arduinos ligados são apresentados na vista [Config] da seguinte forma:

Image

Agora, pretendemos exibi-los da seguinte forma:

Image

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

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

Começamos por criar a vista que irá apresentar um elemento da lista de Arduinos:

 

O código da vista [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 vista 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 vista utiliza também 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 gestor de visualização de um elemento da lista de Arduinos

  

A classe [ListArduinosAdapter] é a classe chamada pela [ListView] para apresentar cada um dos elementos da lista de Arduinos. O 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 visualizaçã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 guardadas
        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 vista a apresentar 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 apresentada ou não;
  • linhas 8-15: estas quatro informações são armazenadas localmente;

Linha 29: o método [getView] é responsável por gerar a vista n.º [position] no [ListView] e por gerir os seus eventos. O 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-lhe o seu valor
            ck.setChecked(arduino.isChecked());
            // gestiona-se o clique
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {

                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    arduino.setChecked(isChecked);
                }
            });
        }
        // a linha é apresentada
        return row;
    }
  • linha 2: o primeiro parâmetro é a posição no [ListView] da linha a criar. É 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 vista [listarduinos_item.xml];
  • linhas 8-9: são obtidas as referências aos dois [TextView];
  • linhas 11-12: os dois [TextView] recebem o seu valor;
  • linha 14: recupera-se uma referência à caixa de seleção;
  • linha 15: torna-se visível ou não, consoante o valor [selectable] inicialmente passado ao construtor;
  • linha 16: se a caixa de seleção estiver presente;
  • linha 18: atribui-se-lhe o valor [isChecked] do Arduino atual;
  • linhas 20-26: trata-se do clique na caixa de seleção;
  • linha 23: o valor da caixa de seleção é guardado no Arduino atual;

Gestão da lista de Arduinos

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

  • [clearArduinos]: que apresenta uma lista vazia;
  • [showArduinos]: que apresenta a lista devolvida pelo servidor;

Estes 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: Efetue estas alterações e teste a nova aplicação.


Image

5.6.10.5. A sessão

A sessão é o local onde colocamos as informações partilhadas pelos fragmentos e pela atividade. Todos os fragmentos precisam de apresentar a lista dos Arduinos ligados. Assim, 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 partilhar 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/deserialização em jSON

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

  // getters e setters
...
}

Tarefa: crie a classe [Session] acima referida.


A criação desta sessão leva-nos a alterar o código já escrito da seguinte forma:


  // exibição da resposta
  private void consumeArduinosResponse(Response<List<Arduino>> response) {
    // erro?
    if (response.getStatus() != 0) {
      // exibição
      showAlert(response.getMessages());
      // anulação
      doAnnuler();
      // regresso à interface do utilizador
      return;
    }
    // cria-se uma lista de [CheckedArduino]
    List<CheckedArduino> checkedArduinos = new ArrayList<>();
    for (Arduino arduino : response.getBody()) {
      checkedArduinos.add(new CheckedArduino(arduino, false));
    }
    // coloca-se a lista na sessão
    session.setCheckedArduinos(checkedArduinos);
    // exibem-se
    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. Gestão do estado do fragmento

Quando o dispositivo é rodado, os componentes visuais da vista são apresentados (por predefinição) no estado em que se encontravam aquando da conceção da vista:

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

Os estados dos componentes visuais no momento da conceção podem ou não ser adequados aquando da restauração de um fragmento. O que acontece neste caso?

  • o [ListView] deve apresentar a lista dos Arduinos ligados. O valor do [ListView] no momento da conceção não pode, portanto, ser utilizado;
  • o [TextView] da mensagem de erro deve ser restaurado no estado (visível ou não) em que se encontrava no momento do salvamento. O seu valor na fase de conceção pode não ser adequado para estes dois casos;

Por isso, temos de guardar o estado destes dois componentes ao guardar o estado do fragmento:

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

Como a lista de Arduinos está presente na sessão, será guardada automaticamente. A visibilidade da mensagem de erro será memorizada 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] anterior.


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


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

  @Override
  protected int getNumView() {
    // é necessário devolver o n.º do fragmento na tabela de fragmentos geridos pela atividade (ver MainActivity)
    return 0;
}

Se não for tomada nenhuma medida, o estado gerado na linha 6 será guardado no elemento 0 (linha 13) da tabela 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, deve ser guardado no elemento correspondente ao n.º 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 vistas
  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á gerido corretamente se escrevermos:


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

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

Além disso, a classe [CoreState], que é a classe-pai dos estados dos fragmentos, é, neste momento, 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 da lista de classes filhas da classe [CoreState]. No entanto, o método [saveFragment] da classe [BlinkFragment] devolve atualmente um tipo [ DummyFragmentState]. Se deixarmos as coisas como estão, a serialização/deserialização da sessão irá falhar e a sessão não será restaurada, o que levará a uma falha da aplicação;

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


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

Tarefa: em cada um dos fragmentos, altere o método [getNumView] para que este devolva o n.º do fragmento e o método [saveFragment] para que este devolva uma instância da classe de estado do fragmento (como acima).


5.6.10.7. Gestão do ciclo de vida do fragmento

Estamos aqui a analisar o ciclo de vida do fragmento [ConfigFragment], nomeadamente os quatro métodos:

  • [saveFragment]: deve guardar o estado do fragmento para que este possa ser recuperado posteriormente;
  • [initFragment]: que deve inicializar determinados campos do fragmento, se necessário. Este método é chamado no arranque da aplicação e sempre que ocorre uma rotação do dispositivo. Mais precisamente, é chamado quando o fragmento se torna visível após um dos dois eventos anteriores;
  • [initView]: que deve inicializar determinados componentes da vista, se necessário. Este método é chamado sempre que o [initFragment] for chamado e quando a vista tiver de ser regenerada porque o fragmento, num determinado momento, saiu da adjacência do fragmento exibido. Tal como anteriormente, é chamado quando o fragmento se torna visível após um destes 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. A sua função é restabelecer o estado anterior do fragmento;

Estes métodos serão os seguintes:


// adaptador da lista de Arduinos
  private ListArduinosAdapter adapterListArduinos;

...
  // gestão 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 a listview e o 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 porque é utilizada em diferentes métodos;
  • linhas 7-12: o método [saveFragment] guarda, num 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). Recorde-se que a função do [initFragment] é inicializar os campos do fragmento. Aqui, esta inicialização deve ser efetuada em todos os casos, quer se trate da primeira visita (previousState==null) ou não;
  • linha 17: verifica-se que o adaptador está ligado à fonte de dados [session.getCheckedArduinos]. Esta não deve ter o valor null. Por este 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 como função inicializar determinados componentes da interface visual, nomeadamente 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 restantes;
  • linha 29: na primeira visita, deve ser apresentado um [ListView] vazio. É o que acontece, uma vez que, 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á se encontra 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 na última gravação do fragmento;
  • linhas 31-36: o método [updateOnRestore] deve repor o fragmento no seu estado inicial. Chega-se ao método [updateOnRestore] de duas formas:
    • ou porque houve uma rotação do dispositivo. Neste caso, todas as inicializações necessárias foram realizadas no método [initView];
    • ou porque se navega de um separador para o separador [Config]. Se o fragmento [Config] saiu da vizinhança dos fragmentos exibidos desde que foi abandonado, o método [initView] foi então executado e o fragmento já se encontra no estado desejado. Se o fragmento [Config] não tiver saído da vizinhança dos fragmentos exibidos desde que foi abandonado, os seus componentes visuais não alteraram de estado e não há nada a fazer;

Vê-se que o método [updateOnRestore] não tem nada a fazer. Por vezes é esse o caso, outras vezes não. A diferença reside no método [updateOnSubmit]: se este método realizar alguma ação que torne desnecessárias certas inicializações efetuadas em [initView], então essas inicializações deveriam ser efetuadas no método [updateOnRestore]. Tomemos o exemplo de um botão de opção com três valores: V1, V2, V3. Talvez, no caso de uma navegação associada a uma ação [SUBMIT], o botão de opção selecionado deva ser sempre aquele com o valor V1. Neste 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 valor fornecido pelo método [updateOnSubmit]. É, portanto, preferível deslocar essa restauração para o método [updateOnRestore], para evitar realizar, por vezes, uma operação desnecessária.

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

Tarefa: adicione o código anterior ao [ConfigFragment] e, em seguida, execute a aplicação. Verifique que, ao rodar o dispositivo, o separador [Config] mantém o seu estado (mensagem de erro, lista de Arduinos). Verifique se o mesmo acontece quando efetuar uma simples navegação da guia [config] --> guia [Commands] --> guia [Config]. Neste último caso, se tiver mantido no [IMainActivity] uma adjacência de fragmentos igual a 1, então a visualização do fragmento [ConfigFragment] é destruída ao passar para o separador [Commands] e, em seguida, recriada ao regressar ao separador [Config]. Durante os testes, analise os registos.


5.6.10.8. Melhoria do código

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


// adaptador da lista de Arduinos
  private ListArduinosAdapter adapterListArduinos;

...

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

  // limpar 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);
  }
  • vemos que, nas linhas 9 e 16, é utilizada uma variável local desligada do campo da linha 2, quando na verdade é a mesma entidade que pretendemos manipular;

Modificamos o código da seguinte forma:


  // 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();
      // regresso à interface do utilizador
      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));
    }
    // exibem-se
    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 ligados foi ligado a este adaptador (linha 48);

Quando queremos alterar a exibição do [ListView], é necessário fazer duas coisas:

  • alterar o conteúdo da fonte de dados [session.checkedArduinos];
  • comunicar essa alteração ao adaptador através da instrução [adapterListArduinos.notifyDataSetChanged()];

Trata-se, de facto, 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á a apresentar a fonte de dados anterior. 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: esvaziamo-la. Por este motivo, eliminámos o método [clearArduinos];
  • linhas 29-31: nesta lista agora vazia, adicionamos novos elementos;
  • linha 33: diz-se ao adaptador para atualizar. Isto irá atualizar a visualização do [ListView] associado;

Tarefa: efetue estas alterações e verifique se a sua aplicação continua a funcionar.


5.6.11. Comunicação entre vistas

Para verificar a comunicação entre vistas, vamos fazer com que todas as outras vistas apresentem a lista de Arduinos obtida pela vista [Config]. Comecemos pela vista [blink.xml]. Enquanto antes não apresentava nada, passará agora a apresentar a lista de Arduinos ligados:

Image

 

O código XML da vista [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>

Este código foi retirado diretamente da vista [config.xml]. Apenas se alterou a margem superior da linha 19.


Tarefa: duplique este código nas vistas [commands.xml, pinread.xml, pinwrite.xml].


O código do fragmento [BlinkFragment] associado à vista [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 a listview e o adaptador
    listArduinos.setAdapter(adapterListArduinos);
  }
...
  • linhas 2-3: o componente [ListView] dos Arduinos ligados;
  • linha 6: o adaptador deste [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 tem de ser reinicializado, reinicializa-se o adaptador da linha 2, associando-o à lista de Arduinos memorizada na sessão. O último parâmetro [true] do construtor [ListArduinosAdapter] significa que se pretende que apareça uma caixa de seleção ao lado de cada Arduino;
  • linha 22: quando a visualização do fragmento tiver de ser reiniciada, associa-se o [ListView] dos Arduinos ligados ao adaptador da linha 6;

Tarefa: Duplique este código nos outros fragmentos [CommandsFragment, PinReadFragment, PinWriteFragment]. Execute a aplicação e verifique agora que cada separador apresenta a lista dos Arduinos ligados. Verifique também que, se marcar os Arduinos num separador e navegar para outro separador, os encontrará marcados neste último.


Nota: 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 com a 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-lhe o seu valor
            ck.setChecked(arduino.isChecked());
            // gestiona-se o clique
            ck.setOnCheckedChangeListener(new OnCheckedChangeListener() {

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

5.6.12. A camada [DAO]

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

Até ao momento, gerámos manualmente a lista dos Arduinos ligados. Vamos agora solicitá-la ao servidor web / jSON. Para tal, 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);

  // utilizador
  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: estas 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 ligados sob a forma de um observável do tipo Observable<[Response<List<Arduino>>>];

Recorde-se que [Response<T>] é o tipo de todas as respostas enviadas pelo servidor na forma de uma cadeia jSON:


package client.android.dao.entities;

import java.util.List;

public class Response<T> {

    // ----------------- propriedades
    // estado da operação
    private int status;
    // eventuais mensagens de erro
    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
...
}

5.6.12.2. A interface [WebClient]

  

A interface [WebClient] é uma interface cuja implementação é fornecida pela biblioteca AA. Esta 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: estas linhas estão presentes por defeito na interface [WebClient] do projeto modelo [client-android-skel];
  • linha 23: o URL do servidor que permite obter a lista de Arduinos através de uma operação GET. Recorde-se que este URL é medido em relação ao URL raiz [RestClientRootUrl] da linha 16;
  • linha 24: o servidor devolve a cadeia jSON de um tipo [Response<List<Arduino>>]. Esta cadeia jSON é automaticamente deserializada para o tipo [Response<List<Arduino>>] graças ao conversor jSON [MappingJackson2HttpMessageConverter] da linha 15;

5.6.12.3. A classe [Dao]

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


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() {
    // registo
    Log.d(className, "afterInject");
    // constrói-se o restTemplate
    factory = new SimpleClientHttpRequestFactory();
    restTemplate = new RestTemplate(factory);
    // configura-se o conversor jSON
    restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    // define-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) {
    // regista-se o utilizador no interceptor
    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));
    }
    // interceptor de autenticação?
    if (isBasicAuthentificationNeeded) {
      // adiciona-se o interceptor 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: estas linhas são de base na classe [Dao] do projeto [client-android-skel];
  • linhas 91-100: implementação do método [getArduinos];
  • linha 94: é chamado o método [getResponse] da classe pai. O único parâmetro deste 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, ou seja, neste caso, um tipo [Response<List<Arduino>>];
  • linha 97: o método [IRequest.getResponse()] delega a tarefa ao método [webClient.getArduinos()] que já apresentámos. O [webClient], definido na linha 24, é instanciado pela biblioteca AA e é uma instância da interface [WebClient] que já apresentámos;

5.6.13. A atividade [MainActivity]

  

Já apresentámos a atividade [MainActivity] no parágrafo 5.6.8. Esta atividade estende a classe [AbstractActivity] e, como tal, 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 forma 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 acabámos de apresentar e à qual há uma referência na linha 8;

5.6.14. O fragmento [ConfigFragment] revisto

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);
    // esta é processada
    consumeArduinosResponse(response);
  }

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

Temos de reescrever as linhas 10 a 16, que geravam de forma estática uma resposta do tipo [Response<List<Arduino>>]. Agora, temos de solicitar essa lista à camada [DAO] através da atividade. O código passa a ser o seguinte:


  @Click(R.id.btn_Rafraichir)
  protected void doRafraichir() {
    // verifica-se os dados introduzidos
    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 introduzido pelo utilizador, é passado para a camada [DAO] através da atividade. Esta será a raiz URL da interface [WebClient] (ver parágrafo 5.6.12.2);
  • linha 10: avisa-se a classe pai de que se vai iniciar uma tarefa assíncrona;
  • linhas 12-19: lançamento da tarefa assíncrona que irá devolver a lista dos Arduinos ligados ao servidor;
  • linha 12: chamada do método [executeInBackground] da classe pai. Este método espera dois parâmetros:
    • linha 12: o processo a observar. Este processo é aqui fornecido 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 devolve o seu resultado do tipo [Response<List<Arduino>>];
  • linha 17: a resposta recebida é passada para o método [consumeArduinosResponse] já definido;

Trabalho: Inicie o servidor conforme indicado no parágrafo 5.4. Ligue 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 ligados. Observe os registos.


Image

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

Verifique se esta lista também aparece nos outros separadores.

5.7. Tarefa a realizar


Seguindo o procedimento que acabou de ser descrito para a vista [Config], crie e teste sucessivamente as outras quatro vistas da aplicação: [Blink], [PinRead], [PinWrite] e [Commands].


As vistas a criar 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 gestores de eventos do fragmento (ver parágrafo 5.6.14);
  • testar e observar os registos;

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

Nota 2: os URL a interrogar e o tipo das respetivas 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. Este 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 esta 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 ter a anotação [@Body];

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

  • só passar para a vista seguinte quando a vista atual tiver sido criada e testada;
  • só gerir o estado das vistas depois de se ter obtido uma aplicação funcional em condições normais. Em seguida, para cada vista, execute o dispositivo para diferentes estados da vista e anote as informações perdidas. São essas que devem ser guardadas e, posteriormente, restauradas. Verifique, em seguida, a navegação: quando se sai de um separador e se regressa a ele posteriormente, deve encontrá-lo no estado em que o deixou;