Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/somente-android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: SOMENTE Android

on:
push:
paths: ["SomenteAndroid/**", ".github/workflows/somente-android.yml"]
pull_request:
paths: ["SomenteAndroid/**", ".github/workflows/somente-android.yml"]

permissions:
contents: read

concurrency:
group: somente-android-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: SomenteAndroid
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: "8.14.3"
- name: Test, lint and assemble debug APK
run: gradle --no-daemon testDebugUnitTest lintDebug assembleDebug --stacktrace
26 changes: 26 additions & 0 deletions SomenteAndroid/PLAY_STORE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Preparação para Google Play

## Nome

SOMENTE — ouvir, ler e criar música

## Descrição curta

Treino auditivo, pauta sincronizada e criação de exercícios musicais.

## Descrição completa

SOMENTE une som e mente em três experiências simples para iniciantes de qualquer idade. Ouça uma nota e tente reconhecê-la; acompanhe pequenas sequências na pauta enquanto elas tocam; ou crie um exercício de até 16 notas para estudar e usar em aula.

Não é necessário saber ler partitura nem possuir instrumento. O aplicativo funciona offline, sem conta e sem anúncios. O progresso e o exercício criado ficam somente no aparelho.

## Checklist antes de enviar

- [ ] confirmar disponibilidade do nome e do pacote;
- [ ] gerar chave de upload e AAB assinado;
- [ ] identificar o responsável e publicar a política de privacidade;
- [ ] declarar corretamente público-alvo e segurança de dados;
- [ ] criar ícone 512 × 512, feature graphic 1024 × 500 e capturas;
- [ ] testar em telefone e tablet, com TalkBack e tamanhos de fonte maiores;
- [ ] concluir classificação indicativa e teste fechado;
- [ ] corrigir o relatório de pré-lançamento.
9 changes: 9 additions & 0 deletions SomenteAndroid/PRIVACY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Política de Privacidade — SOMENTE v0.1

Última atualização: 26 de agosto de 2026.

O SOMENTE v0.1 funciona sem conta e sem conexão com a internet. Não contém publicidade nem ferramentas de análise e não solicita acesso ao microfone, câmera, contatos, localização, identificador de anúncios ou arquivos pessoais.

O aplicativo armazena no próprio aparelho apenas a contagem de respostas corretas e uma sequência musical criada pelo usuário. Esses dados não são enviados ao desenvolvedor e podem ser apagados ao limpar os dados do aplicativo ou desinstalá-lo.

Antes de publicação pública, esta política deverá incluir a identificação e o contato do responsável pelo aplicativo e corresponder exatamente ao pacote distribuído.
36 changes: 36 additions & 0 deletions SomenteAndroid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SOMENTE Android

**SOMENTE** (som + mente) é um laboratório pedagógico gratuito para iniciantes de qualquer idade. A experiência reúne escuta, leitura e criação sem exigir conta, internet, anúncios, microfone ou conhecimento prévio de partitura.

## Três modos de aula

1. **Escutar e reconhecer** — o estudante ouve uma nota, escolhe seu nome e vê a resposta na pauta.
2. **Ler e ouvir** — uma sequência curta é reproduzida enquanto a nota atual é destacada.
3. **Criar exercício** — professor ou estudante monta até 16 notas, ouve o resultado e salva a atividade localmente.

O desenho se inspira na clareza de aplicativos de partitura para Android, mas segue uma progressão própria: ouvir → reconhecer → visualizar → criar. Os exemplos são originais e não incluem repertório protegido.

## Executar

Abra a pasta `SomenteAndroid` no Android Studio, aguarde a sincronização do Gradle e execute o módulo `app` em Android 8.0 ou superior. Na linha de comando, com Gradle 8.14.3 instalado:

```bash
gradle testDebugUnitTest lintDebug assembleDebug
```

O pacote é `br.com.sidineyr.somente`, com alvo Android 16/API 36 e versão `0.1.0`.

## Uso responsável

- use volume confortável, especialmente com fones;
- o nome das notas segue a nomenclatura em português;
- o exercício salvo permanece apenas neste aparelho;
- valide contraste, tamanhos de toque e TalkBack em aparelho real antes de publicar.

## Limites da v0.1

O MVP usa ondas senoidais e valores rítmicos iguais. Compassos, figuras rítmicas, claves adicionais, transposição e exportação ficam para versões posteriores. Antes da Google Play ainda são necessários ícone, capturas, AAB assinado e teste fechado.

## Próximo passo de revisão

Use [`RELEASE_CHECKLIST.md`](RELEASE_CHECKLIST.md) para validar primeiro o PR em CI, telefone, tablet, TalkBack e fonte ampliada. Os itens de Play Console e identidade visual estão separados porque não bloqueiam a revisão do código e não devem ser tratados como autorização para publicar.
33 changes: 33 additions & 0 deletions SomenteAndroid/RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Checklist de revisão — SOMENTE Android v0.1

Este checklist separa o que bloqueia a revisão do código do que será necessário apenas antes de uma futura publicação. Marque um item somente quando houver evidência reproduzível.

## 1. Pronto para solicitar revisão do PR

- [ ] O workflow **SOMENTE Android** conclui em verde no commit mais recente.
- [ ] `testDebugUnitTest`, `lintDebug` e `assembleDebug` passam no GitHub Actions.
- [ ] O APK de depuração instala e abre em um telefone Android 8.0 ou superior.
- [ ] Os três fluxos funcionam no telefone: reconhecer, ler/acompanhar e criar/salvar exercício.
- [ ] O mesmo roteiro é concluído em um tablet, sem conteúdo cortado ou controles inacessíveis.
- [ ] Com TalkBack, todos os controles têm nome compreensível e a ordem de foco permite concluir os três fluxos.
- [ ] Com o maior tamanho de fonte do sistema, o texto permanece legível e os botões continuam utilizáveis.
- [ ] Resultados, aparelho/versão do Android e problemas encontrados estão registrados no PR.

Quando todos os itens desta seção estiverem concluídos, o PR pode sair de rascunho e receber revisão. Isso ainda não autoriza merge nem publicação.

## 2. Somente antes de um teste fechado futuro

- [ ] Confirmar o nome **SOMENTE** e o pacote `br.com.sidineyr.somente`.
- [ ] Definir responsável e contato público na política de privacidade.
- [ ] Criar o ícone 512 × 512, a imagem de destaque 1024 × 500 e capturas de telefone/tablet.
- [ ] Criar e guardar com segurança a chave de upload.
- [ ] Gerar o AAB assinado e testá-lo localmente.
- [ ] Preencher público-alvo, segurança de dados, classificação indicativa e demais campos do Play Console.
- [ ] Executar um teste fechado e corrigir o relatório de pré-lançamento.

## Registro mínimo dos testes manuais

| Ambiente | Versão Android | Tela/fonte | TalkBack | Três fluxos | Resultado/observações |
|---|---|---|---|---|---|
| Telefone | | Padrão e máxima | Ligado/desligado | | |
| Tablet | | Padrão e máxima | Ligado/desligado | | |
37 changes: 37 additions & 0 deletions SomenteAndroid/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}

android {
namespace = "br.com.sidineyr.somente"
compileSdk = 36

defaultConfig {
applicationId = "br.com.sidineyr.somente"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "0.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions { jvmTarget = "17" }
}

dependencies {
implementation("androidx.core:core-ktx:1.16.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("com.google.android.material:material:1.12.0")
testImplementation("junit:junit:4.13.2")
}
1 change: 1 addition & 0 deletions SomenteAndroid/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Rules are intentionally minimal; Google Mobile Ads publishes consumer rules.
18 changes: 18 additions & 0 deletions SomenteAndroid/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:label="SOMENTE"
android:supportsRtl="true"
android:theme="@style/Theme.Somente">
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package br.com.sidineyr.somente

import android.graphics.Color
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlin.random.Random

class MainActivity : AppCompatActivity() {
private val player = TonePlayer()
private lateinit var root: LinearLayout
private val prefs by lazy { getSharedPreferences("somente", MODE_PRIVATE) }
private var answerButtons = mutableListOf<Button>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER_HORIZONTAL
setPadding(32, 44, 32, 44)
setBackgroundColor(Color.rgb(247, 246, 252))
}
setContentView(ScrollView(this).apply { addView(root) })
showHome()
}

@Suppress("DEPRECATION")
override fun onBackPressed() {
if (root.tag == "home") super.onBackPressed() else showHome()
}

private fun showHome() {
page("home")
title("SOMENTE")
subtitle("Ouvir • ler • criar")
paragraph("Um laboratório musical simples para aprender no próprio ritmo ou usar com um professor.")
modeButton("👂 Escutar e reconhecer", "Treino auditivo com notas na pauta") { showEarTraining() }
modeButton("♫ Ler e ouvir", "Acompanhe cada nota enquanto ela toca") { showReader() }
modeButton("✎ Criar exercício", "Monte e salve uma sequência para sua aula") { showComposer() }
paragraph("Funciona sem conta, anúncios ou internet. Use fones apenas em volume confortável.", muted = true)
}

private fun showEarTraining(message: String? = null) {
page("ear")
backButton()
title("Escutar e reconhecer")
paragraph("Ouça primeiro. Depois escolha a nota. A pauta revela a resposta.")
val target = Music.notes[Random.nextInt(7)]
val staff = StaffView(this).apply { notes = emptyList() }
root.addView(staff)
val choices = Music.notes.take(7).shuffled().take(3).toMutableList()
if (target !in choices) choices[0] = target
answerButtons.clear()
button("▶ Ouvir nota") {
setAnswersEnabled(false)
player.play(listOf(target), onDone = { runOnUiThread { setAnswersEnabled(true) } })
}
choices.shuffled().forEach { candidate ->
answerButtons += button(candidate.name, enabled = false) {
staff.notes = listOf(target)
setAnswersEnabled(false)
if (candidate == target) {
prefs.edit().putInt("correct", prefs.getInt("correct", 0) + 1).apply()
showEarTraining("Muito bem: ${target.name}. Ouça a próxima.")
} else {
setAnswersEnabled(true)
toastText("Era ${target.name}. Compare e tente uma nova nota.")
}
}
}
message?.let(::success)
paragraph("Acertos nesta instalação: ${prefs.getInt("correct", 0)}", muted = true)
}

private fun showReader() {
page("reader")
backButton()
title("Ler e ouvir")
paragraph("A nota dourada mostra onde a reprodução está. Toque de novo quantas vezes precisar.")
val custom = Music.decode(prefs.getString("teacher_sequence", "") ?: "")
val sequences = Music.examples + if (custom.isNotEmpty()) listOf(MusicSequence("Exercício do professor", custom)) else emptyList()
var selected = sequences.first()
val staff = StaffView(this).apply { notes = selected.notes }
root.addView(staff)
val heading = subtitle(selected.title)
sequences.forEach { sequence ->
button(sequence.title) {
if (!player.isPlaying()) { selected = sequence; heading.text = sequence.title; staff.notes = sequence.notes }
}
}
button("▶ Reproduzir com acompanhamento") {
if (player.isPlaying()) return@button
player.play(selected.notes,
onNote = { runOnUiThread { staff.activeIndex = it } },
onDone = { runOnUiThread { staff.activeIndex = -1 } })
}
}

private fun showComposer(message: String? = null) {
page("composer")
backButton()
title("Criar exercício")
paragraph("Professor ou estudante: toque nas notas para montar uma frase de até 16 sons.")
val sequence = Music.decode(prefs.getString("teacher_sequence", "") ?: "").toMutableList()
val staff = StaffView(this).apply { notes = sequence.toList() }
root.addView(staff)
val summary = subtitle(sequenceSummary(sequence))
Music.notes.forEach { note ->
button("+ ${note.name}") {
if (sequence.size < 16) { sequence += note; staff.notes = sequence.toList(); summary.text = sequenceSummary(sequence) }
else toastText("O limite é 16 notas para manter o exercício legível.")
}
}
button("▶ Ouvir minha sequência") {
if (sequence.isEmpty()) toastText("Adicione pelo menos uma nota.") else player.play(sequence)
}
button("↶ Desfazer última nota") {
if (sequence.isNotEmpty()) { sequence.removeAt(sequence.lastIndex); staff.notes = sequence.toList(); summary.text = sequenceSummary(sequence) }
}
button("Salvar para usar em aula") {
if (sequence.isEmpty()) toastText("Crie uma sequência antes de salvar.")
else { prefs.edit().putString("teacher_sequence", Music.encode(sequence)).apply(); showComposer("Exercício salvo no aparelho e disponível em “Ler e ouvir”.") }
}
message?.let(::success)
}

private fun sequenceSummary(notes: List<MusicNote>) = if (notes.isEmpty()) "Sua sequência está vazia" else notes.joinToString(" ") { it.name }
private fun setAnswersEnabled(enabled: Boolean) { answerButtons.forEach { it.isEnabled = enabled } }
private fun page(tag: String) { root.removeAllViews(); root.tag = tag; answerButtons.clear() }
private fun backButton() { button("‹ Início") { showHome() } }
private fun title(value: String) = text(value, 30, Color.rgb(43, 37, 74)).also { root.addView(it) }
private fun subtitle(value: String) = text(value, 19, Color.rgb(82, 70, 160)).also { root.addView(it) }
private fun paragraph(value: String, muted: Boolean = false) = text(value, 16, if (muted) Color.GRAY else Color.DKGRAY).also { it.setPadding(0, 18, 0, 18); root.addView(it) }
private fun success(value: String) = text(value, 17, Color.rgb(24, 120, 78)).also { it.setPadding(0, 18, 0, 8); root.addView(it) }
private fun toastText(value: String) { android.widget.Toast.makeText(this, value, android.widget.Toast.LENGTH_LONG).show() }
private fun modeButton(label: String, detail: String, action: () -> Unit) { button(label, action = action); paragraph(detail, muted = true) }

private fun button(label: String, enabled: Boolean = true, action: () -> Unit): Button = Button(this).apply {
text = label; isAllCaps = false; textSize = 17f; isEnabled = enabled
contentDescription = label
setOnClickListener { action() }
layoutParams = LinearLayout.LayoutParams(-1, -2).also { it.setMargins(0, 8, 0, 8) }
root.addView(this)
}

private fun text(value: String, size: Int, color: Int) = TextView(this).apply {
text = value; textSize = size.toFloat(); setTextColor(color); gravity = Gravity.CENTER
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
}
}
26 changes: 26 additions & 0 deletions SomenteAndroid/app/src/main/java/br/com/sidineyr/somente/Music.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package br.com.sidineyr.somente

data class MusicNote(val name: String, val frequency: Double, val staffStep: Int)

data class MusicSequence(val title: String, val notes: List<MusicNote>)

object Music {
val notes = listOf(
MusicNote("Dó", 261.63, 0), MusicNote("Ré", 293.66, 1),
MusicNote("Mi", 329.63, 2), MusicNote("Fá", 349.23, 3),
MusicNote("Sol", 392.00, 4), MusicNote("Lá", 440.00, 5),
MusicNote("Si", 493.88, 6), MusicNote("Dó↑", 523.25, 7)
)

val examples = listOf(
MusicSequence("Escada sonora", notes.take(8)),
MusicSequence("Três notas", listOf(notes[0], notes[2], notes[4], notes[2], notes[0])),
MusicSequence("Pergunta e resposta", listOf(notes[0], notes[1], notes[2], notes[4], notes[2], notes[1], notes[0]))
)

fun encode(sequence: List<MusicNote>) = sequence.joinToString(",") { notes.indexOf(it).toString() }

fun decode(value: String): List<MusicNote> = value.split(',').mapNotNull {
it.toIntOrNull()?.let { index -> notes.getOrNull(index) }
}
}
Loading
Loading