Full Code of concretesolutions/canarinho for AI

master 29858bb998d8 cached
77 files
196.4 KB
51.7k tokens
327 symbols
1 requests
Download .txt
Showing preview only (221K chars total). Download the full file or copy to clipboard to get everything.
Repository: concretesolutions/canarinho
Branch: master
Commit: 29858bb998d8
Files: 77
Total size: 196.4 KB

Directory structure:
gitextract_bmhm5g9q/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       └── android_master.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE.txt
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── build.gradle
├── canarinho/
│   ├── .gitignore
│   ├── build.gradle
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           └── java/
│               └── br/
│                   └── com/
│                       └── concrete/
│                           └── canarinho/
│                               ├── DigitoPara.java
│                               ├── formatador/
│                               │   ├── Formatador.java
│                               │   ├── FormatadorBase.java
│                               │   ├── FormatadorBoleto.java
│                               │   ├── FormatadorCEP.java
│                               │   ├── FormatadorCPFCNPJ.java
│                               │   ├── FormatadorLinhaDigitavel.java
│                               │   ├── FormatadorTelefone.java
│                               │   └── FormatadorValor.java
│                               ├── validator/
│                               │   ├── Validador.java
│                               │   ├── ValidadorBoleto.java
│                               │   ├── ValidadorCEP.java
│                               │   ├── ValidadorCNPJ.java
│                               │   ├── ValidadorCPF.java
│                               │   ├── ValidadorCPFCNPJ.java
│                               │   └── ValidadorTelefone.java
│                               └── watcher/
│                                   ├── BaseCanarinhoTextWatcher.java
│                                   ├── BoletoBancarioTextWatcher.java
│                                   ├── CEPTextWatcher.java
│                                   ├── CPFCNPJTextWatcher.java
│                                   ├── MascaraNumericaTextWatcher.java
│                                   ├── TelefoneTextWatcher.java
│                                   ├── ValorMonetarioWatcher.java
│                                   └── evento/
│                                       ├── EventoDeValidacao.java
│                                       └── EventoDeValidacaoDeBoleto.java
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── sample/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── br/
│       │           └── com/
│       │               └── concrete/
│       │                   └── canarinho/
│       │                       └── sample/
│       │                           ├── BugOnApi28Test.java
│       │                           └── DemoWatchersInstrumentationTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── br/
│       │   │       └── com/
│       │   │           └── concrete/
│       │   │               └── canarinho/
│       │   │                   └── sample/
│       │   │                       └── ui/
│       │   │                           ├── activity/
│       │   │                           │   └── MainActivity.java
│       │   │                           ├── adapter/
│       │   │                           │   └── WatchersPagerAdapter.java
│       │   │                           ├── fragment/
│       │   │                           │   ├── BaseWatcherFragment.java
│       │   │                           │   ├── CanarinhoValorMonetarioWatcherFragment.java
│       │   │                           │   └── WatcherFragment.java
│       │   │                           └── model/
│       │   │                               └── Watchers.java
│       │   └── res/
│       │       ├── layout/
│       │       │   ├── fragment_canarinho_watcher.xml
│       │       │   ├── fragment_valor_monetario_watcher.xml
│       │       │   └── main_activity.xml
│       │       └── values/
│       │           ├── strings.xml
│       │           └── styles.xml
│       └── test/
│           └── java/
│               └── br/
│                   └── com/
│                       └── concrete/
│                           └── canarinho/
│                               └── test/
│                                   ├── TesteFormatadorBOLETO.java
│                                   ├── TesteFormatadorCEP.java
│                                   ├── TesteFormatadorCNPJ.java
│                                   ├── TesteFormatadorCPF.java
│                                   ├── TesteFormatadorCPFCNPJ.java
│                                   ├── TesteFormatadorLinhaDigitavel.java
│                                   ├── TesteFormatadorTelefone.java
│                                   ├── TesteFormatadorValor.java
│                                   ├── TesteValidadores.java
│                                   └── watcher/
│                                       ├── BoletoTextWatcherTest.java
│                                       └── ValorMonetarioWatcherTest.java
├── settings.gradle
└── tools/
    ├── linters/
    │   ├── checkstyle/
    │   │   ├── checkstyle.xml
    │   │   └── suppressions.xml
    │   ├── linters.gradle
    │   └── pmd/
    │       └── pmd-ruleset.xml
    └── publish.gradle

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

**Smartphone (please complete the following information):**
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

**Additional context**
Add any other context about the problem here.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/workflows/android_master.yml
================================================
name: Android Pull Request Master CI

on:
  pull_request:
    branches:
      - 'master'

jobs:
  Instrumented_Test:
    runs-on: macOS-latest
    strategy:
      matrix:
        api-level: [27, 29]
    steps:
      - name: Checkout
        uses: actions/checkout@v1
        with:
          fetch-depth: 1

      - name: Instrumented Tests
        uses: reactivecircus/android-emulator-runner@v1
        with:
          api-level: ${{ matrix.api-level }}
          script: ./gradlew connectedCheck

  unitTests:
    name: Unit Tests
    runs-on: ubuntu-18.04

    steps:
      - uses: actions/checkout@v1
      - name: set up JDK 1.9
        uses: actions/setup-java@v1
        with:
          java-version: 1.9
      - name: Unit tests
        run: bash ./gradlew test

  linters:
    name: Linters
    runs-on: ubuntu-18.04

    steps:
      - uses: actions/checkout@v1
      - name: set up JDK 1.9
        uses: actions/setup-java@v1
        with:
          java-version: 1.9
      - name: Checkstyle
        run: bash ./gradlew checkstyle
      - name: PMD
        run: bash ./gradlew pmd

================================================
FILE: .gitignore
================================================
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.idea/
*.iml

# Built application files
*.apk
*.ap_

# Files for the Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/

# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Proguard folder generated by Eclipse
proguard/

# Log Files
*.log


================================================
FILE: CHANGELOG.md
================================================
# Changelog

## 2.0.3
    - Migração do bintray para Github Package Registry

## 2.0.2
    - Atualização androidx
    - Migração CI para Github Actions
    - Migração jitpack.io

## 2.0.1
    - Corrige bug no formatador da API 28
    - Corrige bug do formatador numérico com símbolo

## 2.0.0
    - Alterado o pacote da bilbioteca. Junto a reformulação do nome da própria Concrete (remoção de Solutions)
    - Corrige issue #19. Obrigado a @luisfernandezbr pelo fix
    - Atualiza sistema de build

### Quebras de API
    - Corrige grafia do validador de telefone (TELFONE -> TELEFONE).

## 1.2.0
    - Adicionada configuração de `ValorMonetarioWatcher`
        - É possível deixar o símbolo de Real
        - É possível manter os zeros quando o campo é apagado em lote
    - Adicionado Builder para criação de watchers de valor

## 1.1.1:
    - Corrigido bug de ArrayIndexOutOfBounds no `BoletoBancarioTextWatcher` após apagar em lote
    - Adicionado teste de regressão para o caso acima na JVM que será executado no Travis

## 1.1.0:
    - Refatorados testes instrumentados
    - Adicionado construtor para máscara genérica

## 1.0.0:
    - Removida necessidade de ter um validador/evento de validação na criação de Watchers

## 0.1.0:
    - Ajustes no código antes da versão 1.0

## 0.0.9:
    - Correção final para o formatador/validador de boleto. Tanto normal quanto tributo.
## 0.0.8:
    - Adição de formatdor/validador de CEP
    - Correção de formatdor monetário ao rotacionar a tela
## 0.0.7:
    - Correção de validador de boleto quando setando o código de boleto inteiro (caso de uso: recebendo de um leitor de imagens)
## 0.0.6:
    - Watcher de CPF/CNPJ simultâneos
## 0.0.5:
    - Release no JCenter
## 0.0.4:
    - Watcher para valor monetário no padão Real (vírgula para casas de milhar e ponto para casas decimais)
    - Re-estruturação do projeto para gerar os binários na pasta correta do Bintray
## 0.0.3:
    - Série de ajustes para Travis e JCenter (ainda não publicado)
## 0.0.2:
    - Formatadores de telefone
## 0.0.1:
    - Release inicial

================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at bruno.silva@concrete.com.br. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/


================================================
FILE: LICENSE.txt
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.


================================================
FILE: PULL_REQUEST_TEMPLATE.md
================================================
# Título para o PullRequest
O que acha de deixar uma descrição simples sobre o seu PR?

## Tipo de mudança
- [ ] Bug fix 
- [ ] Nova Feature
- [ ] Melhoria
- [ ] Outra?

## Está relacionada a uma issue?
Cole o link da issue aqui.

## Quer adicionar outra informação?
Basta escrever aqui.


================================================
FILE: README.md
================================================
# Android Canarinho

![Build](https://github.com/concretesolutions/canarinho/actions/workflows/android_master.yml/badge.svg)

Esta biblioteca é um conjunto de utilitários para trabalhar com padrões brasileiros no Android.
Inspirado em: https://github.com/caelum/caelum-stella.

O foco aqui é o Android. Portanto, não é compatível com aplicações Java puras.

Entre os padrões implementados temos:

- Formatador e validador de CPF
- Formatador e validador de CNPJ
- Formatador e validador de boleto bancário (e linha digitável)
- Formatador e validador de CEP
- Formatador de telefone
- [Formatador de valores financeiros](#formatador-de-valor-financeiro-no-padrão-real) (vírgula para milhares e ponto para decimais com duas casas)

Estes são utilizados para implementar `TextWatcher`s que formatam e validam a digitação do usuário.

## Exemplo de uso:

### Validar um CPF

```java
if (Validador.CPF.ehValido(cpf))
    Toast.makeText(context, "Válido!", Toast.LENGTH_SHORT).show();
else
    Toast.makeText(context, "Inválido!", Toast.LENGTH_SHORT).show();
```

### Formatar um CPF

```java
String cpfFormatado = Formatador.CPF.formata(usuario.getCpf());
```

### Formatar um EditText para CPF sem validação

```java
cpfEditText.addTextChangedListener(new MascaraNumericaTextWatcher("###.###.###-##"));
```

### Formatar um EditText para CPF com validação

```java
cpfEditText.addTextChangedListener(new MascaraNumericaTextWatcher.Builder()
                                        .paraMascara("###.###.###-##")
                                        .comCallbackDeValidacao(new SampleEventoDeValidacao(context))
                                        .comValidador(Validador.CPF)
                                        .build());
```

## Formatador de valor financeiro no padrão Real

Para deixar um usuário digitar valores monetários no padrão Real, basta adicionar um `ValorMonetarioWatcher` e alguns atributos ao `EditText`

```java
// Padrão sem símbolo de Real
editText.addTextChangedListener(new ValorMonetarioWatcher());
editText.append("1234567890");
assertThat(editText.getText().toString(), is("12.345.678,90"));

// Customizado com símbolo e mantendo zeros ao apagar em lote
editText.addTextChangedListener(new ValorMonetarioWatcher.Builder()
        .comSimboloReal()
        .comMantemZerosAoLimpar()
        .build());
editText.append("1234567890");
assertThat(editText.getText().toString(), is("R$ 12.345.678,90"));

editText.getText().clear();
assertThat(editText.getText().toString(), is("R$ 0,00"));
```

Exemplo de declaração no layout:

```xml
<!--
     O inputType abre o teclado númerico e permite caracteres de 
    formatação. O text inicia com o valor zerado, porém não é 
    obrigatório.
-->
<android.support.design.widget.TextInputEditText
    android:id="@+id/amount"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="@string/hint_value"
    android:inputType="number"
    android:maxLength="13"
    android:text="0,00" />
```

## Callback de validação

Os `TextWatcher`s possuem a possibilidade de avisar o usuário conforme ele está digitando sobre algum erro de campo.
Para isso, usamos um `EventoDeValidacao` que possui os seguintes callbacks:

- `void invalido(String valorAtual, String mensagem)`: chamado quando o valor está inválido
- `void parcialmenteValido(String valorAtual)`: chamado quando o valor ainda não está completo e também não está inválido
- `void totalmenteValido(String valorAtual)`: chamado quando o valor está completo e válido

Um exemplo de implementação:

```java
public class SampleEventoDeValidacao implements EventoDeValidacao {

    private final TextInputLayout textInputLayout;

    public SampleEventoDeValidacao(TextInputLayout textInputLayout) {
        this.textInputLayout = textInputLayout;
    }

    @Override
    public void invalido(String valorAtual, String mensagem) {
        textInputLayout.setError(mensagem);
    }

    @Override
    public void parcialmenteValido(String valorAtual) {
        textInputLayout.setErrorEnabled(false);
        textInputLayout.setError(null);
    }

    @Override
    public void totalmenteValido(String valorAtual) {
        new AlertDialog.Builder(textInputLayout.getContext())
                .setTitle("Campo válido!")
                .setMessage(valorAtual)
                .show();
    }
}
```

Veja exemplos de implementação no sample.

## Changelog

Ver [CHANGELOG.md](CHANGELOG.md)

## Arquitetura

- `Formatador`: Formata, desformata e verifica se um valor está formatado e se pode ser formatado. Opera com valores completos.
- `Validador`: Valida de duas formas: absoluta (true ou false) e atualizando um objeto de validação (`ResultadoParcial`).
- Watchers: implementações de `TextWatcher`s para formatação e validação contínua (conforme a digitação do usuário).

Para exemplos, verifique os testes na pasta sample.

## Gradle

`allprojects {
 		repositories {
 			...
 			maven { url 'https://jitpack.io' }
 		}
 	}`

`implementation 'br.com.concrete:canarinho:{latest version}'`

## ATENÇÃO

Este projeto é desenvolvido de boa vontade e com o intuito de ajudar. No entanto, todo o desenvolvimento é feito SEM GARANTIAS.

## LICENÇA

Este projeto é disponibilizado sob a licença Apache vesão 2.0. Ver declaração no arquivo LICENSE.txt


================================================
FILE: build.gradle
================================================
buildscript {
    repositories {
        google()
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:4.1.3'
        classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4'
        classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}


================================================
FILE: canarinho/.gitignore
================================================
/build


================================================
FILE: canarinho/build.gradle
================================================
apply plugin: 'com.android.library'
apply from: "$rootDir/tools/linters/linters.gradle"
apply from: "$rootDir/tools/publish.gradle"


ext {
    publishedGroupId = 'br.com.concrete'
    libraryName = 'Android Canarinho'
    artifact = 'canarinho'

    libraryDescription = 'Utilitários Android para padrões Brasileiros'
    libraryVersion = '2.0.3'

    licenseName = 'The Apache Software License, Version 2.0'
    licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
    allLicenses = ["Apache-2.0"]
}

android {
    compileSdkVersion 31

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 31
    }
}



================================================
FILE: canarinho/src/main/AndroidManifest.xml
================================================
<manifest package="br.com.concrete.canarinho" />


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/DigitoPara.java
================================================
package br.com.concrete.canarinho;

import android.util.SparseArray;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
 * Uma fluent interface para o cálculo de dígitos, que é usado em diversos boletos e
 * documentos.
 * <p>
 * Para exemplificar, o dígito do trecho 0000039104766 para os multiplicadores indo de
 * 2 a 7 e usando módulo 11 é a seguinte:
 * </p>
 * <pre>
 *  0  0  0  0  0  3  9  1  0  4  7  6  6 (trecho numérico)
 *  2  7  6  5  4  3  2  7  6  5  4  3  2 (multiplicadores, da direita para a esquerda e ciclando)
 *  ----------------------------------------- multiplicações algarismo a algarismo
 *   0  0  0  0  0  9 18  7  0 20 28 18 12 -- soma = 112
 * </pre>
 * <p>
 * Tira-se o módulo dessa soma e, então, calcula-se o complementar do módulo e, se o número
 * for 0, 10 ou 11, o dígito passa a ser 1.
 * </p>
 * <pre>
 *      soma = 112
 *      soma % 11 = 2
 *      11 - (soma % 11) = 9
 * </pre>
 * <p>
 * NOTE: Esta é uma versão otimizada para Android inspirada em
 * https://github.com/caelum/caelum-stella/blob/master/stella-core/src/main/java/br/com/caelum/stella/DigitoPara.java
 * </p>
 */
public final class DigitoPara {

    private final List<Integer> numero = new LinkedList<>();
    private final List<Integer> multiplicadores;
    private final boolean complementar;
    private final int modulo;
    private final boolean somarIndividual;
    private final SparseArray<String> substituicoes;

    private DigitoPara(Builder builder) {

        multiplicadores = builder.multiplicadores;
        complementar = builder.complementar;
        modulo = builder.modulo;
        somarIndividual = builder.somarIndividual;
        substituicoes = builder.substituicoes;
    }

    /**
     * Faz a soma geral das multiplicações dos algarismos pelos multiplicadores, tira o
     * módulo e devolve seu complementar.
     *
     * @param trecho Bloco para calcular o dígito
     * @return String o dígito vindo do módulo com o número passado e configurações extra.
     */
    public final String calcula(String trecho) {

        numero.clear();

        final char[] digitos = trecho.toCharArray();

        for (char digito : digitos) {
            numero.add(Character.getNumericValue(digito));
        }

        Collections.reverse(numero);

        int soma = 0;
        int multiplicadorDaVez = 0;

        for (int i = 0; i < numero.size(); i++) {
            final int multiplicador = multiplicadores.get(multiplicadorDaVez);
            final int total = numero.get(i) * multiplicador;
            soma += somarIndividual ? somaDigitos(total) : total;
            multiplicadorDaVez = proximoMultiplicador(multiplicadorDaVez);
        }

        int resultado = soma % modulo;

        if (complementar) {
            resultado = modulo - resultado;
        }

        if (substituicoes.get(resultado) != null) {
            return substituicoes.get(resultado);
        }

        return String.valueOf(resultado);
    }


    /*
     * soma os dígitos do número (até 2)
     *
     * Ex: 18 => 9 (1+8), 12 => 3 (1+2)
     */
    private int somaDigitos(int total) {
        return (total / 10) + (total % 10);
    }

    /*
     * Devolve o próximo multiplicador a ser usado, isto é, a próxima posição da lista de
     * multiplicadores ou, se chegar ao fim da lista, a primeira posição, novamente.
     */
    private int proximoMultiplicador(int multiplicadorDaVez) {

        int multiplicador = multiplicadorDaVez + 1;

        if (multiplicador == multiplicadores.size()) {
            multiplicador = 0;
        }

        return multiplicador;
    }

    /**
     * Builder com interface fluente para criação de instâncias configuradas de
     * {@link DigitoPara}.
     */
    public static final class Builder {

        private List<Integer> multiplicadores = new ArrayList<>();
        private boolean complementar;
        private int modulo;
        private boolean somarIndividual;
        private final SparseArray<String> substituicoes = new SparseArray<>();

        /**
         * TODO Javadoc pendente.
         *
         * @param modulo Inteiro pelo qual o resto será tirado e também seu complementar.
         *               O valor padrão é 11.
         * @return this
         */
        public final Builder mod(int modulo) {
            this.modulo = modulo;
            return this;
        }

        /**
         * Para multiplicadores (ou pesos) sequenciais e em ordem crescente, esse método permite
         * criar a lista de multiplicadores que será usada ciclicamente, caso o número base seja
         * maior do que a sequência de multiplicadores. Por padrão os multiplicadores são iniciados
         * de 2 a 9. No momento em que você inserir outro valor este default será sobrescrito.
         *
         * @param inicio Primeiro número do intervalo sequencial de multiplicadores
         * @param fim    Último número do intervalo sequencial de multiplicadores
         * @return this
         */
        public final Builder comMultiplicadoresDeAte(int inicio, int fim) {

            this.multiplicadores.clear();

            for (int i = inicio; i <= fim; i++) {
                multiplicadores.add(i);
            }

            return this;
        }

        /**
         * <p>
         * Indica se, ao calcular o módulo, a soma dos resultados da multiplicação deve ser
         * considerado digito a dígito.
         * </p>
         * Ex: 2 X 9 = 18, irá somar 9 (1 + 8) invés de 18 ao total.
         *
         * @return this
         */
        public final Builder somandoIndividualmente() {
            this.somarIndividual = true;
            return this;
        }

        /**
         * É comum que os geradores de dígito precisem do complementar do módulo em vez
         * do módulo em sí. Então, a chamada desse método habilita a flag que é usada
         * no método mod para decidir se o resultado devolvido é o módulo puro ou seu
         * complementar.
         *
         * @return this
         */
        public final Builder complementarAoModulo() {
            this.complementar = true;
            return this;
        }

        /**
         * Troca por uma String caso encontre qualquer dos inteiros passados como argumento.
         *
         * @param substituto String para substituir
         * @param i          varargs de inteiros a serem substituídos
         * @return this
         */
        public final Builder trocandoPorSeEncontrar(String substituto, Integer... i) {

            substituicoes.clear();

            for (Integer integer : i) {
                substituicoes.put(integer, substituto);
            }

            return this;
        }

        /**
         * Há documentos em que os multiplicadores não usam todos os números de um intervalo
         * ou alteram sua ordem. Nesses casos, a lista de multiplicadores pode ser passada
         * através de varargs.
         *
         * @param multiplicadoresEmOrdem Sequência de inteiros com os multiplicadores em ordem
         * @return this
         */
        public final Builder comMultiplicadores(Integer... multiplicadoresEmOrdem) {
            this.multiplicadores.clear();
            this.multiplicadores.addAll(Arrays.asList(multiplicadoresEmOrdem));
            return this;
        }

        /**
         * Método responsável por criar o DigitoPara.
         * Este método inicializará os seguintes valores padrões:
         * <ul>
         * <li>multiplicadores:  2 a 9</li>
         * <li>módulo: 11</li>
         * </ul>
         *
         * @return A instância imutável de DigitoPara
         */
        public final DigitoPara build() {

            if (multiplicadores.size() == 0) {
                comMultiplicadoresDeAte(2, 9);
            }

            if (modulo == 0) {
                mod(11);
            }

            return new DigitoPara(this);
        }
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/Formatador.java
================================================
package br.com.concrete.canarinho.formatador;

import java.util.regex.Pattern;

/**
 * Interface de formatação. Formata valores completos. Útil caso receba o valor
 * desformatado de uma API.
 */
public interface Formatador {

    // Formatadores
    /**
     * Singleton de formatação de CEP.
     */
    Formatador CEP = new FormatadorBase(
            Padroes.CEP_FORMATADO,
            "$1-$2",
            Padroes.CEP_DESFORMATADO,
            "$1$2"
    );

    /**
     * Singleton de formatação de CPF.
     */
    Formatador CPF = new FormatadorBase(
            Padroes.CPF_FORMATADO,
            "$1.$2.$3-$4",
            Padroes.CPF_DESFORMATADO,
            "$1$2$3$4"
    );

    /**
     * Singleton de formatação de CNPJ.
     */
    Formatador CNPJ = new FormatadorBase(
            Padroes.CNPJ_FORMATADO,
            "$1.$2.$3/$4-$5",
            Padroes.CNPJ_DESFORMATADO,
            "$1$2$3$4$5"
    );

    /**
     * Singleton de formatação de CPF e CNPJ.
     */
    Formatador CPF_CNPJ = FormatadorCPFCNPJ.getInstance();

    /**
     * Singleton de formatação de valores monetários.
     */
    FormatadorValor VALOR = FormatadorValor.getInstance(false);

    /**
     * Singleton de formatação de valores monetários com símbolo do Real.
     */
    FormatadorValor VALOR_COM_SIMBOLO = FormatadorValor.getInstance(true);

    /**
     * Singleton de formatação de boletos bancários.
     */
    Formatador BOLETO = FormatadorBoleto.getInstance();

    /**
     * Singleton de formatação de telefones (DDD) número.
     */
    Formatador TELEFONE = FormatadorTelefone.getInstance();

    /**
     * Singleton de formatação de linha digitável. Transforma números do código de barras em linha
     * digitável.
     */
    Formatador LINHA_DIGITAVEL = FormatadorLinhaDigitavel.getInstance();

    /**
     * Formata um valor COMPLETO. Deve falhar caso o valor não esteja completo.
     *
     * @param value valor a formatar
     * @return REsultado da formatação
     */
    String formata(String value);

    /**
     * Desformata um valor.
     *
     * @param value Valor a desformatar
     * @return Resultado da desformatação
     */
    String desformata(String value);

    /**
     * Verifica se um parâmetro está formatado.
     *
     * @param value Valor para verificar
     * @return True se estiver formatado, falso caso contrário.
     */
    boolean estaFormatado(String value);

    /**
     * Verifica se um parâmetro pode ser formatado.
     *
     * @param value Valor para verificar
     * @return True se puder ser formatado, falso caso contrário.
     */
    boolean podeSerFormatado(String value);

    /**
     * Classe para guardar os padrões de experssões regulares usados no framework.
     */
    abstract class Padroes {

        public static final Pattern PADRAO_SOMENTE_NUMEROS = Pattern.compile("[^0-9]");
        public static final Pattern CEP_FORMATADO = Pattern.compile("(\\d{5})-(\\d{3})");
        public static final Pattern CEP_DESFORMATADO = Pattern.compile("(\\d{5})(\\d{3})");

        public static final Pattern CNPJ_FORMATADO = Pattern.compile(
                "(\\d{2})[.](\\d{3})[.](\\d{3})/(\\d{4})-(\\d{2})"
        );

        public static final Pattern CNPJ_DESFORMATADO = Pattern.compile(
                "(\\d{2})(\\d{3})(\\d{3})(\\d{4})(\\d{2})"
        );

        public static final Pattern CPF_FORMATADO = Pattern.compile(
                "(\\d{3})[.](\\d{3})[.](\\d{3})-(\\d{2})"
        );

        public static final Pattern CPF_DESFORMATADO = Pattern.compile(
                "(\\d{3})(\\d{3})(\\d{3})(\\d{2})"
        );

        private Padroes() {
        }
    }

}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorBase.java
================================================
package br.com.concrete.canarinho.formatador;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Classe utilitária para a implementação de todos os formatadores que precisam apenas da aplicação
 * de {@link Pattern}s.
 */
final class FormatadorBase implements Formatador {

    private final Pattern formatted;

    private final String formattedReplacement;

    private final Pattern unformatted;

    private final String unformattedReplacement;

    /**
     * Constrói um formatador que recebe a configuração de formatação e substituição.
     *
     * @param formatted              Pattern de regex para formatar o conteúdo
     * @param formattedReplacement   String com as posições de substituição dos grupos encontrados por regex
     * @param unformatted            Pattern de regex para DESformatar o conteúdo
     * @param unformattedReplacement String com as posições de substituição dos grupos encontrados por regex
     */
    FormatadorBase(
            Pattern formatted,
            String formattedReplacement,
            Pattern unformatted,
            String unformattedReplacement) {

        this.formatted = formatted;
        this.formattedReplacement = formattedReplacement;
        this.unformatted = unformatted;
        this.unformattedReplacement = unformattedReplacement;
    }

    @Override
    public final String formata(String value) throws IllegalArgumentException {

        if (value == null) {
            throw new IllegalArgumentException("Value may not be null.");
        }

        if (formatted.matcher(value).matches()) {
            return value;
        }

        return matchAndReplace(unformatted.matcher(value), formattedReplacement);
    }

    @Override
    public final String desformata(String value) throws IllegalArgumentException {

        if (value == null) {
            throw new IllegalArgumentException("Value may not be null.");
        }

        if (unformatted.matcher(value).matches()) {
            return value;
        }

        final Matcher matcher = formatted.matcher(value);
        return matchAndReplace(matcher, unformattedReplacement);
    }

    @Override
    public final boolean estaFormatado(String value) {

        if (value == null) {
            throw new IllegalArgumentException("value must not be null");
        }

        return formatted.matcher(value).matches();
    }

    @Override
    public final boolean podeSerFormatado(String value) {
        return value != null && unformatted.matcher(value).matches();
    }

    private String matchAndReplace(Matcher matcher, String replacement) {

        if (matcher.matches()) {
            return matcher.replaceAll(replacement);
        }

        throw new IllegalArgumentException("Valor não está formatado propriamente.");
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorBoleto.java
================================================
package br.com.concrete.canarinho.formatador;

import java.util.regex.Pattern;

/**
 * Formatador especializado para linha digitável de boletos bancários. Ele detecta automaticamente
 * se o boleto é do tipo tributos ou de conveniados. Caso o boleto comece com o número '8', então
 * é aplicada a formatação: 888888888888 888888888888 888888888888 888888888888. Caso contrário,
 * usa-se o padrão 99999.99999 99999.999999 99999.999999 9 99999999999999.
 */
public final class FormatadorBoleto implements Formatador {

    private static final Pattern TRIBUTO_FORMATADO =
            Pattern.compile("(\\d{12})\\s(\\d{12})\\s(\\d{12})\\s(\\d{12})");
    private static final Pattern TRIBUTO_DESFORMATADO =
            Pattern.compile("(\\d{12})(\\d{12})(\\d{12})(\\d{12})");

    private static final FormatadorBase FORMATADOR_TRIBUTOS = new FormatadorBase(
            TRIBUTO_FORMATADO,
            "$1 $2 $3 $4",
            TRIBUTO_DESFORMATADO,
            "$1$2$3$4"
    );

    private static final Pattern NORMAL_FORMATADO = Pattern.compile(
            "(\\d{5})[.](\\d{5})\\s(\\d{5})[.](\\d{6})\\s(\\d{5})[.](\\d{6})\\s(\\d)\\s(\\d{14})"
    );

    private static final Pattern NORMAL_DESFORMATADO = Pattern.compile(
            "(\\d{5})(\\d{5})(\\d{5})(\\d{6})(\\d{5})(\\d{6})(\\d{1})(\\d{14})"
    );

    private static final FormatadorBase FORMATADOR_NORMAL = new FormatadorBase(
            NORMAL_FORMATADO,
            "$1.$2 $3.$4 $5.$6 $7 $8",
            NORMAL_DESFORMATADO,
            "$1$2$3$4$5$6$7$8"
    );

    // No instance creation
    private FormatadorBoleto() {
    }

    @Override
    public String formata(String value) {

        if (ehTributo(value)) {
            return FORMATADOR_TRIBUTOS.formata(value);
        }

        return FORMATADOR_NORMAL.formata(value);
    }

    @Override
    public String desformata(String value) {

        if (ehTributo(value)) {
            return FORMATADOR_TRIBUTOS.desformata(value);
        }

        return FORMATADOR_NORMAL.desformata(value);
    }

    @Override
    public boolean estaFormatado(String value) {

        if (ehTributo(value)) {
            return FORMATADOR_TRIBUTOS.estaFormatado(value);
        }

        return FORMATADOR_NORMAL.estaFormatado(value);
    }

    @Override
    public boolean podeSerFormatado(String value) {

        if (ehTributo(value)) {
            return FORMATADOR_TRIBUTOS.podeSerFormatado(value);
        }

        return FORMATADOR_NORMAL.podeSerFormatado(value);
    }

    private boolean ehTributo(String value) {

        if (value == null) {
            throw new IllegalArgumentException("Valor não pode ser nulo");
        }

        return value.charAt(0) == '8';
    }

    static FormatadorBoleto getInstance() {
        return SingletonHolder.INSTANCE;
    }

    private static class SingletonHolder {
        private static final FormatadorBoleto INSTANCE = new FormatadorBoleto();
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorCEP.java
================================================
package br.com.concrete.canarinho.formatador;

/**
 * Formatador para CEP. Segue o padrão 99999-999.
 */
public final class FormatadorCEP implements Formatador {

    private FormatadorCEP() {
    }

    static FormatadorCEP getInstance() {
        return SingletonHolder.INSTANCE;
    }

    @Override
    public String formata(final String value) {
        return Formatador.CEP.formata(value);
    }

    @Override
    public String desformata(final String value) {
        return Formatador.CEP.desformata(value);
    }

    @Override
    public boolean estaFormatado(final String value) {
        return Formatador.CEP.estaFormatado(value);
    }

    @Override
    public boolean podeSerFormatado(final String value) {
        if (value == null) {
            return false;
        }

        return Formatador.CEP.podeSerFormatado(value);
    }

    private static class SingletonHolder {
        private static final FormatadorCEP INSTANCE = new FormatadorCEP();
    }
}

================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorCPFCNPJ.java
================================================
package br.com.concrete.canarinho.formatador;

/**
 * Formatador para CPF e CNPJ no mesmo campo. Formata como CPF até 11 dígitos numéricos. Depois
 * formata como CNPJ.
 */
public final class FormatadorCPFCNPJ implements Formatador {

    private FormatadorCPFCNPJ() {
    }

    static FormatadorCPFCNPJ getInstance() {
        return SingletonHolder.INSTANCE;
    }

    @Override
    public String formata(final String value) {
        if (ehCpf(value)) {
            return Formatador.CPF.formata(value);
        }

        return Formatador.CNPJ.formata(value);
    }

    @Override
    public String desformata(final String value) {
        if (ehCpf(value)) {
            return Formatador.CPF.desformata(value);
        }

        return Formatador.CNPJ.desformata(value);
    }

    @Override
    public boolean estaFormatado(final String value) {
        if (ehCpf(value)) {
            return Formatador.CPF.estaFormatado(value);
        }

        return Formatador.CNPJ.estaFormatado(value);
    }

    @Override
    public boolean podeSerFormatado(final String value) {
        if (value == null) {
            return false;
        }

        if (ehCpf(value)) {
            return Formatador.CPF.podeSerFormatado(value);
        }

        return Formatador.CNPJ.podeSerFormatado(value);
    }

    private boolean ehCpf(String value) {
        if (value == null) {
            throw new IllegalArgumentException("Valor não pode ser nulo");
        }

        final String desformatado = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(value)
                .replaceAll("");
        return desformatado.length() < 12;
    }

    private static class SingletonHolder {
        private static final FormatadorCPFCNPJ INSTANCE = new FormatadorCPFCNPJ();
    }
}

================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorLinhaDigitavel.java
================================================
package br.com.concrete.canarinho.formatador;

import br.com.concrete.canarinho.DigitoPara;
import br.com.concrete.canarinho.validator.ValidadorBoleto;

/**
 * Transforma a linha digitável de um boleto em um código de boleto e vice-versa. Use o metodo
 * {@link #formata(String)} para transformar a linha digitavel em boleto e
 * {@link #desformata(String)}.
 * Para verificar se um valor esta em linha digitável ou em boleto, usar os métodos:
 * <ul>
 * <li>{@link #estaFormatado(String)}: indicará se está em formata de boleto</li>
 * <li>{@link #podeSerFormatado(String)}: indicará se é uma linha digitável</li>
 * </ul>
 */
public final class FormatadorLinhaDigitavel implements Formatador {

    private FormatadorLinhaDigitavel() {
    }

    static FormatadorLinhaDigitavel getInstance() {
        return SingletonHolder.INSTANCE;
    }

    @Override
    public String formata(String value) {

        if (value == null || value.length() != 44) {
            throw new IllegalArgumentException("Linha digitável deve conter 44 caracteres. "
                    + "Valor possui " + value + " caracteres");
        }

        if (value.startsWith("8")) {

            final String primeiroBloco = value.substring(0, 11);
            final String segundoBloco = value.substring(11, 22);
            final String terceiroBloco = value.substring(22, 33);
            final String quartoBloco = value.substring(33, 44);

            // o terceiro dígito é o de valor real que define se será mod 10 ou mod 11
            final boolean ehMod10 = value.charAt(2) == '6' || value.charAt(2) == '7';
            final DigitoPara mod = ehMod10 ? ValidadorBoleto.MOD_10 : ValidadorBoleto.MOD_11;

            final String primeiroDigito = mod.calcula(primeiroBloco);
            final String segundoDigito = mod.calcula(segundoBloco);
            final String terceiroDigito = mod.calcula(terceiroBloco);
            final String quartoDigito = mod.calcula(quartoBloco);

            return primeiroBloco + primeiroDigito + segundoBloco + segundoDigito
                    + terceiroBloco + terceiroDigito + quartoBloco + quartoDigito;
        }

        String primeiroBloco = value.substring(0, 4); // 4
        String segundoBloco = value.substring(4, 19); // 15
        String terceiroBloco = value.substring(19, 24); // 5
        String quartoBloco = value.substring(24, 34); // 10
        String quintoBloco = value.substring(34, 44); // 10

        // 1 - 3 - 4 - 5 - 2
        final StringBuilder codigoOrdenado = new StringBuilder(primeiroBloco)
                .append(terceiroBloco)
                .append(quartoBloco)
                .append(quintoBloco)
                .append(segundoBloco);

        primeiroBloco = codigoOrdenado.substring(0, 9);
        segundoBloco = codigoOrdenado.substring(9, 19);
        terceiroBloco = codigoOrdenado.substring(19, 29);
        quartoBloco = codigoOrdenado.substring(29);

        final String primeiroDigito = ValidadorBoleto.MOD_10.calcula(primeiroBloco);
        final String segundoDigito = ValidadorBoleto.MOD_10.calcula(segundoBloco);
        final String terceiroDigito = ValidadorBoleto.MOD_10.calcula(terceiroBloco);

        return primeiroBloco + primeiroDigito + segundoBloco + segundoDigito
                + terceiroBloco + terceiroDigito + quartoBloco;
    }

    @Override
    public String desformata(String valor) {

        if (valor == null || "".equals(valor)) {
            throw new IllegalArgumentException("Valor não pode estar nulo.");
        }

        String valorDesformatadao = Padroes.PADRAO_SOMENTE_NUMEROS.matcher(valor)
                .replaceAll("");

        if (valorDesformatadao.charAt(0) == '8') {

            if (valorDesformatadao.length() != 48) {
                throw new IllegalArgumentException(
                        "Valor para boletos que iniciam com 8 deve conter 48 dígitos"
                );
            }

            final StringBuilder builder = new StringBuilder(valorDesformatadao);

            final String primeiroBloco = builder.substring(0, 11);
            final String segundoBloco = builder.substring(12, 23);
            final String terceiroBloco = builder.substring(24, 35);
            final String quartoBloco = builder.substring(36, 47);

            return "" + primeiroBloco + segundoBloco + terceiroBloco + quartoBloco;
        }

        if (valorDesformatadao.length() != 47) {
            throw new IllegalArgumentException("Valor para boletos deve conter 47 digitos");
        }

        String primeiroBloco = valorDesformatadao.substring(0, 9);
        String segundoBloco = valorDesformatadao.substring(10, 20);
        String terceiroBloco = valorDesformatadao.substring(21, 31);
        String quartoBloco = valorDesformatadao.substring(32, valorDesformatadao.length());

        final StringBuilder boletoOrdenado = new StringBuilder(primeiroBloco)
                .append(segundoBloco)
                .append(terceiroBloco)
                .append(quartoBloco);

        // 1 - 3 - 4 - 5 - 2
        primeiroBloco = boletoOrdenado.substring(0, 4); // 4
        segundoBloco = boletoOrdenado.substring(29, 44); // 15
        terceiroBloco = boletoOrdenado.substring(4, 9); // 5
        quartoBloco = boletoOrdenado.substring(9, 19); // 10
        final String quintoBloco = boletoOrdenado.substring(19, 29); // 10

        return "" + primeiroBloco + segundoBloco + terceiroBloco + quartoBloco + quintoBloco;
    }

    @Override
    public boolean estaFormatado(String value) {
        return Formatador.BOLETO.estaFormatado(value);
    }

    @Override
    public boolean podeSerFormatado(String value) {
        return Padroes.PADRAO_SOMENTE_NUMEROS.matcher(value)
                .replaceAll("")
                .length() == 44;
    }

    private static class SingletonHolder {
        private static final FormatadorLinhaDigitavel INSTANCE = new FormatadorLinhaDigitavel();
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorTelefone.java
================================================
package br.com.concrete.canarinho.formatador;

import java.util.regex.Pattern;

/**
 * Formata no padrão de telefone brasileiro: (99) 99999-9999 ou (99) 9999-9999.
 */
public final class FormatadorTelefone implements Formatador {

    private static final Pattern NOVE_DIGITOS_FORMATADO = Pattern.compile(
            "\\((\\d{2})\\)\\s(\\d{5})-(\\d{4})"
    );

    private static final Pattern NOVE_DIGITOS_DESFORMATADO = Pattern.compile(
            "(\\d{2})(\\d{5})(\\d{4})"
    );

    private static final Pattern OITO_DIGITOS_FORMATADO = Pattern.compile(
            "\\((\\d{2})\\)\\s(\\d{4})-(\\d{4})"
    );

    private static final Pattern OITO_DIGITOS_DESFORMATADO = Pattern.compile(
            "(\\d{2})(\\d{4})(\\d{4})"
    );

    private static final FormatadorBase FORMATADOR_NOVE_DIGITOS = new FormatadorBase(
            NOVE_DIGITOS_FORMATADO,
            "($1) $2-$3",
            NOVE_DIGITOS_DESFORMATADO,
            "$1$2$3"
    );

    private static final FormatadorBase FORMATADOR_OITO_DIGITOS = new FormatadorBase(
            OITO_DIGITOS_FORMATADO,
            "($1) $2-$3",
            OITO_DIGITOS_DESFORMATADO,
            "$1$2$3"
    );

    private FormatadorTelefone() {
    }

    static FormatadorTelefone getInstance() {
        return SingletonHolder.INSTANCE;
    }

    @Override
    public String formata(String value) {
        if (ehNoveDigitos(value)) {
            return FORMATADOR_NOVE_DIGITOS.formata(value);
        }

        return FORMATADOR_OITO_DIGITOS.formata(value);
    }

    @Override
    public String desformata(String value) {
        if (ehNoveDigitos(value)) {
            return FORMATADOR_NOVE_DIGITOS.desformata(value);
        }

        return FORMATADOR_OITO_DIGITOS.desformata(value);
    }

    @Override
    public boolean estaFormatado(String value) {
        if (ehNoveDigitos(value)) {
            return FORMATADOR_NOVE_DIGITOS.estaFormatado(value);
        }

        return FORMATADOR_OITO_DIGITOS.estaFormatado(value);
    }

    @Override
    public boolean podeSerFormatado(String value) {
        if (ehNoveDigitos(value)) {
            return FORMATADOR_NOVE_DIGITOS.podeSerFormatado(value);
        }

        return FORMATADOR_OITO_DIGITOS.podeSerFormatado(value);
    }

    private boolean ehNoveDigitos(String value) {
        if (value == null) {
            throw new IllegalArgumentException("Valor não pode ser nulo");
        }

        return Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(value)
                .replaceAll("")
                .length() > 10;
    }

    private static class SingletonHolder {
        private static final FormatadorTelefone INSTANCE = new FormatadorTelefone();
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorValor.java
================================================
package br.com.concrete.canarinho.formatador;

import android.os.Build;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.Locale;
import java.util.regex.Pattern;

/**
 * Formatador de valores monetários. Possui duas versões:
 * <ul>
 * <li>Com símbolo do Real {@link Formatador#VALOR_COM_SIMBOLO}</li>
 * <li>Sem símbolo do Real {@link Formatador#VALOR}</li>
 * </ul>
 */
public final class FormatadorValor implements Formatador {

    private static final DecimalFormat FORMATADOR_MOEDA = (DecimalFormat)
            NumberFormat.getCurrencyInstance(new Locale("pt", "BR"));
    private static final Pattern PADRAO_DECIMAL = Pattern
            .compile("^\\d+(\\.\\d{1,2})?$");
    private static final Pattern PADRAO_MOEDA = Pattern
            .compile("\\d{1,3}(\\.\\d{3})*(,\\d{2})?");

    private static final String SIMBOLO_REAL = "R$ ";

    static {
        final DecimalFormatSymbols decimalSymbols = FORMATADOR_MOEDA.getDecimalFormatSymbols();
        decimalSymbols.setCurrencySymbol("");
        FORMATADOR_MOEDA.setMinimumFractionDigits(2);
        FORMATADOR_MOEDA.setDecimalFormatSymbols(decimalSymbols);
        FORMATADOR_MOEDA.setNegativePrefix("-");
        FORMATADOR_MOEDA.setNegativeSuffix("");
        FORMATADOR_MOEDA.setPositivePrefix("");
        FORMATADOR_MOEDA.setParseBigDecimal(true);
    }

    private final boolean adicionaSimboloReal;

    // No instance creation
    private FormatadorValor(boolean comSimboloReal) {
        adicionaSimboloReal = comSimboloReal;
    }

    /**
     * Busca uma instância do formatador com símbolo ou sem.
     *
     * @param comSimboloReal Flag para saber qual instância buscar.
     * @return FormatadorValor de acordo com a flag
     */
    static FormatadorValor getInstance(boolean comSimboloReal) {
        return comSimboloReal
                ? SingletonHolder.INSTANCE_COM_SIMBOLO
                : SingletonHolder.INSTANCE_SEM_SIMBOLO;
    }

    @Override
    public String formata(String value) {
        final String resultado = FORMATADOR_MOEDA.format(new BigDecimal(value));
        return adicionaSimboloReal ? SIMBOLO_REAL + resultado : resultado;
    }

    @Override
    public String desformata(String value) {

        String realValue = value;
        if (value.startsWith(SIMBOLO_REAL)) {
            int offset = value.indexOf(SIMBOLO_REAL) + SIMBOLO_REAL.length();
            realValue = value.substring(offset);
        }

        final BigDecimal valor;
        if (Build.VERSION.SDK_INT < 28) {
            valor = (BigDecimal) FORMATADOR_MOEDA.parse(realValue,
                    new ParsePosition(0));
        } else {
            // Implementando o parse manual devido a um bug na API 28
            valor = new BigDecimal(realValue.replaceAll("\\.", "")
                    .replace(",", "."));
        }
        return valor.toPlainString();
    }

    @Override
    public boolean estaFormatado(String value) {

        if (value == null) {
            throw new IllegalArgumentException("Valor não pode ser nulo");
        }

        return PADRAO_MOEDA.matcher(value).matches();
    }

    @Override
    public boolean podeSerFormatado(String value) {

        if (value == null) {
            throw new IllegalArgumentException("Valor não pode ser nulo");
        }

        return PADRAO_DECIMAL.matcher(value).matches();
    }

    private static class SingletonHolder {
        private static final FormatadorValor INSTANCE_SEM_SIMBOLO = new FormatadorValor(false);
        private static final FormatadorValor INSTANCE_COM_SIMBOLO = new FormatadorValor(true);
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/Validador.java
================================================
package br.com.concrete.canarinho.validator;

import android.text.Editable;

/**
 * Interface de validação de campos. Há basicamente duas formas de validação:
 * <ul>
 * <li>Uma {@link String} completa</li>
 * <li>Um {@link Editable} e um {@link br.com.concrete.canarinho.validator.Validador.ResultadoParcial}</li>
 * </ul>
 * No primeiro caso o retorno será: true ou false. No segundo caso, o resultado será sempre
 * atualizado no objeto {@link br.com.concrete.canarinho.validator.Validador.ResultadoParcial} passado.
 */
public interface Validador {

    /**
     * Referência para o singleton de validação de CPF.
     */
    Validador CPF = ValidadorCPF.getInstance();

    /**
     * Referência para o singleton de validação de CNPJ.
     */
    Validador CNPJ = ValidadorCNPJ.getInstance();

    /**
     * Referência para o singleton de validação de boleto.
     */
    Validador BOLETO = ValidadorBoleto.getInstance();

    /**
     * Referência para o singleton de validação de telefone.
     */
    Validador TELEFONE = ValidadorTelefone.getInstance();

    /**
     * Referência para o singleton de validação de CEP.
     */
    Validador CEP = ValidadorCEP.getInstance();

    /**
     * Valida uma {@link String} completa.
     *
     * @param valor Valor a ser validado
     * @return true se estiver válida e false caso contrário
     */
    boolean ehValido(String valor);

    /**
     * Valida um {@link Editable} retornando o
     * {@link br.com.concrete.canarinho.validator.Validador.ResultadoParcial}.
     *
     * @param valor            Editable
     * @param resultadoParcial Objeto com o estado da validação
     * @return Objeto com o estado da validação atualizado
     */
    ResultadoParcial ehValido(Editable valor, ResultadoParcial resultadoParcial);

    /**
     * Value Object com o estado da validação.
     */
    class ResultadoParcial {

        private boolean valido;
        private boolean parcialmenteValido = true;
        private String mensagem;

        public boolean isValido() {
            return valido;
        }

        public boolean isParcialmenteValido() {
            return parcialmenteValido;
        }

        public String getMensagem() {
            return mensagem;
        }

        /**
         * Ajusta a validação com o valor de "totalmente válido".
         *
         * @param valido Flag totalmenteValido
         * @return Fluent Interface "this"
         */
        public ResultadoParcial totalmenteValido(boolean valido) {
            this.valido = valido;
            return this;
        }

        /**
         * Ajusta a validação com o valor de "totalmente válido".
         *
         * @param parcialmenteValido Flag parcialmenteValido
         * @return Fluent Interface "this"
         */
        public ResultadoParcial parcialmenteValido(boolean parcialmenteValido) {
            this.parcialmenteValido = parcialmenteValido;
            return this;
        }

        /**
         * Ajusta a mensagem de erro.
         *
         * @param mensagem Mensagem usada na apresentação do erro.
         * @return Fluent Interface "this"
         */
        public ResultadoParcial mensagem(String mensagem) {
            this.mensagem = mensagem;
            return this;
        }
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorBoleto.java
================================================
package br.com.concrete.canarinho.validator;

import android.text.Editable;
import android.text.SpannableStringBuilder;

import br.com.concrete.canarinho.DigitoPara;
import br.com.concrete.canarinho.formatador.Formatador;

import java.util.regex.Pattern;

/**
 * Implementação de @{link Validador} para boleto.
 *
 * @see Validador
 */
public final class ValidadorBoleto implements Validador {

    /**
     * Instância de módulo 10 para cálculo de digito verificador de boleto.
     */
    public static final DigitoPara MOD_10 = new DigitoPara.Builder()
            .mod(10)
            .comMultiplicadores(2, 1)
            .somandoIndividualmente()
            .trocandoPorSeEncontrar("0", 10)
            .complementarAoModulo()
            .build();

    /**
     * Instância de módulo 11 para cálculo de digito verificador de boleto.
     */
    public static final DigitoPara MOD_11 = new DigitoPara.Builder()
            .trocandoPorSeEncontrar("0", 10, 11)
            .complementarAoModulo()
            .build();

    private static final Pattern PADRAO_PARA_LIMPAR = Pattern.compile("[\\s.]");
    private static final Pattern PADRAO_APENAS_NUMEROS = Pattern.compile("[\\d]*");

    // No instance creation
    private ValidadorBoleto() {
    }

    public static ValidadorBoleto getInstance() {
        return SingletonHolder.INSTANCE;
    }

    @Override
    public boolean ehValido(String valor) {

        if (valor == null) {
            throw new IllegalArgumentException("Campos não podem ser nulos");
        }

        String valorSemFormatacao = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(valor).replaceAll("");
        return ehValido(new SpannableStringBuilder(valorSemFormatacao), new ResultadoParcial()).isValido();
    }

    @Override
    public ResultadoParcial ehValido(Editable valor, ResultadoParcial resultadoParcial) {

        if (resultadoParcial == null || valor == null) {
            throw new IllegalArgumentException("Campos não podem ser nulos");
        }

        final String valorDesformatado = PADRAO_PARA_LIMPAR.matcher(valor).replaceAll("");

        if (!PADRAO_APENAS_NUMEROS.matcher(valorDesformatado).matches()) {
            throw new IllegalArgumentException("Apenas números, '.' e espaços são válidos");
        }

        resultadoParcial.totalmenteValido(false);

        if (valorDesformatado.length() == 0) {
            return resultadoParcial
                    .parcialmenteValido(true)
                    .mensagem("");
        }

        return ehTributo(valorDesformatado)
                ? validaTributo(valorDesformatado, resultadoParcial)
                : validaNormal(valorDesformatado, resultadoParcial);
    }

    private ResultadoParcial validaNormal(String valor, ResultadoParcial resultadoParcial) {

        if (!validaBloco(valor, resultadoParcial, MOD_10, 10, 0, "Primeiro")) {
            return resultadoParcial;
        }

        if (!validaBloco(valor, resultadoParcial, MOD_10, 21, 10, "Segundo")) {
            return resultadoParcial;
        }

        if (!validaBloco(valor, resultadoParcial, MOD_10, 32, 21, "Terceiro")) {
            return resultadoParcial;
        }

        if (valor.length() < 47) {
            return resultadoParcial;
        }

        return resultadoParcial.parcialmenteValido(true).totalmenteValido(true);
    }

    private ResultadoParcial validaTributo(String valor, ResultadoParcial resultadoParcial) {

        if (valor.length() < 3) {
            return resultadoParcial.parcialmenteValido(true);
        }

        // A validação precisa levar em conta o terceiro dígito
        final boolean ehMod10 = valor.charAt(2) == '6' || valor.charAt(2) == '7';
        final DigitoPara digitoPara = ehMod10 ? MOD_10 : MOD_11;

        if (!validaBloco(valor, resultadoParcial, digitoPara, 12, 0, "Primeiro")) {
            return resultadoParcial;
        }

        if (!validaBloco(valor, resultadoParcial, digitoPara, 24, 12, "Segundo")) {
            return resultadoParcial;
        }

        if (!validaBloco(valor, resultadoParcial, digitoPara, 36, 24, "Terceiro")) {
            return resultadoParcial;
        }

        if (!validaBloco(valor, resultadoParcial, digitoPara, 48, 36, "Quarto")) {
            return resultadoParcial;
        }

        // Retorna bloco válido
        return resultadoParcial.parcialmenteValido(true).totalmenteValido(true);
    }

    private boolean ehTributo(CharSequence valor) {
        return valor.charAt(0) == '8';
    }

    private boolean validaBloco(String valor, ResultadoParcial resultadoParcial, DigitoPara mod,
                                int tamanhoMinimo, int st, String mensagem) {

        if (valor.length() < tamanhoMinimo) {
            resultadoParcial.parcialmenteValido(true);
            return false;
        }

        final int end = tamanhoMinimo - 1;
        // Valida primeiro bloco
        final char digito = mod.calcula(valor.subSequence(st, end).toString()).charAt(0);

        if (digito != valor.charAt(end)) {
            return resultadoParcial
                    .mensagem(mensagem + " bloco inválido")
                    .parcialmenteValido(false)
                    .isParcialmenteValido();
        }

        return true;
    }

    private static class SingletonHolder {
        private static final ValidadorBoleto INSTANCE = new ValidadorBoleto();
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorCEP.java
================================================
package br.com.concrete.canarinho.validator;

import android.text.Editable;

import br.com.concrete.canarinho.formatador.Formatador;

/**
 * Implementação de @{link Validador} para CEP (Código de endereçamento Postal).
 *
 * @see Validador
 */
public final class ValidadorCEP implements Validador {

    // No instance creation
    private ValidadorCEP() {
    }

    public static ValidadorCEP getInstance() {
        return SingletonHolder.INSTANCE;
    }

    @Override
    public boolean ehValido(String valor) {

        if (valor == null || valor.length() < 8) {
            return false;
        }

        final String desformatado = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(valor).replaceAll("");

        return desformatado.length() == 8;
    }

    @Override
    public ResultadoParcial ehValido(Editable valor, ResultadoParcial resultadoParcial) {

        if (resultadoParcial == null || valor == null) {
            throw new IllegalArgumentException("Valores não podem ser nulos");
        }

        final String desformatado = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(valor).replaceAll("");

        if (!ehValido(desformatado)) {
            return resultadoParcial
                    .parcialmenteValido(desformatado.length() < 8)
                    .mensagem("CEP inválido")
                    .totalmenteValido(false);
        }

        return resultadoParcial
                .parcialmenteValido(true)
                .totalmenteValido(true);
    }

    private static class SingletonHolder {
        private static final ValidadorCEP INSTANCE = new ValidadorCEP();
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorCNPJ.java
================================================
package br.com.concrete.canarinho.validator;

import android.text.Editable;

import br.com.concrete.canarinho.DigitoPara;
import br.com.concrete.canarinho.formatador.Formatador;

/**
 * Implementação de @{link Validador} para CNPJ.
 *
 * @see Validador
 */
public final class ValidadorCNPJ implements Validador {

    private static final DigitoPara DIGITO_PARA_CNPJ = new DigitoPara.Builder()
            .complementarAoModulo()
            .trocandoPorSeEncontrar("0", 10, 11)
            .build();

    // No instance creation
    private ValidadorCNPJ() {
    }

    public static ValidadorCNPJ getInstance() {
        return SingletonHolder.INSTANCE;
    }

    @Override
    public boolean ehValido(String value) {

        if (value == null || value.length() < 14) {
            return false;
        }

        final String desformatado = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(value).replaceAll("");

        if (desformatado.length() != 14) {
            return false;
        }

        final String cnpjSemDigitos = desformatado.substring(0, desformatado.length() - 2);
        final String digitos = desformatado.substring(desformatado.length() - 2);

        final String dig1 = DIGITO_PARA_CNPJ.calcula(cnpjSemDigitos);
        final String dig2 = DIGITO_PARA_CNPJ.calcula(cnpjSemDigitos + dig1);

        return (dig1 + dig2).equals(digitos);
    }

    @Override
    public ResultadoParcial ehValido(Editable valor, ResultadoParcial resultadoParcial) {

        if (resultadoParcial == null || valor == null) {
            throw new IllegalArgumentException("Valores não podem ser nulos");
        }

        final String desformatado = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(valor).replaceAll("");

        if (!ehValido(desformatado)) {
            return resultadoParcial
                    .parcialmenteValido(desformatado.length() < 14)
                    .mensagem("CNPJ inválido")
                    .totalmenteValido(false);
        }

        return resultadoParcial
                .parcialmenteValido(true)
                .totalmenteValido(true);
    }

    private static class SingletonHolder {
        private static final ValidadorCNPJ INSTANCE = new ValidadorCNPJ();
    }
}

================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorCPF.java
================================================
package br.com.concrete.canarinho.validator;

import android.text.Editable;

import br.com.concrete.canarinho.DigitoPara;
import br.com.concrete.canarinho.formatador.Formatador;

/**
 * Implementação de @{link Validador} para CPF.
 *
 * @see Validador
 */
public final class ValidadorCPF implements Validador {

    private static final DigitoPara DIGITO_PARA_CPF = new DigitoPara.Builder()
            .comMultiplicadoresDeAte(2, 11)
            .complementarAoModulo()
            .trocandoPorSeEncontrar("0", 10, 11)
            .build();

    // No instance creation
    private ValidadorCPF() {
    }

    static ValidadorCPF getInstance() {
        return SingletonHolder.INSTANCE;
    }

    @Override
    public boolean ehValido(String value) {

        if (value == null || value.length() < 11) {
            return false;
        }

        final String desformatado = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(value).replaceAll("");

        if (desformatado.length() != 11) {
            return false;
        }

        if (estaNaListaNegra(desformatado)) {
            return false;
        }

        final String cpfSemDigito = desformatado.substring(0, desformatado.length() - 2);
        final String digitos = desformatado.substring(desformatado.length() - 2);

        final String dig1 = DIGITO_PARA_CPF.calcula(cpfSemDigito);
        final String dig2 = DIGITO_PARA_CPF.calcula(cpfSemDigito + dig1);

        return (dig1 + dig2).equals(digitos);
    }

    @Override
    public ResultadoParcial ehValido(Editable valor, ResultadoParcial resultadoParcial) {

        if (resultadoParcial == null || valor == null) {
            throw new IllegalArgumentException("Valores não podem ser nulos");
        }

        final String desformatado = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(valor).replaceAll("");

        if (!ehValido(desformatado)) {
            return resultadoParcial
                    .parcialmenteValido(desformatado.length() < 11)
                    .mensagem("CPF inválido")
                    .totalmenteValido(false);
        }

        return resultadoParcial
                .parcialmenteValido(true)
                .totalmenteValido(true);
    }

    // De acordo ao cálculo dos digitos verificadores, os CPFs abaixo são válidos, entretanto os mesmo
    // são considerados inválidos pela Receita Federal
    // 00000000000, 11111111111, 22222222222, 33333333333, 44444444444, 55555555555,
    // 66666666666, 77777777777, 88888888888, 99999999999, 12345678909
    private boolean estaNaListaNegra(String valor) {

        boolean igual = true;

        for (int i = 1; i < 11 && igual; i++) {
            if (valor.charAt(i) != valor.charAt(0)) {
                igual = false;
            }
        }

        return igual || valor.equals("12345678909");
    }

    private static class SingletonHolder {
        private static final ValidadorCPF INSTANCE = new ValidadorCPF();
    }
}

================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorCPFCNPJ.java
================================================
package br.com.concrete.canarinho.validator;

import android.text.Editable;

import br.com.concrete.canarinho.formatador.Formatador;

public final class ValidadorCPFCNPJ implements Validador {

    // No instance creation
    private ValidadorCPFCNPJ() {
    }

    public static ValidadorCPFCNPJ getInstance() {
        return SingletonHolder.INSTANCE;
    }

    @Override
    public boolean ehValido(final String valor) {
        if (valor == null || (valor.length() != 11 && valor.length() != 14)) {
            return false;
        }

        if (ehCpf(valor)) {
            return ValidadorCPF.getInstance().ehValido(valor);
        }

        return ValidadorCNPJ.getInstance().ehValido(valor);
    }

    @Override
    public ResultadoParcial ehValido(final Editable valor, final ResultadoParcial resultadoParcial) {
        if (resultadoParcial == null || valor == null) {
            throw new IllegalArgumentException("Valores não podem ser nulos");
        }

        if (ehCpf(valor.toString())) {
            return ValidadorCPF.getInstance().ehValido(valor, resultadoParcial);
        }

        return ValidadorCNPJ.getInstance().ehValido(valor, resultadoParcial);
    }

    private boolean ehCpf(String valor) {
        if (valor == null) {
            throw new IllegalArgumentException("Valor não pode ser nulo");
        }

        final String desformatado = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(valor).replaceAll("");
        return desformatado.length() < 12;
    }

    private static class SingletonHolder {
        private static final ValidadorCPFCNPJ INSTANCE = new ValidadorCPFCNPJ();
    }
}

================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorTelefone.java
================================================
package br.com.concrete.canarinho.validator;

import android.text.Editable;

import br.com.concrete.canarinho.formatador.Formatador;

public final class ValidadorTelefone implements Validador {

    // No instance creation
    private ValidadorTelefone() {
    }

    public static ValidadorTelefone getInstance() {
        return SingletonHolder.INSTANCE;
    }

    @Override
    public boolean ehValido(String valor) {
        if (valor == null || valor.length() < 10) {
            return false;
        }

        final String desformatado = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(valor).replaceAll("");

        return desformatado.length() == 10 || desformatado.length() == 11;
    }

    @Override
    public ResultadoParcial ehValido(Editable valor, ResultadoParcial resultadoParcial) {
        if (resultadoParcial == null || valor == null) {
            throw new IllegalArgumentException("Valores não podem ser nulos");
        }

        final String desformatado = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(valor).replaceAll("");

        if (!ehValido(desformatado)) {
            return resultadoParcial
                    .parcialmenteValido(desformatado.length() < 11)
                    .mensagem("Telefone inválido")
                    .totalmenteValido(false);
        }

        return resultadoParcial
                .parcialmenteValido(true)
                .totalmenteValido(true);
    }

    private static class SingletonHolder {
        private static final ValidadorTelefone INSTANCE = new ValidadorTelefone();
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/BaseCanarinhoTextWatcher.java
================================================
package br.com.concrete.canarinho.watcher;

import android.text.Editable;
import android.text.Selection;
import android.text.TextWatcher;

import br.com.concrete.canarinho.formatador.Formatador;
import br.com.concrete.canarinho.validator.Validador;
import br.com.concrete.canarinho.watcher.evento.EventoDeValidacao;

/**
 * Classe base para Watchers que possuem máscara e efetuam validação.
 *
 * @see Validador
 */
public abstract class BaseCanarinhoTextWatcher implements TextWatcher {

    private boolean mudancaInterna = false;
    private int tamanhoAnterior = 0;
    private EventoDeValidacao eventoDeValidacao;

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        // Não faz nada aqui
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // Não faz nada aqui
    }

    public boolean isMudancaInterna() {
        return mudancaInterna;
    }

    @SuppressWarnings("unchecked")
    public <T extends EventoDeValidacao> T getEventoDeValidacao() {
        return (T) eventoDeValidacao;
    }

    public void setEventoDeValidacao(EventoDeValidacao eventoDeValidacao) {
        this.eventoDeValidacao = eventoDeValidacao;
    }

    /**
     * Utilitário para implementações de Watcher customizadas.
     * Verifica se a ação foi de apagar um caracter
     *
     * @param s o Editable em uso
     * @return True case a ação foi uma deleção e false caso contrário
     */
    protected boolean isApagouCaracter(Editable s) {
        return tamanhoAnterior > s.length();
    }

    /**
     * Utilitário para implementações de Watcher customizadas.
     * Utilitário para atualizar o Editable com flags de atualização.
     *
     * @param validador        Validador utilizado para verificar o input
     * @param resultadoParcial Objeto de validação
     * @param s                Editable em uso
     * @param builder          Valor atual da string
     */
    // Usa o Editable para atualizar o Editable
    // O cursor SEMPRE sera posicionado no final do conteúdo
    protected void atualizaTexto(Validador validador, Validador.ResultadoParcial resultadoParcial,
                                 Editable s, StringBuilder builder) {

        tamanhoAnterior = builder.length();
        mudancaInterna = true;
        s.replace(0, s.length(), builder, 0, builder.length());

        if (builder.toString().equals(s.toString())) {
            // TODO: estudar implantar a manutenção da posição do cursor
            Selection.setSelection(s, builder.length());
        }

        efetuaValidacao(validador, resultadoParcial, s);
        mudancaInterna = false;
    }

    /**
     * Método que efetua a validação em si.
     *
     * @param validador        Validador utilizado para verificar o input
     * @param resultadoParcial Objeto de validação
     * @param s                Editable em uso
     */
    // CUIDADO AO ATUALIZAR O Editable AQUI!!!
    protected void efetuaValidacao(Validador validador, Validador.ResultadoParcial resultadoParcial, Editable s) {

        if (validador == null) {
            return;
        }

        if (eventoDeValidacao == null) {
            validador.ehValido(s.toString());
            return;
        }

        validador.ehValido(s, resultadoParcial);

        if (!resultadoParcial.isParcialmenteValido()) {
            eventoDeValidacao.invalido(s.toString(), resultadoParcial.getMensagem());
        } else if (!resultadoParcial.isValido()) {
            eventoDeValidacao.parcialmenteValido(s.toString());
        } else {
            eventoDeValidacao.totalmenteValido(s.toString());
        }
    }

    /**
     * Implementação genérica para adição ou remoção de caracter.
     *
     * @param s       Editable em uso
     * @param mascara máscara do Watcher
     * @return Builder com o valor final
     */
    protected StringBuilder trataAdicaoRemocaoDeCaracter(Editable s, char[] mascara) {
        return isApagouCaracter(s)
                ? trataRemocaoDeCaracter(s, mascara)
                : trataAdicaoDeCaracter(s, mascara);
    }

    private StringBuilder trataAdicaoDeCaracter(Editable s, char[] mascara) {
        return carregarMascara(s.toString(), mascara);
    }

    // Só é chamado após uma deleção, portanto, é seguro chamar mascara[s.length()]
    private StringBuilder trataRemocaoDeCaracter(Editable s, char[] mascara) {
        final StringBuilder builder = new StringBuilder(s);

        // Obtém a posição do último caracter excluído
        final int posicaoUltimoCaracter = mascara.length > s.length() ? s.length() : mascara.length - 1;

        // Verifica se o último caracter que foi excluído fazia parte da máscara
        final boolean ultimoCaracterEraMascara = mascara[posicaoUltimoCaracter] != '#';

        // Se o último caracter excluído fazia parte da máscara,
        // deve excluir até o primeiro caracter que não faz parte da máscara
        if (ultimoCaracterEraMascara) {
            boolean encontrouCaracterValido = false;
            while (builder.length() > 0 && !encontrouCaracterValido) {
                encontrouCaracterValido = mascara[builder.length() - 1] == '#';
                builder.deleteCharAt(builder.length() - 1);
            }
        }

        // Caso haja mais de um caracter de formatação (da máscara) faz um loop
        // até chegar em um caracter que não seja de formatação
        while (builder.length() > 0 && mascara[builder.length() - 1] != '#') {
            builder.deleteCharAt(builder.length() - 1);
        }

        return carregarMascara(builder.toString(), mascara);
    }

    private StringBuilder carregarMascara(String s, char[] mascara) {
        final StringBuilder builder = new StringBuilder();
        final String str = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(s).replaceAll("");

        // Só carregará a máscara se existir algum valor informado
        if (str.length() > 0) {
            int j = 0; // Acompanha a posição nos dígitos

            // É recomendado não usar enhanced for em Android
            for (int i = 0; i < mascara.length; i++) {

                final char charMascara = mascara[i];

                if (charMascara != '#') { // '#' -> caracter de formatação
                    builder.append(charMascara);
                    continue;
                }

                if (j >= str.length()) {
                    break;
                }

                builder.append(str.charAt(j));
                j++;
            }
        }

        return builder;
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/BoletoBancarioTextWatcher.java
================================================
package br.com.concrete.canarinho.watcher;

import android.text.Editable;
import android.text.InputFilter;

import br.com.concrete.canarinho.validator.Validador;
import br.com.concrete.canarinho.validator.ValidadorBoleto;
import br.com.concrete.canarinho.watcher.evento.EventoDeValidacao;
import br.com.concrete.canarinho.watcher.evento.EventoDeValidacaoDeBoleto;

import java.util.Arrays;

/**
 * {@link android.text.TextWatcher} responsável por formatar e validar um {@link
 * android.widget.EditText} para boletos. Para usar este componente basta criar uma instância e
 * chamar {@link android.widget.EditText#addTextChangedListener(android.text.TextWatcher)}.
 */
public final class BoletoBancarioTextWatcher extends BaseCanarinhoTextWatcher {

    private static final char[] BOLETO_NORMAL = "#####.##### #####.###### #####.###### # ##############".toCharArray();
    private static final char[] BOLETO_TRIBUTO = "############ ############ ############ ############".toCharArray();
    private static final InputFilter[] FILTRO_TRIBUTO = new InputFilter[]{
        new InputFilter.LengthFilter(BOLETO_TRIBUTO.length)};
    private static final InputFilter[] FILTRO_NORMAL = new InputFilter[]{
        new InputFilter.LengthFilter(BOLETO_NORMAL.length)};

    private final Validador validador = ValidadorBoleto.getInstance();
    private final Validador.ResultadoParcial resultadoParcial = new Validador.ResultadoParcial();

    /**
     * TODO Javadoc pendente.
     *
     * @param callbackErros a descrever
     */
    public BoletoBancarioTextWatcher(EventoDeValidacao callbackErros) {
        setEventoDeValidacao(callbackErros);
    }

    @Override
    public final void afterTextChanged(Editable s) {

        // retorna se a String é menor que o mínimo de caracteres
        // para haver uma formatação ou se a mudança foi disparada
        // pelo método atualizaTexto
        if (isMudancaInterna()) {
            return;
        }

        // Trata o caso em que tudo é apagado em lote
        if (s.length() < 3) {
            resultadoParcial.mensagem(null).parcialmenteValido(false).totalmenteValido(false);
            if (getEventoDeValidacao() != null) {
                getEventoDeValidacao().parcialmenteValido("");
            }
        }

        if (s.length() == 0) {
            verificaFiltro(s, false);
            return;
        }

        final boolean tributo = ehTributo(s);
        final char[] mascara = tributo ? BOLETO_TRIBUTO : BOLETO_NORMAL;
        verificaFiltro(s, tributo);

        // Trata deleção e adição de forma diferente (só formata em adições)
        final StringBuilder builder = trataAdicaoRemocaoDeCaracter(s, mascara);
        atualizaTexto(validador, resultadoParcial, s, builder);
    }

    public Validador.ResultadoParcial getResultadoParcial() {
        return resultadoParcial;
    }

    @Override
    protected void efetuaValidacao(Validador validador, Validador.ResultadoParcial resultadoParcial, Editable s) {

        validador.ehValido(s, resultadoParcial);

        final EventoDeValidacao callbackErros = getEventoDeValidacao();

        if (callbackErros == null) {
            return;
        }

        final String valorAtual = s.toString();
        if (!resultadoParcial.isParcialmenteValido()) {

            final String mensagem = resultadoParcial.getMensagem();
            callbackErros.invalido(valorAtual, mensagem);

            if (callbackErros instanceof EventoDeValidacaoDeBoleto) {

                final int bloco;

                if (mensagem.startsWith("Primeiro")) {
                    bloco = 1;
                } else if (mensagem.startsWith("Segundo")) {
                    bloco = 2;
                } else if (mensagem.startsWith("Terceiro")) {
                    bloco = 3;
                } else if (mensagem.startsWith("Quarto")) {
                    bloco = 4;
                } else {
                    throw new IllegalArgumentException("Valor não reconhecido para bloco");
                }

                ((EventoDeValidacaoDeBoleto) callbackErros).invalido(valorAtual, bloco);
            }

        } else if (!resultadoParcial.isValido()) {
            callbackErros.parcialmenteValido(valorAtual);
        } else {
            callbackErros.totalmenteValido(valorAtual);
        }
    }

    private void verificaFiltro(final Editable s, final boolean tributo) {
        // Filtro de tamanho
        if (tributo && !Arrays.equals(s.getFilters(), FILTRO_TRIBUTO)) {
            s.setFilters(FILTRO_TRIBUTO);
        } else if (!tributo && !Arrays.equals(s.getFilters(), FILTRO_NORMAL)) {
            s.setFilters(FILTRO_NORMAL);
        }
    }

    // Boletos iniciados com 8 são tributos ou de concessionárias
    private boolean ehTributo(Editable e) {
        return e.charAt(0) == '8';
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/CEPTextWatcher.java
================================================
package br.com.concrete.canarinho.watcher;

import android.text.Editable;
import android.text.InputFilter;

import br.com.concrete.canarinho.validator.Validador;
import br.com.concrete.canarinho.validator.ValidadorCEP;
import br.com.concrete.canarinho.watcher.evento.EventoDeValidacao;

/**
 * {@link android.text.TextWatcher} responsável por formatar e validar um {@link android.widget.EditText} para CEPs.
 * Para usar este componente basta criar uma instância e chamar
 * {@link android.widget.EditText#addTextChangedListener(android.text.TextWatcher)}.
 */
public final class CEPTextWatcher extends BaseCanarinhoTextWatcher {

    private static final char[] CEP_DIGITOS = "#####-###".toCharArray();

    private static final InputFilter[] FILTRO_OITO_DIGITOS = new InputFilter[]{
        new InputFilter.LengthFilter(CEP_DIGITOS.length)};

    private final Validador validador = ValidadorCEP.getInstance();
    private final Validador.ResultadoParcial resultadoParcial = new Validador.ResultadoParcial();

    /**
     * Inicializa o validador com um callback de erros.
     *
     * @param callbackErros Instância que será chamada quando houverem erros.
     */
    public CEPTextWatcher(EventoDeValidacao callbackErros) {
        setEventoDeValidacao(callbackErros);
    }

    @Override
    public void afterTextChanged(Editable s) {

        if (isMudancaInterna()) {
            return;
        }

        s.setFilters(FILTRO_OITO_DIGITOS);

        final StringBuilder builder = trataAdicaoRemocaoDeCaracter(s, CEP_DIGITOS);

        atualizaTexto(validador, resultadoParcial, s, builder);
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/CPFCNPJTextWatcher.java
================================================
package br.com.concrete.canarinho.watcher;

import android.text.Editable;
import android.text.InputFilter;

import br.com.concrete.canarinho.formatador.Formatador;
import br.com.concrete.canarinho.validator.Validador;
import br.com.concrete.canarinho.validator.ValidadorCPFCNPJ;
import br.com.concrete.canarinho.watcher.evento.EventoDeValidacao;

/**
 * {@link android.text.TextWatcher} responsável por formatar e validar um
 * {@link android.widget.EditText} para CPF / CNPJ.
 * Para usar este componente basta criar uma instância e chamar
 * {@link android.widget.EditText#addTextChangedListener(android.text.TextWatcher)}.
 */
public class CPFCNPJTextWatcher extends BaseCanarinhoTextWatcher {

    private static final char[] CPF = "###.###.###-##".toCharArray();
    private static final char[] CNPJ = "##.###.###/####-##".toCharArray();
    private static final InputFilter[] FILTRO_CPF_CNPJ = new InputFilter[]{new InputFilter.LengthFilter(CNPJ.length)};

    private final Validador validador = ValidadorCPFCNPJ.getInstance();
    private final Validador.ResultadoParcial resultadoParcial = new Validador.ResultadoParcial();

    /**
     * TODO Javadoc pendente.
     */
    public CPFCNPJTextWatcher() {
    }

    /**
     * TODO Javadoc pendente.
     *
     * @param callbackErros a descrever
     */
    public CPFCNPJTextWatcher(EventoDeValidacao callbackErros) {
        setEventoDeValidacao(callbackErros);
    }

    @Override
    public void afterTextChanged(final Editable s) {

        if (isMudancaInterna()) {
            return;
        }

        s.setFilters(FILTRO_CPF_CNPJ);

        final char[] mascara = ehCpf(s) ? CPF : CNPJ;
        final StringBuilder builder = trataAdicaoRemocaoDeCaracter(s, mascara);
        atualizaTexto(validador, resultadoParcial, s, builder);
    }

    // Verifica se o valor informado é cpf
    private boolean ehCpf(Editable e) {
        return Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(e).replaceAll("").length() < 12;
    }
}

================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/MascaraNumericaTextWatcher.java
================================================
package br.com.concrete.canarinho.watcher;

import android.text.Editable;
import android.text.InputFilter;

import br.com.concrete.canarinho.validator.Validador;
import br.com.concrete.canarinho.watcher.evento.EventoDeValidacao;

import java.util.Arrays;

/**
 * Máscara c/ validação genérica para campos numéricos.
 *
 * @see br.com.concrete.canarinho.watcher.MascaraNumericaTextWatcher.Builder
 */
public final class MascaraNumericaTextWatcher extends BaseCanarinhoTextWatcher {

    private final Validador.ResultadoParcial resultadoParcial = new Validador.ResultadoParcial();
    private final Validador validador;
    private final char[] mascara;
    private final InputFilter[] filtroNumerico;

    /**
     * Construtor para adicionar uma máscara sem validação.
     *
     * @param mascara Máscara para efetuar a formatação
     */
    public MascaraNumericaTextWatcher(String mascara) {
        this(new Builder().paraMascara(mascara));
    }

    private MascaraNumericaTextWatcher(Builder builder) {
        this.mascara = builder.mascara.toCharArray();
        this.validador = builder.validador;

        final int length = mascara.length;
        this.filtroNumerico = new InputFilter[]{new InputFilter.LengthFilter(length)};

        setEventoDeValidacao(builder.eventoDeValidacao);
    }

    @Override
    public void afterTextChanged(Editable s) {

        // retorna se a mudança foi disparada pelo método atualizaTexto
        if (isMudancaInterna()) {
            return;
        }

        // Filtro de tamanho
        if (!Arrays.equals(s.getFilters(), filtroNumerico)) {
            s.setFilters(filtroNumerico);
        }

        final StringBuilder builder = trataAdicaoRemocaoDeCaracter(s, mascara);

        atualizaTexto(validador, resultadoParcial, s, builder);
    }

    /**
     * Builder para construção de máscaras que validam.
     */
    public static final class Builder {

        private Validador validador;
        private EventoDeValidacao eventoDeValidacao;
        private String mascara;

        /**
         * O validador que será usado. Será chamada a implementação de
         * {@link Validador#ehValido(Editable, Validador.ResultadoParcial)}
         *
         * @param validador Implementação de {@link Validador}
         * @return this para interface fluente
         */
        public Builder comValidador(Validador validador) {
            this.validador = validador;
            return this;
        }

        /**
         * Para cada caracter digitado será validado de acordo com o Validador e o callback
         * correspondente ao resultado da validação será chamado para que a interface possa ser atualizada.
         *
         * @param callbackErros {@link EventoDeValidacao} que será chamado durante a validação
         * @return this para interface fluente
         */
        public Builder comCallbackDeValidacao(EventoDeValidacao callbackErros) {
            this.eventoDeValidacao = callbackErros;
            return this;
        }

        /**
         * A máscara só pode conter os caracteres '#' no lugar dos números. Assim, a máscara
         * '#####-##' irá aceitar apenas números no lugar de '#'. Ao digitar, o usuário irá ver:
         * '12345-67'.
         *
         * @param mascara Máscara
         * @return this para interface fluente
         */
        public Builder paraMascara(String mascara) {
            this.mascara = mascara;
            return this;
        }

        /**
         * Constrói a máscara.
         *
         * @return A instância imutável da máscara.
         */
        public final MascaraNumericaTextWatcher build() {

            if (mascara == null || mascara.isEmpty() || !mascara.contains("#")) {
                throw new IllegalArgumentException("Máscara precisa conter ao menos um caracter '#'");
            }

            return new MascaraNumericaTextWatcher(this);
        }
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/TelefoneTextWatcher.java
================================================
package br.com.concrete.canarinho.watcher;

import android.text.Editable;
import android.text.InputFilter;

import br.com.concrete.canarinho.formatador.Formatador;
import br.com.concrete.canarinho.validator.Validador;
import br.com.concrete.canarinho.validator.ValidadorTelefone;
import br.com.concrete.canarinho.watcher.evento.EventoDeValidacao;

/**
 * {@link android.text.TextWatcher} responsável por formatar e validar um
 * {@link android.widget.EditText} para telefones.
 * Para usar este componente basta criar uma instância e chamar
 * {@link android.widget.EditText#addTextChangedListener(android.text.TextWatcher)}.
 */
public final class TelefoneTextWatcher extends BaseCanarinhoTextWatcher {

    private static final char[] TELEFONE_OITO_DIGITOS = "(##) ####-####".toCharArray();
    private static final char[] TELEFONE_NOVE_DIGITOS = "(##) #####-####".toCharArray();
    private static final InputFilter[] FILTRO_NOVE_DIGITOS = new InputFilter[]{
        new InputFilter.LengthFilter(TELEFONE_NOVE_DIGITOS.length)};

    private final Validador validador = ValidadorTelefone.getInstance();
    private final Validador.ResultadoParcial resultadoParcial = new Validador.ResultadoParcial();

    /**
     * TODO Javadoc pendente.
     *
     * @param callbackErros a descrever
     */
    public TelefoneTextWatcher(EventoDeValidacao callbackErros) {
        setEventoDeValidacao(callbackErros);
    }

    @Override
    public void afterTextChanged(Editable s) {

        if (isMudancaInterna()) {
            return;
        }

        s.setFilters(FILTRO_NOVE_DIGITOS);

        final char[] mascara = ehNoveDigitos(s) ? TELEFONE_NOVE_DIGITOS : TELEFONE_OITO_DIGITOS;
        final StringBuilder builder = trataAdicaoRemocaoDeCaracter(s, mascara);

        atualizaTexto(validador, resultadoParcial, s, builder);
    }

    // Verifica se o telefone possui 9 dígitos
    private boolean ehNoveDigitos(Editable e) {
        return Formatador.Padroes.PADRAO_SOMENTE_NUMEROS.matcher(e).replaceAll("").length() > 10;
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/ValorMonetarioWatcher.java
================================================
package br.com.concrete.canarinho.watcher;

import android.text.Editable;
import android.text.InputFilter;
import android.text.Selection;
import android.text.TextWatcher;

import br.com.concrete.canarinho.formatador.Formatador;

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * TextWatcher para valores monetários.
 */
public class ValorMonetarioWatcher implements TextWatcher {

    private final boolean mantemZerosAoLimpar;
    private final Formatador formatador;
    private boolean mudancaInterna;

    /**
     * Constrói uma instância sem símbolo de Real (R$).
     */
    public ValorMonetarioWatcher() {
        this(false, true);
    }

    /**
     * Constrói uma instância com opção de símbolo de Real (R$).
     *
     * @param comSimboloReal      Flag para acrescentar ou não o símbolo
     * @param mantemZerosAoLimpar Sempre que não houver números (apagar em lote) manter zeros
     */
    ValorMonetarioWatcher(boolean comSimboloReal, boolean mantemZerosAoLimpar) {
        this.formatador = comSimboloReal
                ? Formatador.VALOR_COM_SIMBOLO
                : Formatador.VALOR;
        this.mantemZerosAoLimpar = mantemZerosAoLimpar;
    }

    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        // Não faz nada aqui
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // Não faz nada aqui
    }

    @Override
    public void afterTextChanged(Editable s) {

        if (mudancaInterna) {
            return;
        }

        final String somenteNumeros = Formatador.Padroes.PADRAO_SOMENTE_NUMEROS
                .matcher(s.toString())
                .replaceAll("");

        // afterTextChanged é chamado ao rotacionar o dispositivo,
        // essa condição evita que ao rotacionar a tela com o campo vazio ocorra NumberFormatException
        if (somenteNumeros.length() == 0) {
            if (mantemZerosAoLimpar) {
                atualizaTexto(s, formatador.formata("000"));
            }
            return;
        }

        final BigDecimal resultado = new BigDecimal(somenteNumeros)
                .divide(new BigDecimal(100))
                .setScale(2, RoundingMode.HALF_DOWN);

        atualizaTexto(s, formatador.formata(resultado.toPlainString()));
    }

    private void atualizaTexto(Editable editable, String valor) {
        mudancaInterna = true;

        final InputFilter[] oldFilters = editable.getFilters();

        editable.setFilters(new InputFilter[] {});
        editable.replace(0, editable.length(), valor);

        editable.setFilters(oldFilters);

        if (valor.equals(editable.toString())) {
            // TODO: estudar implantar a manutenção da posição do cursor
            Selection.setSelection(editable, valor.length());
        }

        mudancaInterna = false;
    }

    /**
     * Builder para facilitar a construção de instâncias de {@link ValorMonetarioWatcher}.
     */
    public static class Builder {

        private boolean mantemZerosAoLimpar;
        private boolean simboloReal;

        /**
         * Manterá os zeros ao limpar o campo.
         *
         * @return this Fluent interface
         */
        public Builder comMantemZerosAoLimpar() {
            this.mantemZerosAoLimpar = true;
            return this;
        }

        /**
         * Inclui o símbolo de real na formatação.
         *
         * @return this Fluent interface
         */
        public Builder comSimboloReal() {
            this.simboloReal = true;
            return this;
        }

        /**
         * Constrói a instância.
         *
         * @return Watcher para ser usado
         */
        public ValorMonetarioWatcher build() {
            return new ValorMonetarioWatcher(simboloReal, mantemZerosAoLimpar);
        }
    }
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/evento/EventoDeValidacao.java
================================================
package br.com.concrete.canarinho.watcher.evento;

/**
 * Interface para quem estiver usando este TextWatcher poder ter uma ação quando um erro de validação acontecer.
 * Os métodos desta interface serão chamados quando for digitado um NOVO caracter e quando for APAGADO um caracter.
 */
public interface EventoDeValidacao {

    /**
     * Invocado quando os números digitados estão inválidos. Pode ser apenas um trecho ou o número completo.
     *
     * @param valorAtual O valor após a digitação.
     * @param mensagem   A mensagem de erro da validação.
     */
    void invalido(String valorAtual, String mensagem);

    /**
     * Invocado quando os números digitados estão parcialmente válidos. Quando o número estiver completamente válido
     * será chamado o callback {@link #totalmenteValido(String)}.
     *
     * @param valorAtual O valor após a digitação.
     */
    void parcialmenteValido(String valorAtual);

    /**
     * Invocado quando a máscara está completa e os números são válidos.
     *
     * @param valorAtual O valor após a digitação.
     */
    void totalmenteValido(String valorAtual);
}


================================================
FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/evento/EventoDeValidacaoDeBoleto.java
================================================
package br.com.concrete.canarinho.watcher.evento;

/**
 * Evento de validação específico para boletos que permite saber qual o bloco que
 * contém caracteres inválidos.
 */
public interface EventoDeValidacaoDeBoleto extends EventoDeValidacao {

    /**
     * Invocado quando os números digitados estão inválidos. Pode ser apenas um trecho ou o número completo.
     *
     * @param valorAtual    O valor após a digitação.
     * @param blocoInvalido O bloco com valor inválido
     */
    void invalido(String valorAtual, int blocoInvalido);

}


================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip


================================================
FILE: gradle.properties
================================================
org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
android.useAndroidX=true


================================================
FILE: gradlew
================================================
#!/usr/bin/env bash

##############################################################################
##
##  Gradle start up script for UN*X
##
##############################################################################

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn ( ) {
    echo "$*"
}

die ( ) {
    echo
    echo "$*"
    echo
    exit 1
}

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
  CYGWIN* )
    cygwin=true
    ;;
  Darwin* )
    darwin=true
    ;;
  MINGW* )
    msys=true
    ;;
esac

# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
    else
        PRG=`dirname "$PRG"`"/$link"
    fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar

# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD="$JAVA_HOME/jre/sh/java"
    else
        JAVACMD="$JAVA_HOME/bin/java"
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD="java"
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
    MAX_FD_LIMIT=`ulimit -H -n`
    if [ $? -eq 0 ] ; then
        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
            MAX_FD="$MAX_FD_LIMIT"
        fi
        ulimit -n $MAX_FD
        if [ $? -ne 0 ] ; then
            warn "Could not set maximum file descriptor limit: $MAX_FD"
        fi
    else
        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
    fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`

    # We build the pattern for arguments to be converted via cygpath
    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
    SEP=""
    for dir in $ROOTDIRSRAW ; do
        ROOTDIRS="$ROOTDIRS$SEP$dir"
        SEP="|"
    done
    OURCYGPATTERN="(^($ROOTDIRS))"
    # Add a user-defined pattern to the cygpath arguments
    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
    fi
    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    i=0
    for arg in "$@" ; do
        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option

        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
        else
            eval `echo args$i`="\"$arg\""
        fi
        i=$((i+1))
    done
    case $i in
        (0) set -- ;;
        (1) set -- "$args0" ;;
        (2) set -- "$args0" "$args1" ;;
        (3) set -- "$args0" "$args1" "$args2" ;;
        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
    esac
fi

# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
    JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"

exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"


================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=

set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto init

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:init
@rem Get command-line arguments, handling Windowz variants

if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args

:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2

:win9xME_args_slurp
if "x%~1" == "x" goto execute

set CMD_LINE_ARGS=%*
goto execute

:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: sample/.gitignore
================================================
/build


================================================
FILE: sample/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'jacoco'

android {
    compileSdkVersion 31

    defaultConfig {
        applicationId 'br.com.concrete.canarinho.sample'
        minSdkVersion 21
        targetSdkVersion 31
        versionCode 1
        versionName '1.0'

        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
    }

    buildTypes {

        debug {
            testCoverageEnabled true
        }

        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    testOptions {
        execution 'ANDROIDX_TEST_ORCHESTRATOR'
        unitTests {
            includeAndroidResources = true
        }
    }
}

dependencies {

    implementation project(':canarinho')

    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'com.google.android.material:material:1.2.1'

    androidTestUtil 'androidx.test:orchestrator:1.3.0'

    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    androidTestImplementation 'androidx.test:runner:1.3.0'
    androidTestImplementation 'androidx.test:rules:1.3.0'
    androidTestImplementation 'androidx.test:core:1.3.0'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'

    testImplementation 'junit:junit:4.13.2'
    testImplementation "org.robolectric:robolectric:4.7.3"
    testImplementation 'androidx.test:core:1.3.0'
    testImplementation 'androidx.test.ext:junit:1.1.2'
}

jacoco {
    toolVersion = '0.8.3'
}

task fullCoverageReport(type: JacocoReport) {
    dependsOn 'createDebugCoverageReport'
    dependsOn 'testDebugUnitTest'

    reports {
        xml.enabled true
        html.enabled true
    }

    def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*',
                      '**/*Test*.*', 'android/**/*.*']
    def debugTree = fileTree(dir: "${buildDir}/intermediates/classes/debug", excludes: fileFilter)
    def mainSrc = "${project.projectDir}/src/main/java"

    sourceDirectories.from = files([mainSrc])
    classDirectories.from = files([debugTree])
    executionData.from = fileTree(dir: "$buildDir", includes: [
            "jacoco/testDebugUnitTest.exec",
            "outputs/code_coverage/connected/*coverage.ec"
    ])
}


================================================
FILE: sample/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/victornascimento/Android/Sdk/linters/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}


================================================
FILE: sample/src/androidTest/java/br/com/concrete/canarinho/sample/BugOnApi28Test.java
================================================
package br.com.concrete.canarinho.sample;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import org.junit.Test;
import org.junit.runner.RunWith;

import br.com.concrete.canarinho.formatador.Formatador;

import static org.junit.Assert.assertEquals;

@RunWith(AndroidJUnit4.class)
public class BugOnApi28Test {

    @Test
    @SdkSuppress(minSdkVersion = 28)
    public void moneyFormatSuccessfulRunsOnApi28() {
        final String value = "1.000.000,00";
        assertEquals("1000000.00", Formatador.VALOR.desformata(value));
    }

    @Test
    @SdkSuppress(maxSdkVersion = 27)
    public void moneyFormatSuccessfulRunsOnApi27() {
        final String value = "1.000.000,00";
        assertEquals("1000000.00", Formatador.VALOR.desformata(value));
    }
}


================================================
FILE: sample/src/androidTest/java/br/com/concrete/canarinho/sample/DemoWatchersInstrumentationTest.java
================================================
package br.com.concrete.canarinho.sample;

import android.os.Build;
import android.view.View;
import android.widget.EditText;
import android.widget.SearchView;

import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import br.com.concrete.canarinho.sample.ui.activity.MainActivity;
import br.com.concrete.canarinho.sample.ui.model.Watchers;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.clearText;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.pressBack;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.supportsInputMethods;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anyOf;

@RunWith(AndroidJUnit4.class)
public class DemoWatchersInstrumentationTest {

    @Rule
    public ActivityTestRule<MainActivity> rule = new ActivityTestRule<>(MainActivity.class);

    @Test
    public void consegueDigitarUmBoletoNormalValido() {

        navigateToTab(Watchers.BOLETO_BANCARIO);

        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(typeText("34199310130010011560900500990007800000000000000"));

        onView(withText("Campo válido!")).check(matches(isDisplayed()));
    }

    @Test
    public void consegueValidarUmBoletoSetandoOCodigoInteiro() {

        navigateToTab(Watchers.BOLETO_BANCARIO);

        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(paste("34199310130010011560900500990007800000000000000"));

        onView(withText("Campo válido!")).check(matches(isDisplayed()));
    }

    @Test
    public void consegueValidarUmBoletoTributoSetandoOCodigoInteiro() {

        navigateToTab(Watchers.BOLETO_BANCARIO);

        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(paste("812345678901812345678901812345678901812345678901"));

        onView(withText("Campo válido!")).check(matches(isDisplayed()));
    }

    @Test
    public void consegueDigitarUmBoletoNormalComBlocosInvalidos() {

        navigateToTab(Watchers.BOLETO_BANCARIO);

        // primeiro
        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(typeText("23790125016"));
        onView(withText("Primeiro bloco inválido")).check(matches(isDisplayed()));

        // segundo
        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(clearText(), typeText("2379012301600000030054"));
        onView(withText("Segundo bloco inválido")).check(matches(isDisplayed()));

        // terceiro
        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(clearText(), typeText("23790123016000000005325000456708"));
        onView(withText("Terceiro bloco inválido")).check(matches(isDisplayed()));
    }

    @Test
    public void consegueDigitarUmBoletoTributoValido() {

        navigateToTab(Watchers.BOLETO_BANCARIO);

        // Boleto válido
        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(typeText("848600000015523301622010506101307129620012111220"));

        onView(withText("Campo válido!"))
                .check(matches(isDisplayed()))
                .perform(pressBack());

        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(clearText(), typeText("836600000019078800481000998854924516001265611135"));

        onView(withText("Campo válido!"))
                .check(matches(isDisplayed()))
                .perform(pressBack());
    }

    @Test
    public void consegueDigitarUmBoletoNormalComBlocosInvalidosComMensagemCustomizada() {

        navigateToTab(Watchers.BOLETO_BANCARIO_MSG_CUSTOM);

        // primeiro
        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(typeText("23790125016"));
        onView(withText("Primeira mensagem")).check(matches(isDisplayed()));

        // segundo
        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(clearText(), typeText("2379012301600000030054"));
        onView(withText("Segunda mensagem")).check(matches(isDisplayed()));

        // terceiro
        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(clearText(), typeText("23790123016000000005325000456708"));
        onView(withText("Terceira mensagem")).check(matches(isDisplayed()));
    }

    @Test
    public void consegueDigitarUmCPFValido() {

        navigateToTab(Watchers.CPF);

        onView(allOf(withId(R.id.edit_text), isDisplayed())).perform(typeText("46574356636"));

        onView(withText("Campo válido!"))
                .check(matches(isDisplayed()))
                .perform(pressBack());
    }

    @Test
    public void consegueDigitarUmCPFInvalido() {

        navigateToTab(Watchers.CPF);

        onView(allOf(withId(R.id.edit_text), isDisplayed())).perform(typeText("46574356637"));
        onView(withText("CPF inválido")).check(matches(isDisplayed()));
    }

    @Test
    public void consegueDigitarUmCNPJValido() {

        navigateToTab(Watchers.CNPJ);

        onView(allOf(withId(R.id.edit_text), isDisplayed())).perform(typeText("95621433000170"));

        onView(withText("Campo válido!"))
                .check(matches(isDisplayed()))
                .perform(pressBack());
    }

    @Test
    public void consegueDigitarUmCNPJInvalido() {

        navigateToTab(Watchers.CNPJ);
        onView(allOf(withId(R.id.edit_text), isDisplayed())).perform(typeText("95621433000180"));
        onView(withText("CNPJ inválido")).check(matches(isDisplayed()));
    }

    @Test
    public void consegueDigitarUmTelefoneValido() {

        navigateToTab(Watchers.TELEFONE);

        onView(allOf(withId(R.id.edit_text), isDisplayed())).perform(typeText("1112345678"));

        onView(withText("Campo válido!"))
                .check(matches(isDisplayed()))
                .perform(pressBack());

        onView(allOf(withId(R.id.edit_text), isDisplayed())).perform(typeText("11123456789"));

        onView(withText("Campo válido!"))
                .check(matches(isDisplayed()))
                .perform(pressBack());
    }

    @Test
    public void consegueDigitarUmValorMonetarioFormatado() throws InterruptedException {

        navigateToTab(Watchers.VALOR_MONETARIO);

        Thread.sleep(200L);

        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(typeText("1"))
                .check(matches(withText("R$ 0,01"))) // inicia com 0,0X
                .perform(typeText("2"))
                .check(matches(withText("R$ 0,12"))) // 0,XY
                .perform(typeText("3"))
                .check(matches(withText("R$ 1,23"))) // X,YZ
                .perform(typeText("4"))
                .check(matches(withText("R$ 12,34"))) // etc, etc, etc
                .perform(typeText("5"))
                .check(matches(withText("R$ 123,45")))
                .perform(typeText("6"))
                .check(matches(withText("R$ 1.234,56")))
                .perform(typeText("7"))
                .check(matches(withText("R$ 12.345,67")))
                .perform(typeText("8"))
                .check(matches(withText("R$ 123.456,78")))
                .perform(typeText("9"))
                .check(matches(withText("R$ 1.234.567,89")))
                .perform(clearText())
                .check(matches(withText("R$ 0,00")));
    }

    @Test
    public void consegueDigitarCPFCNPJValido() {

        navigateToTab(Watchers.CPF_CNPJ);

        onView(allOf(withId(R.id.edit_text), isDisplayed())).perform(typeText("46574356636"));
        onView(withText("Campo válido!")).check(matches(isDisplayed())).perform(pressBack());
        onView(allOf(withId(R.id.edit_text), isDisplayed())).perform(clearText(), typeText("95621433000170"));
        onView(withText("Campo válido!")).check(matches(isDisplayed()));
    }

    @Test
    public void consegueDigitarCPFCNPJInvalido() {

        navigateToTab(Watchers.CPF_CNPJ);

        onView(allOf(withId(R.id.edit_text), isDisplayed())).perform(typeText("46574356637"));
        onView(withText("CPF inválido")).check(matches(isDisplayed()));

        onView(allOf(withId(R.id.edit_text), isDisplayed())).perform(clearText(), typeText("15621433000180"));
        onView(withText("CNPJ inválido")).check(matches(isDisplayed()));
    }

    @Test
    public void consegueDigitarUmCEPValido() {

        navigateToTab(Watchers.CEP);

        onView(allOf(withId(R.id.edit_text), isDisplayed())).perform(typeText("49025090"));

        onView(withText("Campo válido!"))
                .check(matches(isDisplayed()));
    }

    @Test
    public void consegueUtilizarUmaMascaraGenericaSemValidadorOuEvento() {

        navigateToTab(Watchers.MASCARA_GENERICA);

        onView(allOf(withId(R.id.edit_text), isDisplayed()))
                .perform(typeText("12345"))
                .check(matches(withText("1-2-3-4-5")));
    }

    private void navigateToTab(Watchers watcher) {
        onView(
                allOf(
                        withText(watcher.getTitle()),
                        isDescendantOfA(withId(R.id.tabs)))
        )
                .perform(scrollTo(), click());
    }

    private ViewAction paste(final String type) {
        return new ViewAction() {
            @Override
            public Matcher<View> getConstraints() {
                // noinspection unchecked
                Matcher<View> matchers = allOf(isDisplayed());

                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
                    return allOf(matchers, supportsInputMethods());
                } else {
                    // SearchView does not support input methods itself (rather it delegates to an internal text
                    // view for input).
                    return allOf(matchers, anyOf(supportsInputMethods(), isAssignableFrom(SearchView.class)));
                }
            }

            @Override
            public String getDescription() {
                return "Straight typing into view";
            }

            @Override
            public void perform(UiController uiController, View view) {
                ((EditText) view).setText(type);
            }
        };
    }
}


================================================
FILE: sample/src/main/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="br.com.concrete.canarinho.sample">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">

        <activity android:name="br.com.concrete.canarinho.sample.ui.activity.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


================================================
FILE: sample/src/main/java/br/com/concrete/canarinho/sample/ui/activity/MainActivity.java
================================================
package br.com.concrete.canarinho.sample.ui.activity;

import android.os.Bundle;

import com.google.android.material.tabs.TabLayout;

import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.viewpager.widget.ViewPager;
import br.com.concrete.canarinho.sample.R;
import br.com.concrete.canarinho.sample.ui.adapter.WatchersPagerAdapter;

/** */
public class MainActivity extends AppCompatActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);

        final Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        final ActionBar supportActionBar = getSupportActionBar();
        if (supportActionBar != null) {
            supportActionBar.setDisplayHomeAsUpEnabled(true);
            supportActionBar.setDisplayShowTitleEnabled(false);
        }

        final ViewPager viewPager = findViewById(R.id.viewpager);
        viewPager.setAdapter(new WatchersPagerAdapter(getSupportFragmentManager()));

        final TabLayout tabs = findViewById(R.id.tabs);
        tabs.setupWithViewPager(viewPager);
    }
}


================================================
FILE: sample/src/main/java/br/com/concrete/canarinho/sample/ui/adapter/WatchersPagerAdapter.java
================================================
package br.com.concrete.canarinho.sample.ui.adapter;

import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import br.com.concrete.canarinho.sample.ui.model.Watchers;

public class WatchersPagerAdapter extends FragmentPagerAdapter {

    private Watchers[] models = Watchers.values();

    public WatchersPagerAdapter(FragmentManager fragmentManager) {
        super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
    }

    @Override
    public Fragment getItem(int position) {
        return models[position].buildFragment();
    }

    @Override
    public int getCount() {
        return models.length;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return models[position].getTitle();
    }
}


================================================
FILE: sample/src/main/java/br/com/concrete/canarinho/sample/ui/fragment/BaseWatcherFragment.java
================================================
package br.com.concrete.canarinho.sample.ui.fragment;

import androidx.fragment.app.Fragment;
import br.com.concrete.canarinho.sample.ui.model.Watchers;

public abstract class BaseWatcherFragment extends Fragment {

    protected Watchers model;

    public void setModel(Watchers model) {
        this.model = model;
    }
}


================================================
FILE: sample/src/main/java/br/com/concrete/canarinho/sample/ui/fragment/CanarinhoValorMonetarioWatcherFragment.java
================================================
package br.com.concrete.canarinho.sample.ui.fragment;

import android.os.Bundle;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;

import com.google.android.material.textfield.TextInputLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import br.com.concrete.canarinho.sample.R;
import br.com.concrete.canarinho.sample.ui.model.Watchers;
import br.com.concrete.canarinho.watcher.ValorMonetarioWatcher;

public class CanarinhoValorMonetarioWatcherFragment extends BaseWatcherFragment {

    private EditText watcherEdit;
    private TextView watcherTitle;
    private TextInputLayout watcherInputLayout;
    private TextWatcher currentWatcher;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        final View layout = inflater.inflate(R.layout.fragment_valor_monetario_watcher, null);
        watcherTitle = layout.findViewById(R.id.watcher_title);
        watcherInputLayout = layout.findViewById(R.id.edit_input_layout);
        watcherEdit = layout.findViewById(R.id.edit_text);
        setModel(Watchers.VALOR_MONETARIO);
        bind(model);
        return layout;
    }

    public CanarinhoValorMonetarioWatcherFragment bind(Watchers model) {

        watcherTitle.setText(model.getTitle());
        watcherEdit.setHint(model.getHint());

        if (currentWatcher != null) {
            watcherEdit.removeTextChangedListener(currentWatcher);
        }

        currentWatcher = new ValorMonetarioWatcher.Builder()
                .comMantemZerosAoLimpar()
                .comSimboloReal()
                .build();
        watcherEdit.addTextChangedListener(currentWatcher);

        return this;
    }
}


================================================
FILE: sample/src/main/java/br/com/concrete/canarinho/sample/ui/fragment/WatcherFragment.java
================================================
package br.com.concrete.canarinho.sample.ui.fragment;

import android.os.Bundle;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;

import com.google.android.material.textfield.TextInputLayout;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import br.com.concrete.canarinho.sample.R;
import br.com.concrete.canarinho.sample.ui.model.Watchers;

public class WatcherFragment extends BaseWatcherFragment {

    private EditText watcherEdit;
    private TextView watcherTitle;
    private TextInputLayout watcherInputLayout;
    private TextWatcher currentWatcher;

    public static WatcherFragment newInstance(Watchers model) {
        final WatcherFragment watcherFragment = new WatcherFragment();
        watcherFragment.setArguments(new Bundle());
        watcherFragment.setModel(model);
        return watcherFragment;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        final View layout = inflater.inflate(R.layout.fragment_canarinho_watcher, null);
        watcherTitle = layout.findViewById(R.id.watcher_title);
        watcherInputLayout = layout.findViewById(R.id.edit_input_layout);
        watcherEdit = layout.findViewById(R.id.edit_text);
        bind(model);
        return layout;
    }

    public WatcherFragment bind(Watchers model) {

        if (currentWatcher != null) {
            watcherEdit.removeTextChangedListener(currentWatcher);
        }

        currentWatcher = model.setupWatcher(watcherInputLayout);
        watcherTitle.setText(model.getTitle());
        watcherEdit.setHint(model.getHint());
        watcherEdit.addTextChangedListener(currentWatcher);
        return this;
    }
}


================================================
FILE: sample/src/main/java/br/com/concrete/canarinho/sample/ui/model/Watchers.java
================================================
package br.com.concrete.canarinho.sample.ui.model;

import android.text.TextWatcher;

import com.google.android.material.textfield.TextInputLayout;

import androidx.appcompat.app.AlertDialog;
import br.com.concrete.canarinho.sample.ui.fragment.BaseWatcherFragment;
import br.com.concrete.canarinho.sample.ui.fragment.CanarinhoValorMonetarioWatcherFragment;
import br.com.concrete.canarinho.sample.ui.fragment.WatcherFragment;
import br.com.concrete.canarinho.validator.Validador;
import br.com.concrete.canarinho.watcher.BoletoBancarioTextWatcher;
import br.com.concrete.canarinho.watcher.CEPTextWatcher;
import br.com.concrete.canarinho.watcher.CPFCNPJTextWatcher;
import br.com.concrete.canarinho.watcher.MascaraNumericaTextWatcher;
import br.com.concrete.canarinho.watcher.TelefoneTextWatcher;
import br.com.concrete.canarinho.watcher.evento.EventoDeValidacao;
import br.com.concrete.canarinho.watcher.evento.EventoDeValidacaoDeBoleto;

/**
 */
public enum Watchers {

    BOLETO_BANCARIO("Boleto Bancário", "Digite um boleto válido") {
        @Override
        public WatcherFragment buildFragment() {
            return WatcherFragment.newInstance(this);
        }

        @Override
        public TextWatcher setupWatcher(TextInputLayout textInputLayout) {
            return new BoletoBancarioTextWatcher(new SampleEventoDeValidacao(textInputLayout));
        }
    },
    BOLETO_BANCARIO_MSG_CUSTOM("Boleto Bancário com mensagem", "Digite um boleto válido") {
        @Override
        public WatcherFragment buildFragment() {
            return WatcherFragment.newInstance(this);
        }

        @Override
        public TextWatcher setupWatcher(TextInputLayout textInputLayout) {
            return new BoletoBancarioTextWatcher(new EventoDeValidacaoBoleto(textInputLayout));
        }
    },

    CPF("CPF", "Digite um CPF válido") {
        @Override
        public WatcherFragment buildFragment() {
            return WatcherFragment.newInstance(this);
        }

        @Override
        public TextWatcher setupWatcher(TextInputLayout textInputLayout) {
            return new MascaraNumericaTextWatcher.Builder()
                    .paraMascara("###.###.###-##")
                    .comCallbackDeValidacao(new SampleEventoDeValidacao(textInputLayout))
                    .comValidador(Validador.CPF)
                    .build();
        }
    },

    CNPJ("CNPJ", "Digite um CNPJ válido") {
        @Override
        public WatcherFragment buildFragment() {
            return WatcherFragment.newInstance(this);
        }

        @Override
        public TextWatcher setupWatcher(TextInputLayout textInputLayout) {
            return new MascaraNumericaTextWatcher.Builder()
                    .paraMascara("##.###.###/####-##")
                    .comCallbackDeValidacao(new SampleEventoDeValidacao(textInputLayout))
                    .comValidador(Validador.CNPJ)
                    .build();
        }
    },

    TELEFONE("Telefone", "Digite um telefone válido") {
        @Override
        public WatcherFragment buildFragment() {
            return WatcherFragment.newInstance(this);
        }

        @Override
        public TextWatcher setupWatcher(TextInputLayout textInputLayout) {
            return new TelefoneTextWatcher(new SampleEventoDeValidacao(textInputLayout));
        }
    },

    CPF_CNPJ("CPF e CNPJ", "Digite um CPF ou CNPJ válido") {
        @Override
        public WatcherFragment buildFragment() {
            return WatcherFragment.newInstance(this);
        }

        @Override
        public TextWatcher setupWatcher(TextInputLayout textInputLayout) {
            return new CPFCNPJTextWatcher(new SampleEventoDeValidacao(textInputLayout));
        }
    },

    CEP("CEP", "Digite um CEP válido") {
        @Override
        public WatcherFragment buildFragment() {
            return WatcherFragment.newInstance(this);
        }

        @Override
        public TextWatcher setupWatcher(TextInputLayout textInputLayout) {
            return new CEPTextWatcher(new SampleEventoDeValidacao(textInputLayout));
        }
    },

    MASCARA_GENERICA("Máscara genérica", "5 números") {
        @Override
        public WatcherFragment buildFragment() {
            return WatcherFragment.newInstance(this);
        }

        @Override
        public TextWatcher setupWatcher(TextInputLayout textInputLayout) {
            return new MascaraNumericaTextWatcher("#-#-#-#-#");
        }
    },

    VALOR_MONETARIO("Valor monetário", "Digite um valor monetário") {
        @Override
        public CanarinhoValorMonetarioWatcherFragment buildFragment() {
            return new CanarinhoValorMonetarioWatcherFragment();
        }

        @Override
        public TextWatcher setupWatcher(TextInputLayout textInputLayout) {
            return null;
        }
    };

    private final String title;
    private final String hint;

    Watchers(String title, String hint) {
        this.hint = hint;
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    public String getHint() {
        return hint;
    }

    public abstract BaseWatcherFragment buildFragment();

    public abstract TextWatcher setupWatcher(TextInputLayout textInputLayout);

    /**
     * {@link EventoDeValidacao} que mostra uma mensagem de erro no {@link TextInputLayout} passado
     * como argumento.
     */
    public static class SampleEventoDeValidacao implements EventoDeValidacao {

        TextInputLayout textInputLayout;

        public SampleEventoDeValidacao(TextInputLayout textInputLayout) {
            this.textInputLayout = textInputLayout;
        }

        @Override
        public void invalido(String valorAtual, String mensagem) {
            textInputLayout.setError(mensagem);
        }

        @Override
        public void parcialmenteValido(String valorAtual) {
            textInputLayout.setErrorEnabled(false);
            textInputLayout.setError(null);
        }

        @Override
        public void totalmenteValido(String valorAtual) {
            new AlertDialog.Builder(textInputLayout.getContext())
                    .setTitle("Campo válido!")
                    .setMessage(valorAtual)
                    .show();
        }
    }

    /**
     * {@link EventoDeValidacao} que valida blocos de boleto.
     *
     * @see SampleEventoDeValidacao
     */
    public static class EventoDeValidacaoBoleto
            extends SampleEventoDeValidacao
            implements EventoDeValidacaoDeBoleto {

        EventoDeValidacaoBoleto(TextInputLayout textInputLayout) {
            super(textInputLayout);
        }

        @Override
        public void invalido(String valorAtual, int blocoInvalido) {

            if (blocoInvalido == 1)
                textInputLayout.setError("Primeira mensagem");

            else if (blocoInvalido == 2)
                textInputLayout.setError("Segunda mensagem");

            else if (blocoInvalido == 3)
                textInputLayout.setError("Terceira mensagem");

            else if (blocoInvalido == 4)
                textInputLayout.setError("Quarta mensagem");

        }
    }

}


================================================
FILE: sample/src/main/res/layout/fragment_canarinho_watcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="6dp">

        <TextView
            android:id="@+id/watcher_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="@style/TextAppearance.AppCompat.Title" />

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/edit_input_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:errorEnabled="true">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/edit_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="4dp"
                android:inputType="number" />

        </com.google.android.material.textfield.TextInputLayout>

    </androidx.appcompat.widget.LinearLayoutCompat>
</com.google.android.material.card.MaterialCardView>

================================================
FILE: sample/src/main/res/layout/fragment_valor_monetario_watcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="6dp">

        <TextView
            android:id="@+id/watcher_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="@style/TextAppearance.AppCompat.Title" />

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/edit_input_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:errorEnabled="true">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/edit_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="4dp"
                android:inputType="number" />

        </com.google.android.material.textfield.TextInputLayout>

    </androidx.appcompat.widget.LinearLayoutCompat>
</com.google.android.material.card.MaterialCardView>

================================================
FILE: sample/src/main/res/layout/main_activity.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/rootLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="#008800"
            android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
            app:layout_scrollFlags="scroll|enterAlwaysCollapsed"
            app:popupTheme="@style/ThemeOverlay.AppCompat.Light">

            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="12dp"
                android:layout_marginRight="12dp"
                android:contentDescription="@string/app_name"
                android:src="@mipmap/ic_launcher" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/app_name"
                android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />

        </androidx.appcompat.widget.Toolbar>

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:tabGravity="fill"
            app:tabMode="scrollable" />

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

================================================
FILE: sample/src/main/res/values/strings.xml
================================================
<resources>
    <string name="app_name">Android Canarinho</string>
</resources>


================================================
FILE: sample/src/main/res/values/styles.xml
================================================
<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
        <item name="android:windowBackground">@android:color/white</item>
    </style>
</resources>


================================================
FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorBOLETO.java
================================================
package br.com.concrete.canarinho.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import br.com.concrete.canarinho.formatador.Formatador;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

@RunWith(AndroidJUnit4.class)
public class TesteFormatadorBOLETO {

    @Test
    public void consegueFormatar() {
        assertThat(Formatador.BOLETO.formata("12345123451234512345612345123456712345678901234"),
                is("12345.12345 12345.123456 12345.123456 7 12345678901234"));

        assertThat(Formatador.BOLETO.formata("23790123016000000005325000456704364670000013580"),
                is("23790.12301 60000.000053 25000.456704 3 64670000013580"));

        assertThat(Formatador.BOLETO.formata("812345678901812345678901812345678901812345678901"),
                is("812345678901 812345678901 812345678901 812345678901"));

        assertThat(Formatador.BOLETO.formata("23790.12301 60000.000053 25000.456704 3 64670000013580"),
                is("23790.12301 60000.000053 25000.456704 3 64670000013580"));

        assertThat(Formatador.BOLETO.formata("812345678901 812345678901 812345678901 812345678901"),
                is("812345678901 812345678901 812345678901 812345678901"));
    }

    @Test
    public void consegueDesformatar() {

        assertThat(Formatador.BOLETO.desformata("12345.12345 12345.123456 12345.123456 7 12345678901234"),
                is("12345123451234512345612345123456712345678901234"));

        assertThat(Formatador.BOLETO.desformata("23790.12301 60000.000053 25000.456704 3 64670000013580"),
                is("23790123016000000005325000456704364670000013580"));

        assertThat(Formatador.BOLETO.desformata("812345678901 812345678901 812345678901 812345678901"),
                is("812345678901812345678901812345678901812345678901"));

        assertThat(Formatador.BOLETO.desformata("23790123016000000005325000456704364670000013580"),
                is("23790123016000000005325000456704364670000013580"));

        assertThat(Formatador.BOLETO.desformata("812345678901812345678901812345678901812345678901"),
                is("812345678901812345678901812345678901812345678901"));
    }

    @Test
    public void consegueDizerSeEstaFormatado() {

        assertThat(Formatador.BOLETO.estaFormatado("12345.12345 12345.123456 12345.123456 7 12345678901234"), is(true));
        assertThat(Formatador.BOLETO.estaFormatado("23790.12301 60000.000053 25000.456704 3 64670000013580"), is(true));

        assertThat(Formatador.BOLETO.estaFormatado("812345678901812345678901812345678901812345678901"), is(false));
        assertThat(Formatador.BOLETO.estaFormatado("23790123016000000005325000456704364670000013580"), is(false));
        assertThat(Formatador.BOLETO.estaFormatado("812345678901812345678901812345678901812345678901"), is(false));
    }

    @Test
    public void consegueDizerSePodeFormatar() {
        // TODO
    }
}


================================================
FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorCEP.java
================================================
package br.com.concrete.canarinho.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import br.com.concrete.canarinho.formatador.Formatador;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.fail;

@RunWith(AndroidJUnit4.class)
public class TesteFormatadorCEP {

    @Test
    public void consegueFormatar() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CEP.formata("00000-000"), is("00000-000"));
        assertThat(Formatador.CEP.formata("00000000"), is("00000-000"));

        assertThat(Formatador.CEP.formata("12345-123"), is("12345-123"));
        assertThat(Formatador.CEP.formata("12345678"), is("12345-678"));

        assertThrowsFormat("");
        assertThrowsFormat("123123");
    }

    @Test
    public void consegueDesformatar() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CEP.desformata("00000-000"), is("00000000"));
        assertThat(Formatador.CEP.desformata("00000000"), is("00000000"));

        assertThat(Formatador.CEP.desformata("12345-123"), is("12345123"));
        assertThat(Formatador.CEP.desformata("12345678"), is("12345678"));

        assertThrowsDesformat("");
        assertThrowsDesformat("123123");
    }

    @Test
    public void consegueDizerSeEstaFormatado() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CEP.estaFormatado("12345-678"), is(true));
        assertThat(Formatador.CEP.estaFormatado("12345678"), is(false));
        assertThat(Formatador.CEP.estaFormatado("00000-000"), is(true));
        assertThat(Formatador.CEP.estaFormatado("12345-67"), is(false));

        assertThat(Formatador.CEP.estaFormatado("047486"), is(false));
        assertThat(Formatador.CEP.estaFormatado(""), is(false));
        try {
            Formatador.CEP.estaFormatado(null);
            fail("Should have thrown!!!");
        } catch (IllegalArgumentException e) {
        }
    }

    @Test
    public void consegueDizerSePodeFormatar() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CEP.podeSerFormatado("12345-678"), is(false));
        assertThat(Formatador.CEP.podeSerFormatado("12345678"), is(true));
        assertThat(Formatador.CEP.podeSerFormatado("020"), is(false));
        assertThat(Formatador.CEP.podeSerFormatado(""), is(false));
        assertThat(Formatador.CEP.podeSerFormatado(null), is(false));
    }

    private void assertThrowsFormat(String valor) {
        try {
            Formatador.CEP.formata(valor);
            fail("Deveria ter jogado exceção!!!");
        } catch (IllegalArgumentException e) {
        }
    }

    private void assertThrowsDesformat(String valor) {
        try {
            Formatador.CEP.desformata(valor);
            fail("Deveria ter jogado exceção!!!");
        } catch (IllegalArgumentException e) {
        }
    }
}


================================================
FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorCNPJ.java
================================================
package br.com.concrete.canarinho.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import br.com.concrete.canarinho.formatador.Formatador;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.fail;

@RunWith(AndroidJUnit4.class)
public class TesteFormatadorCNPJ {

    @Test
    public void consegueFormatar() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CNPJ.formata("50.713.534/0001-33"), is("50.713.534/0001-33"));
        assertThat(Formatador.CNPJ.formata("50713534000133"), is("50.713.534/0001-33"));

        assertThat(Formatador.CNPJ.formata("72.606.598/0001-78"), is("72.606.598/0001-78"));
        assertThat(Formatador.CNPJ.formata("72606598000178"), is("72.606.598/0001-78"));

        assertThat(Formatador.CNPJ.formata("23.106.535/0001-47"), is("23.106.535/0001-47"));
        assertThat(Formatador.CNPJ.formata("23106535000147"), is("23.106.535/0001-47"));

        assertThrowsFormat("");
        assertThrowsFormat("123123");
        assertThrowsFormat("23.106.535/0001  -   47");
        assertThrowsFormat("       23.106.535/0001-47      ");
    }

    @Test
    public void consegueDesformatar() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CNPJ.desformata("50.713.534/0001-33"), is("50713534000133"));
        assertThat(Formatador.CNPJ.desformata("50713534000133"), is("50713534000133"));

        assertThat(Formatador.CNPJ.desformata("72.606.598/0001-78"), is("72606598000178"));
        assertThat(Formatador.CNPJ.desformata("72606598000178"), is("72606598000178"));

        assertThat(Formatador.CNPJ.desformata("23.106.535/0001-47"), is("23106535000147"));
        assertThat(Formatador.CNPJ.desformata("23106535000147"), is("23106535000147"));

        assertThrowsDesformat("");
        assertThrowsDesformat("123123");
        assertThrowsDesformat("123.123.123    -12");
        assertThrowsDesformat("       047.486.777-32      ");
    }

    @Test
    public void consegueDizerSeEstaFormatado() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CNPJ.estaFormatado("72.606.598/0001-78"), is(true));
        assertThat(Formatador.CNPJ.estaFormatado("72606598000178"), is(false));
        assertThat(Formatador.CNPJ.estaFormatado("23.106.535/0001-47"), is(true));
        assertThat(Formatador.CNPJ.estaFormatado("23106535000147"), is(false));

        assertThat(Formatador.CNPJ.estaFormatado("047486"), is(false));
        assertThat(Formatador.CNPJ.estaFormatado(""), is(false));
        try {
            Formatador.CNPJ.estaFormatado(null);
            fail("Should have thrown!!!");
        } catch (IllegalArgumentException e) {
        }
    }

    @Test
    public void consegueDizerSePodeFormatar() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CNPJ.podeSerFormatado("23.106.535/0001-47"), is(false));
        assertThat(Formatador.CNPJ.podeSerFormatado("23106535000147"), is(true));
        assertThat(Formatador.CNPJ.podeSerFormatado("020"), is(false));
        assertThat(Formatador.CNPJ.podeSerFormatado(""), is(false));
        assertThat(Formatador.CNPJ.podeSerFormatado(null), is(false));
    }

    private void assertThrowsFormat(String valor) {
        try {
            Formatador.CNPJ.formata(valor);
            fail("Deveria ter jogado exceção!!!");
        } catch (IllegalArgumentException e) {
        }
    }

    private void assertThrowsDesformat(String valor) {
        try {
            Formatador.CNPJ.desformata(valor);
            fail("Deveria ter jogado exceção!!!");
        } catch (IllegalArgumentException e) {
        }
    }
}


================================================
FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorCPF.java
================================================
package br.com.concrete.canarinho.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import br.com.concrete.canarinho.formatador.Formatador;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.fail;

@RunWith(AndroidJUnit4.class)
public class TesteFormatadorCPF {

    @Test
    public void consegueFormatarCPF() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CPF.formata("545.586.262-66"), is("545.586.262-66"));
        assertThat(Formatador.CPF.formata("54558626266"), is("545.586.262-66"));

        assertThat(Formatador.CPF.formata("020.724.833-87"), is("020.724.833-87"));
        assertThat(Formatador.CPF.formata("02072483387"), is("020.724.833-87"));

        assertThat(Formatador.CPF.formata("188.527.045-31"), is("188.527.045-31"));
        assertThat(Formatador.CPF.formata("18852704531"), is("188.527.045-31"));

        assertThat(Formatador.CPF.formata("047.486.777-32"), is("047.486.777-32"));
        assertThat(Formatador.CPF.formata("04748677732"), is("047.486.777-32"));

        assertThrowsFormat("");
        assertThrowsFormat("123123");
        assertThrowsFormat("123.123.123    -12");
        assertThrowsFormat("       047.486.777-32      ");
    }

    @Test
    public void consegueDesformatarCPF() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CPF.desformata("545.586.262-66"), is("54558626266"));
        assertThat(Formatador.CPF.desformata("54558626266"), is("54558626266"));

        assertThat(Formatador.CPF.desformata("020.724.833-87"), is("02072483387"));
        assertThat(Formatador.CPF.desformata("02072483387"), is("02072483387"));

        assertThat(Formatador.CPF.desformata("188.527.045-31"), is("18852704531"));
        assertThat(Formatador.CPF.desformata("18852704531"), is("18852704531"));

        assertThat(Formatador.CPF.desformata("047.486.777-32"), is("04748677732"));
        assertThat(Formatador.CPF.desformata("04748677732"), is("04748677732"));

        assertThrowsDesformat("");
        assertThrowsDesformat("123123");
        assertThrowsDesformat("123.123.123    -12");
        assertThrowsDesformat("       047.486.777-32      ");
    }

    @Test
    public void consegueDizerSeEstaFormatado() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CPF.estaFormatado("545.586.262-66"), is(true));
        assertThat(Formatador.CPF.estaFormatado("54558626266"), is(false));
        assertThat(Formatador.CPF.estaFormatado("020.724.833-87"), is(true));
        assertThat(Formatador.CPF.estaFormatado("02072483387"), is(false));
        assertThat(Formatador.CPF.estaFormatado("188.527.045-31"), is(true));
        assertThat(Formatador.CPF.estaFormatado("18852704531"), is(false));
        assertThat(Formatador.CPF.estaFormatado("047.486.777-32"), is(true));
        assertThat(Formatador.CPF.estaFormatado("04748677732"), is(false));

        assertThat(Formatador.CPF.estaFormatado("047486"), is(false));
        assertThat(Formatador.CPF.estaFormatado(""), is(false));
        try {
            Formatador.CPF.estaFormatado(null);
            fail("Should have thrown!!!");
        } catch (IllegalArgumentException e) {
        }
    }

    @Test
    public void consegueDizerSePodeFormatar() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CPF.podeSerFormatado("545.586.262-66"), is(false));
        assertThat(Formatador.CPF.podeSerFormatado("54558626266"), is(true));
        assertThat(Formatador.CPF.podeSerFormatado("020"), is(false));
        assertThat(Formatador.CPF.podeSerFormatado(""), is(false));
        assertThat(Formatador.CPF.podeSerFormatado(null), is(false));
    }

    private void assertThrowsFormat(String valor) {
        try {
            Formatador.CPF.formata(valor);
            fail("Should have thrown!!!");
        } catch (IllegalArgumentException e) {
        }
    }

    private void assertThrowsDesformat(String valor) {
        try {
            Formatador.CPF.desformata(valor);
            fail("Should have thrown!!!");
        } catch (IllegalArgumentException e) {
        }
    }
}


================================================
FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorCPFCNPJ.java
================================================
package br.com.concrete.canarinho.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import br.com.concrete.canarinho.formatador.Formatador;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.fail;

@RunWith(AndroidJUnit4.class)
public class TesteFormatadorCPFCNPJ {

    @Test
    public void consegueFormatar() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CPF_CNPJ.formata("50.713.534/0001-33"), is("50.713.534/0001-33"));
        assertThat(Formatador.CPF_CNPJ.formata("50713534000133"), is("50.713.534/0001-33"));

        assertThat(Formatador.CPF_CNPJ.formata("72.606.598/0001-78"), is("72.606.598/0001-78"));
        assertThat(Formatador.CPF_CNPJ.formata("72606598000178"), is("72.606.598/0001-78"));

        assertThat(Formatador.CPF_CNPJ.formata("23.106.535/0001-47"), is("23.106.535/0001-47"));
        assertThat(Formatador.CPF_CNPJ.formata("23106535000147"), is("23.106.535/0001-47"));

        assertThrowsFormat("");
        assertThrowsFormat("123123");
        assertThrowsFormat("23.106.535/0001  -   47");
        assertThrowsFormat("       23.106.535/0001-47      ");

        // Gerados automaticamente para testes
        assertThat(Formatador.CPF_CNPJ.formata("545.586.262-66"), is("545.586.262-66"));
        assertThat(Formatador.CPF_CNPJ.formata("54558626266"), is("545.586.262-66"));

        assertThat(Formatador.CPF_CNPJ.formata("020.724.833-87"), is("020.724.833-87"));
        assertThat(Formatador.CPF_CNPJ.formata("02072483387"), is("020.724.833-87"));

        assertThat(Formatador.CPF_CNPJ.formata("188.527.045-31"), is("188.527.045-31"));
        assertThat(Formatador.CPF_CNPJ.formata("18852704531"), is("188.527.045-31"));

        assertThat(Formatador.CPF_CNPJ.formata("047.486.777-32"), is("047.486.777-32"));
        assertThat(Formatador.CPF_CNPJ.formata("04748677732"), is("047.486.777-32"));

        assertThrowsFormat("");
        assertThrowsFormat("123123");
        assertThrowsFormat("123.123.123    -12");
        assertThrowsFormat("       047.486.777-32      ");
    }

    @Test
    public void consegueDesformatar() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CPF_CNPJ.desformata("50.713.534/0001-33"), is("50713534000133"));
        assertThat(Formatador.CPF_CNPJ.desformata("50713534000133"), is("50713534000133"));

        assertThat(Formatador.CPF_CNPJ.desformata("72.606.598/0001-78"), is("72606598000178"));
        assertThat(Formatador.CPF_CNPJ.desformata("72606598000178"), is("72606598000178"));

        assertThat(Formatador.CPF_CNPJ.desformata("23.106.535/0001-47"), is("23106535000147"));
        assertThat(Formatador.CPF_CNPJ.desformata("23106535000147"), is("23106535000147"));

        assertThrowsDesformat("");
        assertThrowsDesformat("123123");
        assertThrowsDesformat("123.123.123    -12");
        assertThrowsDesformat("       047.486.777-32      ");

        // Gerados automaticamente para testes
        assertThat(Formatador.CPF_CNPJ.desformata("545.586.262-66"), is("54558626266"));
        assertThat(Formatador.CPF_CNPJ.desformata("54558626266"), is("54558626266"));

        assertThat(Formatador.CPF_CNPJ.desformata("020.724.833-87"), is("02072483387"));
        assertThat(Formatador.CPF_CNPJ.desformata("02072483387"), is("02072483387"));

        assertThat(Formatador.CPF_CNPJ.desformata("188.527.045-31"), is("18852704531"));
        assertThat(Formatador.CPF_CNPJ.desformata("18852704531"), is("18852704531"));

        assertThat(Formatador.CPF_CNPJ.desformata("047.486.777-32"), is("04748677732"));
        assertThat(Formatador.CPF_CNPJ.desformata("04748677732"), is("04748677732"));

        assertThrowsDesformat("");
        assertThrowsDesformat("123123");
        assertThrowsDesformat("123.123.123    -12");
        assertThrowsDesformat("       047.486.777-32      ");
    }

    @Test
    public void consegueDizerSeEstaFormatado() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CPF_CNPJ.estaFormatado("72.606.598/0001-78"), is(true));
        assertThat(Formatador.CPF_CNPJ.estaFormatado("72606598000178"), is(false));
        assertThat(Formatador.CPF_CNPJ.estaFormatado("23.106.535/0001-47"), is(true));
        assertThat(Formatador.CPF_CNPJ.estaFormatado("23106535000147"), is(false));

        assertThat(Formatador.CPF_CNPJ.estaFormatado("047486"), is(false));
        assertThat(Formatador.CPF_CNPJ.estaFormatado(""), is(false));
        try {
            Formatador.CPF_CNPJ.estaFormatado(null);
            fail("Should have thrown!!!");
        } catch (IllegalArgumentException e) {
        }

        // Gerados automaticamente para testes
        assertThat(Formatador.CPF_CNPJ.estaFormatado("545.586.262-66"), is(true));
        assertThat(Formatador.CPF_CNPJ.estaFormatado("54558626266"), is(false));
        assertThat(Formatador.CPF_CNPJ.estaFormatado("020.724.833-87"), is(true));
        assertThat(Formatador.CPF_CNPJ.estaFormatado("02072483387"), is(false));
        assertThat(Formatador.CPF_CNPJ.estaFormatado("188.527.045-31"), is(true));
        assertThat(Formatador.CPF_CNPJ.estaFormatado("18852704531"), is(false));
        assertThat(Formatador.CPF_CNPJ.estaFormatado("047.486.777-32"), is(true));
        assertThat(Formatador.CPF_CNPJ.estaFormatado("04748677732"), is(false));

        assertThat(Formatador.CPF_CNPJ.estaFormatado("047486"), is(false));
        assertThat(Formatador.CPF_CNPJ.estaFormatado(""), is(false));
        try {
            Formatador.CPF_CNPJ.estaFormatado(null);
            fail("Should have thrown!!!");
        } catch (IllegalArgumentException e) {
        }
    }

    @Test
    public void consegueDizerSePodeFormatar() {

        // Gerados automaticamente para testes
        assertThat(Formatador.CPF_CNPJ.podeSerFormatado("23.106.535/0001-47"), is(false));
        assertThat(Formatador.CPF_CNPJ.podeSerFormatado("23106535000147"), is(true));
        assertThat(Formatador.CPF_CNPJ.podeSerFormatado("020"), is(false));
        assertThat(Formatador.CPF_CNPJ.podeSerFormatado(""), is(false));
        assertThat(Formatador.CPF_CNPJ.podeSerFormatado(null), is(false));

        // Gerados automaticamente para testes
        assertThat(Formatador.CPF_CNPJ.podeSerFormatado("545.586.262-66"), is(false));
        assertThat(Formatador.CPF_CNPJ.podeSerFormatado("54558626266"), is(true));
        assertThat(Formatador.CPF_CNPJ.podeSerFormatado("020"), is(false));
        assertThat(Formatador.CPF_CNPJ.podeSerFormatado(""), is(false));
        assertThat(Formatador.CPF_CNPJ.podeSerFormatado(null), is(false));
    }

    private void assertThrowsFormat(String valor) {
        try {
            Formatador.CPF_CNPJ.formata(valor);
            fail("Deveria ter jogado exceção!!!");
        } catch (IllegalArgumentException e) {
        }
    }

    private void assertThrowsDesformat(String valor) {
        try {
            Formatador.CPF_CNPJ.desformata(valor);
            fail("Deveria ter jogado exceção!!!");
        } catch (IllegalArgumentException e) {
        }
    }
}


================================================
FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorLinhaDigitavel.java
================================================
package br.com.concrete.canarinho.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import br.com.concrete.canarinho.formatador.Formatador;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.is;

@RunWith(AndroidJUnit4.class)
public class TesteFormatadorLinhaDigitavel {

    @Test
    public void consegueFormatarEDesformatar() {

        assertThat(Formatador.LINHA_DIGITAVEL.desformata("812345678901812345678901812345678901812345678901"),
                is("81234567890812345678908123456789081234567890"));

        final String original = "23790123016000000005325000456704964680000013580";
        final String boleto = Formatador.LINHA_DIGITAVEL.desformata(original);
        final String linhaDigitavel = Formatador.LINHA_DIGITAVEL.formata(boleto);

        assertThat(linhaDigitavel, equalTo(original));
    }

    @Test
    public void consegueDizerSeEstaFormatado() {
        assertThat(Formatador.LINHA_DIGITAVEL.estaFormatado("81234567890812345678908123456789081234567890"), is(false));
        assertThat(Formatador.LINHA_DIGITAVEL.estaFormatado("812345678901 812345678901 812345678901 812345678901"), is(true));
        assertThat(Formatador.LINHA_DIGITAVEL.estaFormatado("23790.12301 60000.000053 25000.456704 9 64680000013580"), is(true));
    }

    @Test
    public void consegueDizerSePodeFormatar() {
        assertThat(Formatador.LINHA_DIGITAVEL.podeSerFormatado("8123456789081234567890812345678908123456"), is(false));
        assertThat(Formatador.LINHA_DIGITAVEL.podeSerFormatado("23790.1230 60000.00005 25000.45670 9 64680000013580"), is(true));
    }
}


================================================
FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorTelefone.java
================================================
package br.com.concrete.canarinho.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import br.com.concrete.canarinho.formatador.Formatador;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.fail;

@RunWith(AndroidJUnit4.class)
public class TesteFormatadorTelefone {

    @Test
    public void consegueFormatar() {
        assertThat(Formatador.TELEFONE.formata("1112345678"),
                is("(11) 1234-5678"));

        assertThat(Formatador.TELEFONE.formata("11123456789"),
                is("(11) 12345-6789"));

        try {
            Formatador.TELEFONE.formata(null);
            fail("Deveria ter jogado exceção!!!");
        } catch (IllegalArgumentException e) {
        }
    }

    @Test
    public void consegueDesformatar() {
        assertThat(Formatador.TELEFONE.desformata("(11) 1234-5678"),
                is("1112345678"));

        assertThat(Formatador.TELEFONE.desformata("(11) 12345-6789"),
                is("11123456789"));

        try {
            Formatador.TELEFONE.desformata(null);
            fail("Deveria ter jogado exceção!!!");
        } catch (IllegalArgumentException e) {
        }
    }

    @Test
    public void consegueDizerSeEstaFormatado() {
        assertThat(Formatador.TELEFONE.estaFormatado("(11) 1234-5678"), is(true));
        assertThat(Formatador.TELEFONE.estaFormatado("(11) 12345-6789"), is(true));

        assertThat(Formatador.TELEFONE.estaFormatado("(11) 1234-56789"), is(false));
        assertThat(Formatador.TELEFONE.estaFormatado("1112345678"), is(false));
        assertThat(Formatador.TELEFONE.estaFormatado("11123456789"), is(false));

        try {
            Formatador.TELEFONE.estaFormatado(null);
            fail("Deveria ter jogado exceção!!!");
        } catch (IllegalArgumentException e) {
        }
    }

    @Test
    public void consegueDizerSePodeFormatar() {
        assertThat(Formatador.TELEFONE.podeSerFormatado("11"), is(false));
        assertThat(Formatador.TELEFONE.podeSerFormatado("111234567890"), is(false));

        assertThat(Formatador.TELEFONE.podeSerFormatado("1112345678"), is(true));
        assertThat(Formatador.TELEFONE.podeSerFormatado("11123456789"), is(true));

        try {
            Formatador.TELEFONE.podeSerFormatado(null);
            fail("Deveria ter jogado exceção!!!");
        } catch (IllegalArgumentException e) {
        }
    }

}


================================================
FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorValor.java
================================================
package br.com.concrete.canarinho.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import br.com.concrete.canarinho.formatador.Formatador;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.fail;

@RunWith(AndroidJUnit4.class)
public class TesteFormatadorValor {

    @Test
    public void consegueFormatar() {

        assertThat(Formatador.VALOR.formata("1.00"), is("1,00"));
        assertThat(Formatador.VALOR.formata("1.0"), is("1,00"));
        assertThat(Formatador.VALOR.formata("1"), is("1,00"));
        assertThat(Formatador.VALOR.formata("1000"), is("1.000,00"));
        assertThat(Formatador.VALOR.formata("1.23"), is("1,23"));
        assertThat(Formatador.VALOR.formata("1.233"), is("1,23"));
        assertThat(Formatador.VALOR.formata("1.01"), is("1,01"));
        assertThat(Formatador.VALOR.formata("1.1"), is("1,10"));
        assertThat(Formatador.VALOR.formata("1234.56"), is("1.234,56"));
        assertThat(Formatador.VALOR.formata("1234.5"), is("1.234,50"));
    }

    @Test
    public void consegueFormatarComSimbolo() {

        assertThat(Formatador.VALOR_COM_SIMBOLO.formata("1.00"), is("R$ 1,00"));
        assertThat(Formatador.VALOR_COM_SIMBOLO.formata("1.0"), is("R$ 1,00"));
        assertThat(Formatador.VALOR_COM_SIMBOLO.formata("1"), is("R$ 1,00"));
        assertThat(Formatador.VALOR_COM_SIMBOLO.formata("1000"), is("R$ 1.000,00"));
        assertThat(Formatador.VALOR_COM_SIMBOLO.formata("1.23"), is("R$ 1,23"));
        assertThat(Formatador.VALOR_COM_SIMBOLO.formata("1.233"), is("R$ 1,23"));
        assertThat(Formatador.VALOR_COM_SIMBOLO.formata("1.01"), is("R$ 1,01"));
        assertThat(Formatador.VALOR_COM_SIMBOLO.formata("1.1"), is("R$ 1,10"));
        assertThat(Formatador.VALOR_COM_SIMBOLO.formata("1234.56"), is("R$ 1.234,56"));
        assertThat(Formatador.VALOR_COM_SIMBOLO.formata("1234.5"), is("R$ 1.234,50"));
    }

    @Test
    public void consegueDesformatar() {
        assertThat(Formatador.VALOR.desformata("1,00"), is("1.00"));
        assertThat(Formatador.VALOR.desformata("1.234,10"), is("1234.10"));
        assertThat(Formatador.VALOR.desformata("0,10"), is("0.10"));
    }

    @Test
    public void consegueDesformatarComSimbolo() {
        assertThat(Formatador.VALOR_COM_SIMBOLO.desformata("R$ 1,00"), is("1.00"));
        assertThat(Formatador.VALOR_COM_SIMBOLO.desformata("R$ 1.234,10"), is("1234.10"));
        assertThat(Formatador.VALOR_COM_SIMBOLO.desformata("R$ 0,10"), is("0.10"));
    }

    @Test
    public void consegueDizerSeEstaFormatado() {

        assertThat(Formatador.VALOR.estaFormatado("1,00"), is(true));
        assertThat(Formatador.VALOR.estaFormatado("1.234,10"), is(true));
        assertThat(Formatador.VALOR.estaFormatado("1.34,10"), is(false));

        try {
            Formatador.VALOR.estaFormatado(null);
            fail("Should have thrown!!!");
        } catch (IllegalArgumentException e) {
        }
    }

    @Test
    public void consegueDizerSePodeFormatar() {

        assertThat(Formatador.VALOR.podeSerFormatado("1.00"), is(true));
        assertThat(Formatador.VALOR.podeSerFormatado("10"), is(true));
        assertThat(Formatador.VALOR.podeSerFormatado("10.1"), is(true));
        assertThat(Formatador.VALOR.podeSerFormatado("10,10"), is(false));

        try {
            Formatador.VALOR.podeSerFormatado(null);
            fail("Should have thrown!!!");
        } catch (IllegalArgumentException e) {
        }
    }
}


================================================
FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteValidadores.java
================================================
package br.com.concrete.canarinho.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import br.com.concrete.canarinho.validator.Validador;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.CoreMatchers.is;

@RunWith(AndroidJUnit4.class)
public class TesteValidadores {

    @Test
    public void consegueValidarCPF() {

        // Gerados automaticamente para testes
        assertThat(Validador.CPF.ehValido("545.586.262-66"), is(true));
        assertThat(Validador.CPF.ehValido("655.274.921-02"), is(true));
        assertThat(Validador.CPF.ehValido("020.724.833-87"), is(true));
        assertThat(Validador.CPF.ehValido("188.527.045-31"), is(true));
        assertThat(Validador.CPF.ehValido("047.486.777-32"), is(true));

        assertThat(Validador.CPF.ehValido("54558626266"), is(true));
        assertThat(Validador.CPF.ehValido("65527492102"), is(true));
        assertThat(Validador.CPF.ehValido("02072483387"), is(true));
        assertThat(Validador.CPF.ehValido("18852704531"), is(true));
        assertThat(Validador.CPF.ehValido("04748677732"), is(true));

        assertThat(Validador.CPF.ehValido("545.111.262-66"), is(false));
        assertThat(Validador.CPF.ehValido("655.111.921-02"), is(false));
        assertThat(Validador.CPF.ehValido("020.111.833-87"), is(false));
        assertThat(Validador.CPF.ehValido("188.111.045-31"), is(false));
        assertThat(Validador.CPF.ehValido("047.111.777-32"), is(false));

        assertThat(Validador.CPF.ehValido("54111626266"), is(false));
        assertThat(Validador.CPF.ehValido("65111492102"), is(false));
        assertThat(Validador.CPF.ehValido("02111483387"), is(false));
        assertThat(Validador.CPF.ehValido("18111704531"), is(false));
        assertThat(Validador.CPF.ehValido("04111677732"), is(false));
    }

    @Test
    public void consegueValidarCNPJ() {

        // Gerados automaticamente para testes
        assertThat(Validador.CNPJ.ehValido("50.713.534/0001-33"), is(true));
        assertThat(Validador.CNPJ.ehValido("77.135.038/0001-04"), is(true));
        assertThat(Validador.CNPJ.ehValido("58.613.246/0001-19"), is(true));
        assertThat(Validador.CNPJ.ehValido("50713534000133"), is(true));
        assertThat(Validador.CNPJ.ehValido("77135038000104"), is(true));

        // Gerados automaticamente para testes
        assertThat(Validador.CNPJ.ehValido("50.713.111/0001-33"), is(false));
        assertThat(Validador.CNPJ.ehValido("77.135.111/0001-04"), is(false));
        assertThat(Validador.CNPJ.ehValido("50713531110133"), is(false));
        assertThat(Validador.CNPJ.ehValido("77135031110104"), is(false));
    }

    @Test
    public void consegueValidarBoletoNormal() {

        // boleto bradesco
        assertThat(Validador.BOLETO
                .ehValido("23790.12301 60000.000053 25000.456704 9 64680000013580"), is(true));

        // boleto bradesco
        assertThat(Validador.BOLETO
                .ehValido("23790123016000000005325000456704964680000013580"), is(true));

        // boleto banco do brasil
        assertThat(Validador.BOLETO
                .ehValido("00199.38414 90480.002550 84666.970219 4 64290000007726"), is(true));


        // Boleto itau desformatado
        assertThat(Validador.BOLETO
                .ehValido("34191750090000159091820521070001664890002370000"), is(true));

        // Boleto itau
        assertThat(Validador.BOLETO
                .ehValido("34191.75009 00001.590918 20521.070001 6 64890002370000"), is(true));

        // boleto Banestes
        assertThat(Validador.BOLETO
                .ehValido("02190.00015 05000.000017 23452.021696 2 25230000034000"), is(true));

        // boleto Caixa
        assertThat(Validador.BOLETO
                .ehValido("10491.44338 55119.000002 00000.000141 3 25230000093423"), is(true));

        // boleto Sudameris
        assertThat(Validador.BOLETO
                .ehValido("34790.12001 12345.695006 10502.000002 5 25230000034000"), is(true));
    }

    @Test
    public void consegueValidarBoletoTributoSemSerTaxa() {

        assertThat(Validador.BOLETO
                .ehValido("848600000015 523301622010 506101307129 620012111220"), is(true));
    }

    @Test
    public void consegueValidarBoletoTributoDeTaxa() {

        assertThat(Validador.BOLETO
                .ehValido("836600000019 078800481000 998854924516 001265611135"), is(true));

        assertThat(Validador.BOLETO
                .ehValido("826100000007 265400971429 620232390612 725103150621"), is(true));
    }

    @Test
    public void consegueValidarTelefone() {
        assertThat(Validador.TELEFONE.ehValido("1112345678"), is(true));
        assertThat(Validador.TELEFONE.ehValido("11123456789"), is(true));
        assertThat(Validador.TELEFONE.ehValido("(11) 12345-6789"), is(true));
        assertThat(Validador.TELEFONE.ehValido("111234567890"), is(false));
        assertThat(Validador.TELEFONE.ehValido("1112345"), is(false));
    }

    @Test
    public void consegueValidarCEP() {
        assertThat(Validador.CEP.ehValido("12345678"), is(true));
        assertThat(Validador.CEP.ehValido("12345-678"), is(true));
        assertThat(Validador.CEP.ehValido("12345-67"), is(false));
        assertThat(Validador.CEP.ehValido("1234-678"), is(false));
        assertThat(Validador.CEP.ehValido(""), is(false));
    }
}


================================================
FILE: sample/src/test/java/br/com/concrete/canarinho/test/watcher/BoletoTextWatcherTest.java
================================================
package br.com.concrete.canarinho.test.watcher;

import android.widget.EditText;

import com.google.android.material.textfield.TextInputLayout;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import br.com.concrete.canarinho.sample.ui.activity.MainActivity;
import br.com.concrete.canarinho.sample.ui.model.Watchers;
import br.com.concrete.canarinho.watcher.BoletoBancarioTextWatcher;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

@RunWith(AndroidJUnit4.class)
public class BoletoTextWatcherTest {

    private BoletoBancarioTextWatcher watcher;
    private EditText editText;

    @Before
    public void setUp() {
        final ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class);
        scenario.onActivity(new ActivityScenario.ActivityAction<MainActivity>() {
            @Override
            public void perform(MainActivity activity) {
                final TextInputLayout textInputLayout = new TextInputLayout(activity);
                textInputLayout.addView(editText = new EditText(activity));

                final Watchers.SampleEventoDeValidacao sampleEventoDeValidacao =
                        new Watchers.SampleEventoDeValidacao(textInputLayout);

                editText.addTextChangedListener(watcher = new BoletoBancarioTextWatcher(sampleEventoDeValidacao));

                activity.setContentView(textInputLayout);
            }
        });
    }

    @Test
    public void typing_canValidateEmptyState() {
        editText.append("");
        assertThat(editText.getText().toString(), is(""));
        assertThat(watcher.getResultadoParcial().isParcialmenteValido(), is(true));
    }

    @Test
    public void typing_canValidateProperCharacters() {
        editText.append("1bas2nas3lamsd4");
        assertThat(editText.getText().toString(), is("1234"));
        assertThat(watcher.getResultadoParcial().isParcialmenteValido(), is(true));
    }

    @Test
    public void deleting_canEmptyEditText() {
        editText.append("1234");
        assertThat(editText.getText().toString(), is("1234"));
        assertThat(watcher.getResultadoParcial().isParcialmenteValido(), is(true));

        editText.getEditableText().clear();
        assertThat(editText.getText().toString(), is(""));
    }

    // Teste de regressão
    @Test
    public void deleting_afterEmptyingEditTextItKeepsValidatingInput() {
        editText.append("1234");
        assertThat(editText.getText().toString(), is("1234"));
        assertThat(watcher.getResultadoParcial().isParcialmenteValido(), is(true));

        editText.getEditableText().clear();
        assertThat(editText.getText().toString(), is(""));

        // menos caracteres que o tamanho inicial para saber qual máscara aplicar
        editText.append("$$");
        assertThat(editText.getText().toString(), is(""));
    }
}


================================================
FILE: sample/src/test/java/br/com/concrete/canarinho/test/watcher/ValorMonetarioWatcherTest.java
================================================
package br.com.concrete.canarinho.test.watcher;

import android.app.Activity;
import android.widget.EditText;

import com.google.android.material.textfield.TextInputLayout;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.robolectric.android.controller.ActivityController;

import br.com.concrete.canarinho.watcher.ValorMonetarioWatcher;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.robolectric.Robolectric.buildActivity;

@RunWith(AndroidJUnit4.class)
public class ValorMonetarioWatcherTest {

    private EditText editText;

    @Before
    public void setUp() {
        final ActivityController<Activity> activityController = buildActivity(Activity.class);
        final Activity activity = activityController.create().get();
        activityController.start().resume().visible();

        TextInputLayout textInputLayout;
        activity.setContentView(textInputLayout = new TextInputLayout(activity));
        textInputLayout.addView(editText = new EditText(activity));
        editText.setText("0,00");
    }

    @Test
    public void watcher_formataOk() {
        editText.addTextChangedListener(new ValorMonetarioWatcher());
        editText.append("1234567890");
        assertThat(editText.getText().toString(), is("12.345.678,90"));
    }

    @Test
    public void watcher_formataOkComSimbolo() {
        editText.addTextChangedListener(new ValorMonetarioWatcher.Builder()
                .comSimboloReal()
                .comMantemZerosAoLimpar()
                .build());
        editText.append("1234567890");
        assertThat(editText.getText().toString(), is("R$ 12.345.678,90"));
    }

    @Test
    public void watcher_canEmptyTextAndKeepZeroes() {
        editText.addTextChangedListener(new ValorMonetarioWatcher.Builder()
                .comSimboloReal()
                .comMantemZerosAoLimpar()
                .build());
        editText.append("1234567890");
        assertThat(editText.getText().toString(), is("R$ 12.345.678,90"));

        editText.getText().clear();
        assertThat(editText.getText().toString(), is("R$ 0,00"));

        editText.getText().append('1');
        assertThat(editText.getText().toString(), is("R$ 0,01"));
    }

    @Test
    public void watcher_canEmptyTextWithoutZeroes() {
        editText.addTextChangedListener(new ValorMonetarioWatcher.Builder()
                .comSimboloReal()
                .build());
        editText.append("1234567890");
        assertThat(editText.getText().toString(), is("R$ 12.345.678,90"));

        editText.getText().clear();
        assertThat(editText.getText().toString(), is(""));

        editText.getText().append('1');
        assertThat(editText.getText().toString(), is("R$ 0,01"));
    }
}


================================================
FILE: settings.gradle
================================================
include ':sample', ':canarinho'


================================================
FILE: tools/linters/checkstyle/checkstyle.xml
================================================
<?xml version="1.0"?><!DOCTYPE module PUBLIC
    "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
    "http://checkstyle.sourceforge.net/dtds/configuration_1_3.dtd">

<!--
    Checkstyle configuration that checks the Google coding conventions from Google Java Style
    that can be found at https://google.github.io/styleguide/javaguide.html.

    Checkstyle is very configurable. Be sure to read the documentation at
    http://checkstyle.sf.net (or in your downloaded distribution).

    To completely disable a check, just comment it out or delete it from the file.

    Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.
 -->

<module name="Checker">
    <property name="charset" value="UTF-8" />

    <property name="severity" value="warning" />

    <property name="fileExtensions" value="java, properties, xml" />
    <!-- Checks for whitespace                               -->
    <!-- See http://checkstyle.sf.net/config_whitespace.html -->
    <module name="FileTabCharacter">
        <property name="eachLine" value="true" />
    </module>

    <module name="TreeWalker">
        <module name="OuterTypeFilename" />
        <module name="IllegalTokenText">
            <property name="tokens" value="STRING_LITERAL, CHAR_LITERAL" />
            <property name="format"
                value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)" />
            <property name="message"
                value="Consider using special escape sequence instead of octal value or Unicode escaped value." />
        </module>
        <module name="AvoidEscapedUnicodeCharacters">
            <property name="allowEscapesForControlCharacters" value="true" />
            <property name="allowByTailComment" value="true" />
            <property name="allowNonPrintableEscapes" value="true" />
        </module>
        <module name="LineLength">
            <property name="max" value="120" />
            <property name="ignorePattern"
                value="^package.*|^import.*|a href|href|http://|https://|ftp://" />
        </module>
        <module name="AvoidStarImport" />
        <module name="OneTopLevelClass" />
        <module name="NoLineWrap" />
        <module name="EmptyBlock">
            <property name="option" value="TEXT" />
            <property name="tokens"
                value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH" />
        </module>
        <module name="NeedBraces" />
        <module name="LeftCurly" />
        <module name="RightCurly">
            <property name="id" value="Righ
Download .txt
gitextract_bmhm5g9q/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       └── android_master.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE.txt
├── PULL_REQUEST_TEMPLATE.md
├── README.md
├── build.gradle
├── canarinho/
│   ├── .gitignore
│   ├── build.gradle
│   └── src/
│       └── main/
│           ├── AndroidManifest.xml
│           └── java/
│               └── br/
│                   └── com/
│                       └── concrete/
│                           └── canarinho/
│                               ├── DigitoPara.java
│                               ├── formatador/
│                               │   ├── Formatador.java
│                               │   ├── FormatadorBase.java
│                               │   ├── FormatadorBoleto.java
│                               │   ├── FormatadorCEP.java
│                               │   ├── FormatadorCPFCNPJ.java
│                               │   ├── FormatadorLinhaDigitavel.java
│                               │   ├── FormatadorTelefone.java
│                               │   └── FormatadorValor.java
│                               ├── validator/
│                               │   ├── Validador.java
│                               │   ├── ValidadorBoleto.java
│                               │   ├── ValidadorCEP.java
│                               │   ├── ValidadorCNPJ.java
│                               │   ├── ValidadorCPF.java
│                               │   ├── ValidadorCPFCNPJ.java
│                               │   └── ValidadorTelefone.java
│                               └── watcher/
│                                   ├── BaseCanarinhoTextWatcher.java
│                                   ├── BoletoBancarioTextWatcher.java
│                                   ├── CEPTextWatcher.java
│                                   ├── CPFCNPJTextWatcher.java
│                                   ├── MascaraNumericaTextWatcher.java
│                                   ├── TelefoneTextWatcher.java
│                                   ├── ValorMonetarioWatcher.java
│                                   └── evento/
│                                       ├── EventoDeValidacao.java
│                                       └── EventoDeValidacaoDeBoleto.java
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── sample/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-rules.pro
│   └── src/
│       ├── androidTest/
│       │   └── java/
│       │       └── br/
│       │           └── com/
│       │               └── concrete/
│       │                   └── canarinho/
│       │                       └── sample/
│       │                           ├── BugOnApi28Test.java
│       │                           └── DemoWatchersInstrumentationTest.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── java/
│       │   │   └── br/
│       │   │       └── com/
│       │   │           └── concrete/
│       │   │               └── canarinho/
│       │   │                   └── sample/
│       │   │                       └── ui/
│       │   │                           ├── activity/
│       │   │                           │   └── MainActivity.java
│       │   │                           ├── adapter/
│       │   │                           │   └── WatchersPagerAdapter.java
│       │   │                           ├── fragment/
│       │   │                           │   ├── BaseWatcherFragment.java
│       │   │                           │   ├── CanarinhoValorMonetarioWatcherFragment.java
│       │   │                           │   └── WatcherFragment.java
│       │   │                           └── model/
│       │   │                               └── Watchers.java
│       │   └── res/
│       │       ├── layout/
│       │       │   ├── fragment_canarinho_watcher.xml
│       │       │   ├── fragment_valor_monetario_watcher.xml
│       │       │   └── main_activity.xml
│       │       └── values/
│       │           ├── strings.xml
│       │           └── styles.xml
│       └── test/
│           └── java/
│               └── br/
│                   └── com/
│                       └── concrete/
│                           └── canarinho/
│                               └── test/
│                                   ├── TesteFormatadorBOLETO.java
│                                   ├── TesteFormatadorCEP.java
│                                   ├── TesteFormatadorCNPJ.java
│                                   ├── TesteFormatadorCPF.java
│                                   ├── TesteFormatadorCPFCNPJ.java
│                                   ├── TesteFormatadorLinhaDigitavel.java
│                                   ├── TesteFormatadorTelefone.java
│                                   ├── TesteFormatadorValor.java
│                                   ├── TesteValidadores.java
│                                   └── watcher/
│                                       ├── BoletoTextWatcherTest.java
│                                       └── ValorMonetarioWatcherTest.java
├── settings.gradle
└── tools/
    ├── linters/
    │   ├── checkstyle/
    │   │   ├── checkstyle.xml
    │   │   └── suppressions.xml
    │   ├── linters.gradle
    │   └── pmd/
    │       └── pmd-ruleset.xml
    └── publish.gradle
Download .txt
SYMBOL INDEX (327 symbols across 44 files)

FILE: canarinho/src/main/java/br/com/concrete/canarinho/DigitoPara.java
  class DigitoPara (line 38) | public final class DigitoPara {
    method DigitoPara (line 47) | private DigitoPara(Builder builder) {
    method calcula (line 63) | public final String calcula(String trecho) {
    method somaDigitos (line 104) | private int somaDigitos(int total) {
    method proximoMultiplicador (line 112) | private int proximoMultiplicador(int multiplicadorDaVez) {
    class Builder (line 127) | public static final class Builder {
      method mod (line 142) | public final Builder mod(int modulo) {
      method comMultiplicadoresDeAte (line 157) | public final Builder comMultiplicadoresDeAte(int inicio, int fim) {
      method somandoIndividualmente (line 177) | public final Builder somandoIndividualmente() {
      method complementarAoModulo (line 190) | public final Builder complementarAoModulo() {
      method trocandoPorSeEncontrar (line 202) | public final Builder trocandoPorSeEncontrar(String substituto, Integ...
      method comMultiplicadores (line 221) | public final Builder comMultiplicadores(Integer... multiplicadoresEm...
      method build (line 237) | public final DigitoPara build() {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/Formatador.java
  type Formatador (line 9) | public interface Formatador {
    method formata (line 79) | String formata(String value);
    method desformata (line 87) | String desformata(String value);
    method estaFormatado (line 95) | boolean estaFormatado(String value);
    method podeSerFormatado (line 103) | boolean podeSerFormatado(String value);
    class Padroes (line 108) | abstract class Padroes {
      method Padroes (line 130) | private Padroes() {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorBase.java
  class FormatadorBase (line 10) | final class FormatadorBase implements Formatador {
    method FormatadorBase (line 28) | FormatadorBase(
    method formata (line 40) | @Override
    method desformata (line 54) | @Override
    method estaFormatado (line 69) | @Override
    method podeSerFormatado (line 79) | @Override
    method matchAndReplace (line 84) | private String matchAndReplace(Matcher matcher, String replacement) {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorBoleto.java
  class FormatadorBoleto (line 11) | public final class FormatadorBoleto implements Formatador {
    method FormatadorBoleto (line 41) | private FormatadorBoleto() {
    method formata (line 44) | @Override
    method desformata (line 54) | @Override
    method estaFormatado (line 64) | @Override
    method podeSerFormatado (line 74) | @Override
    method ehTributo (line 84) | private boolean ehTributo(String value) {
    method getInstance (line 93) | static FormatadorBoleto getInstance() {
    class SingletonHolder (line 97) | private static class SingletonHolder {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorCEP.java
  class FormatadorCEP (line 6) | public final class FormatadorCEP implements Formatador {
    method FormatadorCEP (line 8) | private FormatadorCEP() {
    method getInstance (line 11) | static FormatadorCEP getInstance() {
    method formata (line 15) | @Override
    method desformata (line 20) | @Override
    method estaFormatado (line 25) | @Override
    method podeSerFormatado (line 30) | @Override
    class SingletonHolder (line 39) | private static class SingletonHolder {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorCPFCNPJ.java
  class FormatadorCPFCNPJ (line 7) | public final class FormatadorCPFCNPJ implements Formatador {
    method FormatadorCPFCNPJ (line 9) | private FormatadorCPFCNPJ() {
    method getInstance (line 12) | static FormatadorCPFCNPJ getInstance() {
    method formata (line 16) | @Override
    method desformata (line 25) | @Override
    method estaFormatado (line 34) | @Override
    method podeSerFormatado (line 43) | @Override
    method ehCpf (line 56) | private boolean ehCpf(String value) {
    class SingletonHolder (line 66) | private static class SingletonHolder {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorLinhaDigitavel.java
  class FormatadorLinhaDigitavel (line 16) | public final class FormatadorLinhaDigitavel implements Formatador {
    method FormatadorLinhaDigitavel (line 18) | private FormatadorLinhaDigitavel() {
    method getInstance (line 21) | static FormatadorLinhaDigitavel getInstance() {
    method formata (line 25) | @Override
    method desformata (line 79) | @Override
    method estaFormatado (line 131) | @Override
    method podeSerFormatado (line 136) | @Override
    class SingletonHolder (line 143) | private static class SingletonHolder {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorTelefone.java
  class FormatadorTelefone (line 8) | public final class FormatadorTelefone implements Formatador {
    method FormatadorTelefone (line 40) | private FormatadorTelefone() {
    method getInstance (line 43) | static FormatadorTelefone getInstance() {
    method formata (line 47) | @Override
    method desformata (line 56) | @Override
    method estaFormatado (line 65) | @Override
    method podeSerFormatado (line 74) | @Override
    method ehNoveDigitos (line 83) | private boolean ehNoveDigitos(String value) {
    class SingletonHolder (line 93) | private static class SingletonHolder {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorValor.java
  class FormatadorValor (line 20) | public final class FormatadorValor implements Formatador {
    method FormatadorValor (line 45) | private FormatadorValor(boolean comSimboloReal) {
    method getInstance (line 55) | static FormatadorValor getInstance(boolean comSimboloReal) {
    method formata (line 61) | @Override
    method desformata (line 67) | @Override
    method estaFormatado (line 88) | @Override
    method podeSerFormatado (line 98) | @Override
    class SingletonHolder (line 108) | private static class SingletonHolder {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/Validador.java
  type Validador (line 14) | public interface Validador {
    method ehValido (line 47) | boolean ehValido(String valor);
    method ehValido (line 57) | ResultadoParcial ehValido(Editable valor, ResultadoParcial resultadoPa...
    class ResultadoParcial (line 62) | class ResultadoParcial {
      method isValido (line 68) | public boolean isValido() {
      method isParcialmenteValido (line 72) | public boolean isParcialmenteValido() {
      method getMensagem (line 76) | public String getMensagem() {
      method totalmenteValido (line 86) | public ResultadoParcial totalmenteValido(boolean valido) {
      method parcialmenteValido (line 97) | public ResultadoParcial parcialmenteValido(boolean parcialmenteValid...
      method mensagem (line 108) | public ResultadoParcial mensagem(String mensagem) {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorBoleto.java
  class ValidadorBoleto (line 16) | public final class ValidadorBoleto implements Validador {
    method ValidadorBoleto (line 41) | private ValidadorBoleto() {
    method getInstance (line 44) | public static ValidadorBoleto getInstance() {
    method ehValido (line 48) | @Override
    method ehValido (line 59) | @Override
    method validaNormal (line 85) | private ResultadoParcial validaNormal(String valor, ResultadoParcial r...
    method validaTributo (line 106) | private ResultadoParcial validaTributo(String valor, ResultadoParcial ...
    method ehTributo (line 136) | private boolean ehTributo(CharSequence valor) {
    method validaBloco (line 140) | private boolean validaBloco(String valor, ResultadoParcial resultadoPa...
    class SingletonHolder (line 162) | private static class SingletonHolder {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorCEP.java
  class ValidadorCEP (line 12) | public final class ValidadorCEP implements Validador {
    method ValidadorCEP (line 15) | private ValidadorCEP() {
    method getInstance (line 18) | public static ValidadorCEP getInstance() {
    method ehValido (line 22) | @Override
    method ehValido (line 34) | @Override
    class SingletonHolder (line 55) | private static class SingletonHolder {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorCNPJ.java
  class ValidadorCNPJ (line 13) | public final class ValidadorCNPJ implements Validador {
    method ValidadorCNPJ (line 21) | private ValidadorCNPJ() {
    method getInstance (line 24) | public static ValidadorCNPJ getInstance() {
    method ehValido (line 28) | @Override
    method ehValido (line 50) | @Override
    class SingletonHolder (line 71) | private static class SingletonHolder {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorCPF.java
  class ValidadorCPF (line 13) | public final class ValidadorCPF implements Validador {
    method ValidadorCPF (line 22) | private ValidadorCPF() {
    method getInstance (line 25) | static ValidadorCPF getInstance() {
    method ehValido (line 29) | @Override
    method ehValido (line 55) | @Override
    method estaNaListaNegra (line 80) | private boolean estaNaListaNegra(String valor) {
    class SingletonHolder (line 93) | private static class SingletonHolder {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorCPFCNPJ.java
  class ValidadorCPFCNPJ (line 7) | public final class ValidadorCPFCNPJ implements Validador {
    method ValidadorCPFCNPJ (line 10) | private ValidadorCPFCNPJ() {
    method getInstance (line 13) | public static ValidadorCPFCNPJ getInstance() {
    method ehValido (line 17) | @Override
    method ehValido (line 30) | @Override
    method ehCpf (line 43) | private boolean ehCpf(String valor) {
    class SingletonHolder (line 52) | private static class SingletonHolder {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorTelefone.java
  class ValidadorTelefone (line 7) | public final class ValidadorTelefone implements Validador {
    method ValidadorTelefone (line 10) | private ValidadorTelefone() {
    method getInstance (line 13) | public static ValidadorTelefone getInstance() {
    method ehValido (line 17) | @Override
    method ehValido (line 28) | @Override
    class SingletonHolder (line 48) | private static class SingletonHolder {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/BaseCanarinhoTextWatcher.java
  class BaseCanarinhoTextWatcher (line 16) | public abstract class BaseCanarinhoTextWatcher implements TextWatcher {
    method beforeTextChanged (line 22) | @Override
    method onTextChanged (line 27) | @Override
    method isMudancaInterna (line 32) | public boolean isMudancaInterna() {
    method getEventoDeValidacao (line 36) | @SuppressWarnings("unchecked")
    method setEventoDeValidacao (line 41) | public void setEventoDeValidacao(EventoDeValidacao eventoDeValidacao) {
    method isApagouCaracter (line 52) | protected boolean isApagouCaracter(Editable s) {
    method atualizaTexto (line 67) | protected void atualizaTexto(Validador validador, Validador.ResultadoP...
    method efetuaValidacao (line 91) | protected void efetuaValidacao(Validador validador, Validador.Resultad...
    method trataAdicaoRemocaoDeCaracter (line 120) | protected StringBuilder trataAdicaoRemocaoDeCaracter(Editable s, char[...
    method trataAdicaoDeCaracter (line 126) | private StringBuilder trataAdicaoDeCaracter(Editable s, char[] mascara) {
    method trataRemocaoDeCaracter (line 131) | private StringBuilder trataRemocaoDeCaracter(Editable s, char[] mascar...
    method carregarMascara (line 159) | private StringBuilder carregarMascara(String s, char[] mascara) {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/BoletoBancarioTextWatcher.java
  class BoletoBancarioTextWatcher (line 18) | public final class BoletoBancarioTextWatcher extends BaseCanarinhoTextWa...
    method BoletoBancarioTextWatcher (line 35) | public BoletoBancarioTextWatcher(EventoDeValidacao callbackErros) {
    method afterTextChanged (line 39) | @Override
    method getResultadoParcial (line 71) | public Validador.ResultadoParcial getResultadoParcial() {
    method efetuaValidacao (line 75) | @Override
    method verificaFiltro (line 118) | private void verificaFiltro(final Editable s, final boolean tributo) {
    method ehTributo (line 128) | private boolean ehTributo(Editable e) {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/CEPTextWatcher.java
  class CEPTextWatcher (line 15) | public final class CEPTextWatcher extends BaseCanarinhoTextWatcher {
    method CEPTextWatcher (line 30) | public CEPTextWatcher(EventoDeValidacao callbackErros) {
    method afterTextChanged (line 34) | @Override

FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/CPFCNPJTextWatcher.java
  class CPFCNPJTextWatcher (line 17) | public class CPFCNPJTextWatcher extends BaseCanarinhoTextWatcher {
    method CPFCNPJTextWatcher (line 29) | public CPFCNPJTextWatcher() {
    method CPFCNPJTextWatcher (line 37) | public CPFCNPJTextWatcher(EventoDeValidacao callbackErros) {
    method afterTextChanged (line 41) | @Override
    method ehCpf (line 56) | private boolean ehCpf(Editable e) {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/MascaraNumericaTextWatcher.java
  class MascaraNumericaTextWatcher (line 16) | public final class MascaraNumericaTextWatcher extends BaseCanarinhoTextW...
    method MascaraNumericaTextWatcher (line 28) | public MascaraNumericaTextWatcher(String mascara) {
    method MascaraNumericaTextWatcher (line 32) | private MascaraNumericaTextWatcher(Builder builder) {
    method afterTextChanged (line 42) | @Override
    class Builder (line 63) | public static final class Builder {
      method comValidador (line 76) | public Builder comValidador(Validador validador) {
      method comCallbackDeValidacao (line 88) | public Builder comCallbackDeValidacao(EventoDeValidacao callbackErro...
      method paraMascara (line 101) | public Builder paraMascara(String mascara) {
      method build (line 111) | public final MascaraNumericaTextWatcher build() {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/TelefoneTextWatcher.java
  class TelefoneTextWatcher (line 17) | public final class TelefoneTextWatcher extends BaseCanarinhoTextWatcher {
    method TelefoneTextWatcher (line 32) | public TelefoneTextWatcher(EventoDeValidacao callbackErros) {
    method afterTextChanged (line 36) | @Override
    method ehNoveDigitos (line 52) | private boolean ehNoveDigitos(Editable e) {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/ValorMonetarioWatcher.java
  class ValorMonetarioWatcher (line 16) | public class ValorMonetarioWatcher implements TextWatcher {
    method ValorMonetarioWatcher (line 25) | public ValorMonetarioWatcher() {
    method ValorMonetarioWatcher (line 35) | ValorMonetarioWatcher(boolean comSimboloReal, boolean mantemZerosAoLim...
    method beforeTextChanged (line 42) | @Override
    method onTextChanged (line 47) | @Override
    method afterTextChanged (line 52) | @Override
    method atualizaTexto (line 79) | private void atualizaTexto(Editable editable, String valor) {
    class Builder (line 100) | public static class Builder {
      method comMantemZerosAoLimpar (line 110) | public Builder comMantemZerosAoLimpar() {
      method comSimboloReal (line 120) | public Builder comSimboloReal() {
      method build (line 130) | public ValorMonetarioWatcher build() {

FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/evento/EventoDeValidacao.java
  type EventoDeValidacao (line 7) | public interface EventoDeValidacao {
    method invalido (line 15) | void invalido(String valorAtual, String mensagem);
    method parcialmenteValido (line 23) | void parcialmenteValido(String valorAtual);
    method totalmenteValido (line 30) | void totalmenteValido(String valorAtual);

FILE: canarinho/src/main/java/br/com/concrete/canarinho/watcher/evento/EventoDeValidacaoDeBoleto.java
  type EventoDeValidacaoDeBoleto (line 7) | public interface EventoDeValidacaoDeBoleto extends EventoDeValidacao {
    method invalido (line 15) | void invalido(String valorAtual, int blocoInvalido);

FILE: sample/src/androidTest/java/br/com/concrete/canarinho/sample/BugOnApi28Test.java
  class BugOnApi28Test (line 12) | @RunWith(AndroidJUnit4.class)
    method moneyFormatSuccessfulRunsOnApi28 (line 15) | @Test
    method moneyFormatSuccessfulRunsOnApi27 (line 22) | @Test

FILE: sample/src/androidTest/java/br/com/concrete/canarinho/sample/DemoWatchersInstrumentationTest.java
  class DemoWatchersInstrumentationTest (line 36) | @RunWith(AndroidJUnit4.class)
    method consegueDigitarUmBoletoNormalValido (line 42) | @Test
    method consegueValidarUmBoletoSetandoOCodigoInteiro (line 53) | @Test
    method consegueValidarUmBoletoTributoSetandoOCodigoInteiro (line 64) | @Test
    method consegueDigitarUmBoletoNormalComBlocosInvalidos (line 75) | @Test
    method consegueDigitarUmBoletoTributoValido (line 96) | @Test
    method consegueDigitarUmBoletoNormalComBlocosInvalidosComMensagemCustomizada (line 117) | @Test
    method consegueDigitarUmCPFValido (line 138) | @Test
    method consegueDigitarUmCPFInvalido (line 150) | @Test
    method consegueDigitarUmCNPJValido (line 159) | @Test
    method consegueDigitarUmCNPJInvalido (line 171) | @Test
    method consegueDigitarUmTelefoneValido (line 179) | @Test
    method consegueDigitarUmValorMonetarioFormatado (line 197) | @Test
    method consegueDigitarCPFCNPJValido (line 227) | @Test
    method consegueDigitarCPFCNPJInvalido (line 238) | @Test
    method consegueDigitarUmCEPValido (line 250) | @Test
    method consegueUtilizarUmaMascaraGenericaSemValidadorOuEvento (line 261) | @Test
    method navigateToTab (line 271) | private void navigateToTab(Watchers watcher) {
    method paste (line 280) | private ViewAction paste(final String type) {

FILE: sample/src/main/java/br/com/concrete/canarinho/sample/ui/activity/MainActivity.java
  class MainActivity (line 15) | public class MainActivity extends AppCompatActivity {
    method onCreate (line 17) | @Override

FILE: sample/src/main/java/br/com/concrete/canarinho/sample/ui/adapter/WatchersPagerAdapter.java
  class WatchersPagerAdapter (line 8) | public class WatchersPagerAdapter extends FragmentPagerAdapter {
    method WatchersPagerAdapter (line 12) | public WatchersPagerAdapter(FragmentManager fragmentManager) {
    method getItem (line 16) | @Override
    method getCount (line 21) | @Override
    method getPageTitle (line 26) | @Override

FILE: sample/src/main/java/br/com/concrete/canarinho/sample/ui/fragment/BaseWatcherFragment.java
  class BaseWatcherFragment (line 6) | public abstract class BaseWatcherFragment extends Fragment {
    method setModel (line 10) | public void setModel(Watchers model) {

FILE: sample/src/main/java/br/com/concrete/canarinho/sample/ui/fragment/CanarinhoValorMonetarioWatcherFragment.java
  class CanarinhoValorMonetarioWatcherFragment (line 19) | public class CanarinhoValorMonetarioWatcherFragment extends BaseWatcherF...
    method onCreateView (line 26) | @Nullable
    method bind (line 39) | public CanarinhoValorMonetarioWatcherFragment bind(Watchers model) {

FILE: sample/src/main/java/br/com/concrete/canarinho/sample/ui/fragment/WatcherFragment.java
  class WatcherFragment (line 18) | public class WatcherFragment extends BaseWatcherFragment {
    method newInstance (line 25) | public static WatcherFragment newInstance(Watchers model) {
    method onCreateView (line 32) | @Nullable
    method bind (line 44) | public WatcherFragment bind(Watchers model) {

FILE: sample/src/main/java/br/com/concrete/canarinho/sample/ui/model/Watchers.java
  type Watchers (line 22) | public enum Watchers {
    method buildFragment (line 25) | @Override
    method setupWatcher (line 30) | @Override
    method buildFragment (line 36) | @Override
    method setupWatcher (line 41) | @Override
    method buildFragment (line 48) | @Override
    method setupWatcher (line 53) | @Override
    method buildFragment (line 64) | @Override
    method setupWatcher (line 69) | @Override
    method buildFragment (line 80) | @Override
    method setupWatcher (line 85) | @Override
    method buildFragment (line 92) | @Override
    method setupWatcher (line 97) | @Override
    method buildFragment (line 104) | @Override
    method setupWatcher (line 109) | @Override
    method buildFragment (line 116) | @Override
    method setupWatcher (line 121) | @Override
    method buildFragment (line 128) | @Override
    method setupWatcher (line 133) | @Override
    method Watchers (line 142) | Watchers(String title, String hint) {
    method getTitle (line 147) | public String getTitle() {
    method getHint (line 151) | public String getHint() {
    method buildFragment (line 155) | public abstract BaseWatcherFragment buildFragment();
    method setupWatcher (line 157) | public abstract TextWatcher setupWatcher(TextInputLayout textInputLayo...
    class SampleEventoDeValidacao (line 163) | public static class SampleEventoDeValidacao implements EventoDeValidac...
      method SampleEventoDeValidacao (line 167) | public SampleEventoDeValidacao(TextInputLayout textInputLayout) {
      method invalido (line 171) | @Override
      method parcialmenteValido (line 176) | @Override
      method totalmenteValido (line 182) | @Override
    class EventoDeValidacaoBoleto (line 196) | public static class EventoDeValidacaoBoleto
      method EventoDeValidacaoBoleto (line 200) | EventoDeValidacaoBoleto(TextInputLayout textInputLayout) {
      method invalido (line 204) | @Override

FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorBOLETO.java
  class TesteFormatadorBOLETO (line 12) | @RunWith(AndroidJUnit4.class)
    method consegueFormatar (line 15) | @Test
    method consegueDesformatar (line 33) | @Test
    method consegueDizerSeEstaFormatado (line 52) | @Test
    method consegueDizerSePodeFormatar (line 63) | @Test

FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorCEP.java
  class TesteFormatadorCEP (line 13) | @RunWith(AndroidJUnit4.class)
    method consegueFormatar (line 16) | @Test
    method consegueDesformatar (line 30) | @Test
    method consegueDizerSeEstaFormatado (line 44) | @Test
    method consegueDizerSePodeFormatar (line 62) | @Test
    method assertThrowsFormat (line 73) | private void assertThrowsFormat(String valor) {
    method assertThrowsDesformat (line 81) | private void assertThrowsDesformat(String valor) {

FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorCNPJ.java
  class TesteFormatadorCNPJ (line 13) | @RunWith(AndroidJUnit4.class)
    method consegueFormatar (line 16) | @Test
    method consegueDesformatar (line 35) | @Test
    method consegueDizerSeEstaFormatado (line 54) | @Test
    method consegueDizerSePodeFormatar (line 72) | @Test
    method assertThrowsFormat (line 83) | private void assertThrowsFormat(String valor) {
    method assertThrowsDesformat (line 91) | private void assertThrowsDesformat(String valor) {

FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorCPF.java
  class TesteFormatadorCPF (line 13) | @RunWith(AndroidJUnit4.class)
    method consegueFormatarCPF (line 16) | @Test
    method consegueDesformatarCPF (line 38) | @Test
    method consegueDizerSeEstaFormatado (line 60) | @Test
    method consegueDizerSePodeFormatar (line 82) | @Test
    method assertThrowsFormat (line 93) | private void assertThrowsFormat(String valor) {
    method assertThrowsDesformat (line 101) | private void assertThrowsDesformat(String valor) {

FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorCPFCNPJ.java
  class TesteFormatadorCPFCNPJ (line 13) | @RunWith(AndroidJUnit4.class)
    method consegueFormatar (line 16) | @Test
    method consegueDesformatar (line 53) | @Test
    method consegueDizerSeEstaFormatado (line 90) | @Test
    method consegueDizerSePodeFormatar (line 126) | @Test
    method assertThrowsFormat (line 144) | private void assertThrowsFormat(String valor) {
    method assertThrowsDesformat (line 152) | private void assertThrowsDesformat(String valor) {

FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorLinhaDigitavel.java
  class TesteFormatadorLinhaDigitavel (line 13) | @RunWith(AndroidJUnit4.class)
    method consegueFormatarEDesformatar (line 16) | @Test
    method consegueDizerSeEstaFormatado (line 29) | @Test
    method consegueDizerSePodeFormatar (line 36) | @Test

FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorTelefone.java
  class TesteFormatadorTelefone (line 13) | @RunWith(AndroidJUnit4.class)
    method consegueFormatar (line 16) | @Test
    method consegueDesformatar (line 31) | @Test
    method consegueDizerSeEstaFormatado (line 46) | @Test
    method consegueDizerSePodeFormatar (line 62) | @Test

FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorValor.java
  class TesteFormatadorValor (line 13) | @RunWith(AndroidJUnit4.class)
    method consegueFormatar (line 16) | @Test
    method consegueFormatarComSimbolo (line 31) | @Test
    method consegueDesformatar (line 46) | @Test
    method consegueDesformatarComSimbolo (line 53) | @Test
    method consegueDizerSeEstaFormatado (line 60) | @Test
    method consegueDizerSePodeFormatar (line 74) | @Test

FILE: sample/src/test/java/br/com/concrete/canarinho/test/TesteValidadores.java
  class TesteValidadores (line 12) | @RunWith(AndroidJUnit4.class)
    method consegueValidarCPF (line 15) | @Test
    method consegueValidarCNPJ (line 44) | @Test
    method consegueValidarBoletoNormal (line 61) | @Test
    method consegueValidarBoletoTributoSemSerTaxa (line 98) | @Test
    method consegueValidarBoletoTributoDeTaxa (line 105) | @Test
    method consegueValidarTelefone (line 115) | @Test
    method consegueValidarCEP (line 124) | @Test

FILE: sample/src/test/java/br/com/concrete/canarinho/test/watcher/BoletoTextWatcherTest.java
  class BoletoTextWatcherTest (line 20) | @RunWith(AndroidJUnit4.class)
    method setUp (line 26) | @Before
    method typing_canValidateEmptyState (line 45) | @Test
    method typing_canValidateProperCharacters (line 52) | @Test
    method deleting_canEmptyEditText (line 59) | @Test
    method deleting_afterEmptyingEditTextItKeepsValidatingInput (line 70) | @Test

FILE: sample/src/test/java/br/com/concrete/canarinho/test/watcher/ValorMonetarioWatcherTest.java
  class ValorMonetarioWatcherTest (line 20) | @RunWith(AndroidJUnit4.class)
    method setUp (line 25) | @Before
    method watcher_formataOk (line 37) | @Test
    method watcher_formataOkComSimbolo (line 44) | @Test
    method watcher_canEmptyTextAndKeepZeroes (line 54) | @Test
    method watcher_canEmptyTextWithoutZeroes (line 70) | @Test
Condensed preview — 77 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (217K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 799,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\n\n---\n\n**Describe the bug**\nA clear and concise descriptio"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 560,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\n\n---\n\n**Is your feature request related to a problem? "
  },
  {
    "path": ".github/workflows/android_master.yml",
    "chars": 1092,
    "preview": "name: Android Pull Request Master CI\n\non:\n  pull_request:\n    branches:\n      - 'master'\n\njobs:\n  Instrumented_Test:\n   "
  },
  {
    "path": ".gitignore",
    "chars": 395,
    "preview": ".gradle\n/local.properties\n/.idea/workspace.xml\n/.idea/libraries\n.DS_Store\n/build\n/captures\n.idea/\n*.iml\n\n# Built applica"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 2068,
    "preview": "# Changelog\n\n## 2.0.3\n    - Migração do bintray para Github Package Registry\n\n## 2.0.2\n    - Atualização androidx\n    - "
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3224,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
  },
  {
    "path": "LICENSE.txt",
    "chars": 10142,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "PULL_REQUEST_TEMPLATE.md",
    "chars": 288,
    "preview": "# Título para o PullRequest\nO que acha de deixar uma descrição simples sobre o seu PR?\n\n## Tipo de mudança\n- [ ] Bug fix"
  },
  {
    "path": "README.md",
    "chars": 5309,
    "preview": "# Android Canarinho\n\n![Build](https://github.com/concretesolutions/canarinho/actions/workflows/android_master.yml/badge."
  },
  {
    "path": "build.gradle",
    "chars": 382,
    "preview": "buildscript {\n    repositories {\n        google()\n        jcenter()\n    }\n\n    dependencies {\n        classpath 'com.and"
  },
  {
    "path": "canarinho/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "canarinho/build.gradle",
    "chars": 630,
    "preview": "apply plugin: 'com.android.library'\napply from: \"$rootDir/tools/linters/linters.gradle\"\napply from: \"$rootDir/tools/publ"
  },
  {
    "path": "canarinho/src/main/AndroidManifest.xml",
    "chars": 49,
    "preview": "<manifest package=\"br.com.concrete.canarinho\" />\n"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/DigitoPara.java",
    "chars": 7981,
    "preview": "package br.com.concrete.canarinho;\n\nimport android.util.SparseArray;\n\nimport java.util.ArrayList;\nimport java.util.Array"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/formatador/Formatador.java",
    "chars": 3662,
    "preview": "package br.com.concrete.canarinho.formatador;\n\nimport java.util.regex.Pattern;\n\n/**\n * Interface de formatação. Formata "
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorBase.java",
    "chars": 2806,
    "preview": "package br.com.concrete.canarinho.formatador;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * C"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorBoleto.java",
    "chars": 2936,
    "preview": "package br.com.concrete.canarinho.formatador;\n\nimport java.util.regex.Pattern;\n\n/**\n * Formatador especializado para lin"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorCEP.java",
    "chars": 978,
    "preview": "package br.com.concrete.canarinho.formatador;\n\n/**\n * Formatador para CEP. Segue o padrão 99999-999.\n */\npublic final cl"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorCPFCNPJ.java",
    "chars": 1777,
    "preview": "package br.com.concrete.canarinho.formatador;\n\n/**\n * Formatador para CPF e CNPJ no mesmo campo. Formata como CPF até 11"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorLinhaDigitavel.java",
    "chars": 5931,
    "preview": "package br.com.concrete.canarinho.formatador;\n\nimport br.com.concrete.canarinho.DigitoPara;\nimport br.com.concrete.canar"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorTelefone.java",
    "chars": 2709,
    "preview": "package br.com.concrete.canarinho.formatador;\n\nimport java.util.regex.Pattern;\n\n/**\n * Formata no padrão de telefone bra"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/formatador/FormatadorValor.java",
    "chars": 3715,
    "preview": "package br.com.concrete.canarinho.formatador;\n\nimport android.os.Build;\n\nimport java.math.BigDecimal;\nimport java.text.D"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/validator/Validador.java",
    "chars": 3278,
    "preview": "package br.com.concrete.canarinho.validator;\n\nimport android.text.Editable;\n\n/**\n * Interface de validação de campos. Há"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorBoleto.java",
    "chars": 5386,
    "preview": "package br.com.concrete.canarinho.validator;\n\nimport android.text.Editable;\nimport android.text.SpannableStringBuilder;\n"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorCEP.java",
    "chars": 1619,
    "preview": "package br.com.concrete.canarinho.validator;\n\nimport android.text.Editable;\n\nimport br.com.concrete.canarinho.formatador"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorCNPJ.java",
    "chars": 2233,
    "preview": "package br.com.concrete.canarinho.validator;\n\nimport android.text.Editable;\n\nimport br.com.concrete.canarinho.DigitoPara"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorCPF.java",
    "chars": 2954,
    "preview": "package br.com.concrete.canarinho.validator;\n\nimport android.text.Editable;\n\nimport br.com.concrete.canarinho.DigitoPara"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorCPFCNPJ.java",
    "chars": 1636,
    "preview": "package br.com.concrete.canarinho.validator;\n\nimport android.text.Editable;\n\nimport br.com.concrete.canarinho.formatador"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/validator/ValidadorTelefone.java",
    "chars": 1571,
    "preview": "package br.com.concrete.canarinho.validator;\n\nimport android.text.Editable;\n\nimport br.com.concrete.canarinho.formatador"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/watcher/BaseCanarinhoTextWatcher.java",
    "chars": 6584,
    "preview": "package br.com.concrete.canarinho.watcher;\n\nimport android.text.Editable;\nimport android.text.Selection;\nimport android."
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/watcher/BoletoBancarioTextWatcher.java",
    "chars": 4822,
    "preview": "package br.com.concrete.canarinho.watcher;\n\nimport android.text.Editable;\nimport android.text.InputFilter;\n\nimport br.co"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/watcher/CEPTextWatcher.java",
    "chars": 1610,
    "preview": "package br.com.concrete.canarinho.watcher;\n\nimport android.text.Editable;\nimport android.text.InputFilter;\n\nimport br.co"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/watcher/CPFCNPJTextWatcher.java",
    "chars": 1997,
    "preview": "package br.com.concrete.canarinho.watcher;\n\nimport android.text.Editable;\nimport android.text.InputFilter;\n\nimport br.co"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/watcher/MascaraNumericaTextWatcher.java",
    "chars": 3916,
    "preview": "package br.com.concrete.canarinho.watcher;\n\nimport android.text.Editable;\nimport android.text.InputFilter;\n\nimport br.co"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/watcher/TelefoneTextWatcher.java",
    "chars": 2036,
    "preview": "package br.com.concrete.canarinho.watcher;\n\nimport android.text.Editable;\nimport android.text.InputFilter;\n\nimport br.co"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/watcher/ValorMonetarioWatcher.java",
    "chars": 3862,
    "preview": "package br.com.concrete.canarinho.watcher;\n\nimport android.text.Editable;\nimport android.text.InputFilter;\nimport androi"
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/watcher/evento/EventoDeValidacao.java",
    "chars": 1124,
    "preview": "package br.com.concrete.canarinho.watcher.evento;\n\n/**\n * Interface para quem estiver usando este TextWatcher poder ter "
  },
  {
    "path": "canarinho/src/main/java/br/com/concrete/canarinho/watcher/evento/EventoDeValidacaoDeBoleto.java",
    "chars": 546,
    "preview": "package br.com.concrete.canarinho.watcher.evento;\n\n/**\n * Evento de validação específico para boletos que permite saber "
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 202,
    "preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dist"
  },
  {
    "path": "gradle.properties",
    "chars": 154,
    "preview": "org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\norg.gradle.paral"
  },
  {
    "path": "gradlew",
    "chars": 5080,
    "preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start "
  },
  {
    "path": "gradlew.bat",
    "chars": 2404,
    "preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
  },
  {
    "path": "sample/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "sample/build.gradle",
    "chars": 2315,
    "preview": "apply plugin: 'com.android.application'\napply plugin: 'jacoco'\n\nandroid {\n    compileSdkVersion 31\n\n    defaultConfig {\n"
  },
  {
    "path": "sample/proguard-rules.pro",
    "chars": 667,
    "preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /h"
  },
  {
    "path": "sample/src/androidTest/java/br/com/concrete/canarinho/sample/BugOnApi28Test.java",
    "chars": 809,
    "preview": "package br.com.concrete.canarinho.sample;\n\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport androidx.test.fi"
  },
  {
    "path": "sample/src/androidTest/java/br/com/concrete/canarinho/sample/DemoWatchersInstrumentationTest.java",
    "chars": 11080,
    "preview": "package br.com.concrete.canarinho.sample;\n\nimport android.os.Build;\nimport android.view.View;\nimport android.widget.Edit"
  },
  {
    "path": "sample/src/main/AndroidManifest.xml",
    "chars": 643,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    package=\"br.com.concrete.canarinho.sample\">\n\n  "
  },
  {
    "path": "sample/src/main/java/br/com/concrete/canarinho/sample/ui/activity/MainActivity.java",
    "chars": 1252,
    "preview": "package br.com.concrete.canarinho.sample.ui.activity;\n\nimport android.os.Bundle;\n\nimport com.google.android.material.tab"
  },
  {
    "path": "sample/src/main/java/br/com/concrete/canarinho/sample/ui/adapter/WatchersPagerAdapter.java",
    "chars": 825,
    "preview": "package br.com.concrete.canarinho.sample.ui.adapter;\n\nimport androidx.fragment.app.Fragment;\nimport androidx.fragment.ap"
  },
  {
    "path": "sample/src/main/java/br/com/concrete/canarinho/sample/ui/fragment/BaseWatcherFragment.java",
    "chars": 326,
    "preview": "package br.com.concrete.canarinho.sample.ui.fragment;\n\nimport androidx.fragment.app.Fragment;\nimport br.com.concrete.can"
  },
  {
    "path": "sample/src/main/java/br/com/concrete/canarinho/sample/ui/fragment/CanarinhoValorMonetarioWatcherFragment.java",
    "chars": 1883,
    "preview": "package br.com.concrete.canarinho.sample.ui.fragment;\n\nimport android.os.Bundle;\nimport android.text.TextWatcher;\nimport"
  },
  {
    "path": "sample/src/main/java/br/com/concrete/canarinho/sample/ui/fragment/WatcherFragment.java",
    "chars": 1890,
    "preview": "package br.com.concrete.canarinho.sample.ui.fragment;\n\nimport android.os.Bundle;\nimport android.text.TextWatcher;\nimport"
  },
  {
    "path": "sample/src/main/java/br/com/concrete/canarinho/sample/ui/model/Watchers.java",
    "chars": 7158,
    "preview": "package br.com.concrete.canarinho.sample.ui.model;\n\nimport android.text.TextWatcher;\n\nimport com.google.android.material"
  },
  {
    "path": "sample/src/main/res/layout/fragment_canarinho_watcher.xml",
    "chars": 1496,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas."
  },
  {
    "path": "sample/src/main/res/layout/fragment_valor_monetario_watcher.xml",
    "chars": 1496,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<com.google.android.material.card.MaterialCardView xmlns:android=\"http://schemas."
  },
  {
    "path": "sample/src/main/res/layout/main_activity.xml",
    "chars": 2169,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android=\"http://schema"
  },
  {
    "path": "sample/src/main/res/values/strings.xml",
    "chars": 80,
    "preview": "<resources>\n    <string name=\"app_name\">Android Canarinho</string>\n</resources>\n"
  },
  {
    "path": "sample/src/main/res/values/styles.xml",
    "chars": 326,
    "preview": "<resources>\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.MaterialComponents.Light.NoAct"
  },
  {
    "path": "sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorBOLETO.java",
    "chars": 3001,
    "preview": "package br.com.concrete.canarinho.test;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport androidx.test.ex"
  },
  {
    "path": "sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorCEP.java",
    "chars": 2996,
    "preview": "package br.com.concrete.canarinho.test;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport androidx.test.ex"
  },
  {
    "path": "sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorCNPJ.java",
    "chars": 3781,
    "preview": "package br.com.concrete.canarinho.test;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport androidx.test.ex"
  },
  {
    "path": "sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorCPF.java",
    "chars": 4270,
    "preview": "package br.com.concrete.canarinho.test;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport androidx.test.ex"
  },
  {
    "path": "sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorCPFCNPJ.java",
    "chars": 7222,
    "preview": "package br.com.concrete.canarinho.test;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport androidx.test.ex"
  },
  {
    "path": "sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorLinhaDigitavel.java",
    "chars": 1739,
    "preview": "package br.com.concrete.canarinho.test;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport androidx.test.ex"
  },
  {
    "path": "sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorTelefone.java",
    "chars": 2512,
    "preview": "package br.com.concrete.canarinho.test;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport androidx.test.ex"
  },
  {
    "path": "sample/src/test/java/br/com/concrete/canarinho/test/TesteFormatadorValor.java",
    "chars": 3601,
    "preview": "package br.com.concrete.canarinho.test;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport androidx.test.ex"
  },
  {
    "path": "sample/src/test/java/br/com/concrete/canarinho/test/TesteValidadores.java",
    "chars": 5425,
    "preview": "package br.com.concrete.canarinho.test;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport androidx.test.ex"
  },
  {
    "path": "sample/src/test/java/br/com/concrete/canarinho/test/watcher/BoletoTextWatcherTest.java",
    "chars": 3012,
    "preview": "package br.com.concrete.canarinho.test.watcher;\n\nimport android.widget.EditText;\n\nimport com.google.android.material.tex"
  },
  {
    "path": "sample/src/test/java/br/com/concrete/canarinho/test/watcher/ValorMonetarioWatcherTest.java",
    "chars": 2870,
    "preview": "package br.com.concrete.canarinho.test.watcher;\n\nimport android.app.Activity;\nimport android.widget.EditText;\n\nimport co"
  },
  {
    "path": "settings.gradle",
    "chars": 32,
    "preview": "include ':sample', ':canarinho'\n"
  },
  {
    "path": "tools/linters/checkstyle/checkstyle.xml",
    "chars": 12379,
    "preview": "<?xml version=\"1.0\"?><!DOCTYPE module PUBLIC\n    \"-//Puppy Crawl//DTD Check Configuration 1.3//EN\"\n    \"http://checkstyl"
  },
  {
    "path": "tools/linters/checkstyle/suppressions.xml",
    "chars": 369,
    "preview": "<?xml version=\"1.0\"?>\n<!DOCTYPE suppressions PUBLIC\n    \"-//Puppy Crawl//DTD Suppressions 1.1//EN\"\n    \"http://www.puppy"
  },
  {
    "path": "tools/linters/linters.gradle",
    "chars": 1099,
    "preview": "apply plugin: 'checkstyle'\napply plugin: 'pmd'\n\ncheck.dependsOn 'checkstyle', 'pmd'\n\ncheckstyle {\n    toolVersion '8.17'"
  },
  {
    "path": "tools/linters/pmd/pmd-ruleset.xml",
    "chars": 1261,
    "preview": "<?xml version=\"1.0\"?>\n<ruleset xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" name=\"Android Application Rules\"\n  "
  },
  {
    "path": "tools/publish.gradle",
    "chars": 638,
    "preview": "apply plugin: 'maven-publish'\n\nafterEvaluate {\n    publishing {\n        publications {\n            stable(MavenPublicati"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the concretesolutions/canarinho GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 77 files (196.4 KB), approximately 51.7k tokens, and a symbol index with 327 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!