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 ``` ## 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 ================================================ ================================================ 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. *

* Para exemplificar, o dígito do trecho 0000039104766 para os multiplicadores indo de * 2 a 7 e usando módulo 11 é a seguinte: *

*
 *  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
 * 
*

* 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. *

*
 *      soma = 112
 *      soma % 11 = 2
 *      11 - (soma % 11) = 9
 * 
*

* 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 *

*/ public final class DigitoPara { private final List numero = new LinkedList<>(); private final List multiplicadores; private final boolean complementar; private final int modulo; private final boolean somarIndividual; private final SparseArray 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 multiplicadores = new ArrayList<>(); private boolean complementar; private int modulo; private boolean somarIndividual; private final SparseArray 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; } /** *

* Indica se, ao calcular o módulo, a soma dos resultados da multiplicação deve ser * considerado digito a dígito. *

* 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: *
    *
  • multiplicadores: 2 a 9
  • *
  • módulo: 11
  • *
* * @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: *
    *
  • {@link #estaFormatado(String)}: indicará se está em formata de boleto
  • *
  • {@link #podeSerFormatado(String)}: indicará se é uma linha digitável
  • *
*/ 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: *
    *
  • Com símbolo do Real {@link Formatador#VALOR_COM_SIMBOLO}
  • *
  • Sem símbolo do Real {@link Formatador#VALOR}
  • *
*/ 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: *
    *
  • Uma {@link String} completa
  • *
  • Um {@link Editable} e um {@link br.com.concrete.canarinho.validator.Validador.ResultadoParcial}
  • *
* 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 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 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 getConstraints() { // noinspection unchecked Matcher 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 ================================================ ================================================ 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 ================================================ ================================================ FILE: sample/src/main/res/layout/fragment_valor_monetario_watcher.xml ================================================ ================================================ FILE: sample/src/main/res/layout/main_activity.xml ================================================ ================================================ FILE: sample/src/main/res/values/strings.xml ================================================ Android Canarinho ================================================ FILE: sample/src/main/res/values/styles.xml ================================================ ================================================ 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 scenario = ActivityScenario.launch(MainActivity.class); scenario.onActivity(new ActivityScenario.ActivityAction() { @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 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 ================================================ ================================================ FILE: tools/linters/checkstyle/suppressions.xml ================================================ ================================================ FILE: tools/linters/linters.gradle ================================================ apply plugin: 'checkstyle' apply plugin: 'pmd' check.dependsOn 'checkstyle', 'pmd' checkstyle { toolVersion '8.17' configFile file("$rootDir/tools/linters/checkstyle/checkstyle.xml") configProperties.checkstyleSuppressionsPath = file( "$rootDir/tools/linters/checkstyle/suppressions.xml" ).absolutePath } pmd { toolVersion = '6.11.0' ignoreFailures = false ruleSetFiles = files("$rootDir/tools/linters/pmd/pmd-ruleset.xml") ruleSets = [] } task checkstyle(type: Checkstyle, group: 'verification') { source 'src' include '**/*.java' exclude '**/gen/**' exclude '**/test/**' exclude '**/androidTest/**' exclude '**/R.java' exclude '**/BuildConfig.java' classpath = files() } task pmd(type: Pmd, group: 'verification') { source 'src' include '**/*.java' exclude('**/gen/**', '**/debug/**') reports { xml.enabled = true html.enabled = true xml.destination file("$projectDir/build/reports/pmd/pmd.xml") html.destination file("$projectDir/build/reports/pmd/pmd.html") } } ================================================ FILE: tools/linters/pmd/pmd-ruleset.xml ================================================ Custom ruleset for Android application .*/R.java .*/gen/.* ================================================ FILE: tools/publish.gradle ================================================ apply plugin: 'maven-publish' afterEvaluate { publishing { publications { stable(MavenPublication) { groupId 'br.com.concrete' artifactId 'canarinho' version '2.0.3' from components.release } } repositories { maven { url = String.valueOf(System.getenv("GPR_URL")) credentials { username = String.valueOf(System.getenv("GPR_USER")) password = String.valueOf(System.getenv("GPR_PASSWORD")) } } } } }