From 275a8b2c64588240a200c61ad782d5f208b3893e Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sun, 3 Dec 2023 18:15:29 -0500 Subject: [PATCH 01/10] Bump Gradle para 8.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (e também alguns cleanups nas configs relacionados a experimental features do Java e configs antigas de Gradle) --- app/build.gradle | 3 +++ build.gradle | 2 +- gradle.properties | 7 +------ server/build.gradle | 1 - 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 1f23be05..7e156f63 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,6 +33,9 @@ android { } } } + buildFeatures { + buildConfig true + } } dependencies { diff --git a/build.gradle b/build.gradle index 44a9d956..7ba34f8f 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.2' + classpath 'com.android.tools.build:gradle:8.2.0' } } diff --git a/gradle.properties b/gradle.properties index c66f67cf..9686bf37 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,12 +11,7 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true #Sat Jun 17 18:53:44 EDT 2023 -android.defaults.buildfeatures.buildconfig=true android.nonFinalResIds=false android.nonTransitiveRClass=false android.useAndroidX=true -org.gradle.unsafe.configuration-cache=true -# server uses Virtual Threads (JEP 436); we need to enable on the compiler... -android.experimental.enableJavaPreview=true -# ...and on the VM -org.gradle.jvmargs=-Xmx2048M --enable-preview +org.gradle.unsafe.configuration-cache=true \ No newline at end of file diff --git a/server/build.gradle b/server/build.gradle index 192c4b38..e15523a8 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -23,7 +23,6 @@ test { testLogging { events "passed", "skipped", "failed" } - jvmArgs += '--enable-preview' } tasks.jar { From 130bcc2ea7d1b4c584e2913a62ad071a86215b29 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sun, 3 Dec 2023 19:09:34 -0500 Subject: [PATCH 02/10] Responde a (HTTP GET) /status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ainda está barebones; a ideia é mandar um OK em primeiro lugar (para monitores de status) e depois o # de jogadores conectados e quais as modalidades aguardando jogadores (para que o jogo possa convidar pessoas) --- .../chester/minitruco/server/JogadorConectado.java | 12 ++++++++++++ .../chester/minitruco/server/MiniTrucoServer.java | 13 +++++++++++++ 2 files changed, 25 insertions(+) diff --git a/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java b/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java index e9661565..a5a6295e 100644 --- a/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java +++ b/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java @@ -108,6 +108,18 @@ public void run() { // Como o SO_TIMEOUT está em zero, podemos assumir desconexão break; } + if (linha.startsWith("GET /status HTTP/")) { + // Essa parte tem que rodar em menos de 1s, para evitar que o keepalive + // seja enviado antes do header HTTP (vide iniciaThreadAuxiliar()) + LOGGER.info("Status do servidor solicitado"); + out.println("HTTP/1.1 200 OK"); + out.println("Content-Type: text/plain"); + out.println(); + out.println("OK"); + out.println(MiniTrucoServer.status()); + out.flush(); + return; + } if (("K " + keepAlive).equals(linha)) { keepAlive = 0; continue; diff --git a/server/src/main/java/me/chester/minitruco/server/MiniTrucoServer.java b/server/src/main/java/me/chester/minitruco/server/MiniTrucoServer.java index 4d556a8f..812bb5a1 100644 --- a/server/src/main/java/me/chester/minitruco/server/MiniTrucoServer.java +++ b/server/src/main/java/me/chester/minitruco/server/MiniTrucoServer.java @@ -168,4 +168,17 @@ private static void aguardaThreadsJogadoresFinalizarem() { } LOGGER.info("Todos os jogadores finalizaram."); } + + /** + * Gera o status do servidor (a ser retornado logo após o OK quando + * for solicitada via HTTP a URL /status). + *

+ * Quem processa a URL (e usa este método) é o JogadorConectado + */ + public static String status() { + for (Thread t : threadsJogadores) { + + } + return "online: " + (threadsJogadores.size() - 1); + } } From f583c392075040d47533227ea4be5fd26dc94af8 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sun, 3 Dec 2023 19:16:58 -0500 Subject: [PATCH 03/10] Atualizando meu servidor local para o novo IP interno fixo. --- app/src/main/res/values/opcoes.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/opcoes.xml b/app/src/main/res/values/opcoes.xml index b5b6e9c3..1900a15d 100644 --- a/app/src/main/res/values/opcoes.xml +++ b/app/src/main/res/values/opcoes.xml @@ -1,7 +1,7 @@ minitruco.chester.me - 192.168.2.10 + 192.168.2.100 Normal From 758c96a93ae93a33be321977a3c6cf2a3f352800 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sun, 3 Dec 2023 20:29:41 -0500 Subject: [PATCH 04/10] =?UTF-8?q?M=C3=A9todo=20em=20Sala=20que=20retorna?= =?UTF-8?q?=20os=20modos=20que=20t=C3=AAm=20salas=20p=C3=BAblicas=20aguard?= =?UTF-8?q?ando=20jogadores?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/me/chester/minitruco/core/Modo.java | 4 +++ .../me/chester/minitruco/server/Sala.java | 19 ++++++++++++ .../me/chester/minitruco/server/SalaTest.java | 29 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/core/src/main/java/me/chester/minitruco/core/Modo.java b/core/src/main/java/me/chester/minitruco/core/Modo.java index f8ba2249..e0cfbfaf 100644 --- a/core/src/main/java/me/chester/minitruco/core/Modo.java +++ b/core/src/main/java/me/chester/minitruco/core/Modo.java @@ -40,6 +40,10 @@ static boolean isModoValido(String modoStr) { } } + static String[] getModosValidos() { + return new String[] { "M", "P", "V", "L" }; + } + int pontuacaoParaMaoDeX(); int valorInicialDaMao(); diff --git a/server/src/main/java/me/chester/minitruco/server/Sala.java b/server/src/main/java/me/chester/minitruco/server/Sala.java index 2a8e93c5..8f06a698 100644 --- a/server/src/main/java/me/chester/minitruco/server/Sala.java +++ b/server/src/main/java/me/chester/minitruco/server/Sala.java @@ -14,6 +14,7 @@ import me.chester.minitruco.core.Jogador; import me.chester.minitruco.core.JogadorBot; +import me.chester.minitruco.core.Modo; import me.chester.minitruco.core.Partida; import me.chester.minitruco.core.PartidaLocal; @@ -138,6 +139,24 @@ public static synchronized Sala colocaEmNovaSala(JogadorConectado j, boolean pub return sala; } + /** + * Retorna os modos que estão precisando de jogadores em salas públicas, + * para que o cliente possa convidar esses jogadores quando eles abrem + * o app ou selecionam um novo modo. + * + * @return String com as letras dos modos. Ex.: "MP" significa que há + * salas aguardando jogadores para os modos mineiro e paulista. + */ + public static synchronized String modosAguardandoJogadores() { + StringBuilder sb = new StringBuilder(); + for (String modo : Modo.getModosValidos()) { + if (salasPublicasDisponiveis.stream().anyMatch(s -> s.modo.equals(modo))) { + sb.append(modo); + } + } + return sb.toString(); + } + /** * Adiciona um jogador na primeira posição disponível da sala, * garantindo os links bidirecionais e, se necessário, diff --git a/server/src/test/java/me/chester/minitruco/server/SalaTest.java b/server/src/test/java/me/chester/minitruco/server/SalaTest.java index f5f3d7b4..30d82fb7 100644 --- a/server/src/test/java/me/chester/minitruco/server/SalaTest.java +++ b/server/src/test/java/me/chester/minitruco/server/SalaTest.java @@ -19,6 +19,8 @@ import org.junit.jupiter.api.Test; import java.net.Socket; +import java.util.Arrays; +import java.util.stream.Collectors; import me.chester.minitruco.core.Jogador; import me.chester.minitruco.core.Partida; @@ -398,4 +400,31 @@ void testSalaPrivadaGeraCodigoNumericoDeCincoDigitos() { Sala s = new Sala(false, "P"); assertThat(s.codigo, matchesPattern("[0-9]{5}")); } + + String sorted(String s) { + return Arrays.stream(s.split("")).sorted().collect(Collectors.joining()); + } + + @Test + void testModosAguardandoJogadores() { + Sala.colocaEmSalaPublica(j1, "P"); + Sala.colocaEmSalaPublica(j2, "P"); + Sala.colocaEmSalaPublica(j3, "M"); + Sala.colocaEmSalaPublica(j4, "L"); + Sala.colocaEmSalaPublica(j5, "P"); + Sala.colocaEmSalaPublica(j6, "P"); + Sala.colocaEmSalaPublica(j7, "M"); + Sala.colocaEmSalaPublica(j8, "L"); + assertEquals("LM", sorted(Sala.modosAguardandoJogadores())); // P está cheia; V está vazia + + Sala.colocaEmSalaPublica(j9, "P"); + assertEquals("LMP", sorted(Sala.modosAguardandoJogadores())); // Abriu uma nova sala P para o j9 + + j9.getSala().remove(j9); + assertEquals("LM", sorted(Sala.modosAguardandoJogadores())); // A sala fechou automaticamente com 1 jogador + + Sala.colocaEmSalaPublica(j10, "L"); + Sala.colocaEmSalaPublica(jj1, "L"); + assertEquals("M", sorted(Sala.modosAguardandoJogadores())); // Completou a sala L + } } From 874920371f4c76959a861896302dcd77efa9f527 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sun, 3 Dec 2023 20:31:28 -0500 Subject: [PATCH 05/10] Formata o status do servidor como CHAVE valor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ex.: ONLINE 5 AGUARDANDO PM (5 jogadores online, salas de truco paulista e mineiro estão aguardando jogadores) --- .../java/me/chester/minitruco/server/MiniTrucoServer.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/me/chester/minitruco/server/MiniTrucoServer.java b/server/src/main/java/me/chester/minitruco/server/MiniTrucoServer.java index 812bb5a1..426a3fa3 100644 --- a/server/src/main/java/me/chester/minitruco/server/MiniTrucoServer.java +++ b/server/src/main/java/me/chester/minitruco/server/MiniTrucoServer.java @@ -174,11 +174,11 @@ private static void aguardaThreadsJogadoresFinalizarem() { * for solicitada via HTTP a URL /status). *

* Quem processa a URL (e usa este método) é o JogadorConectado + * + * @return linhas no formato "CHAVE valor", separadas por \n */ public static String status() { - for (Thread t : threadsJogadores) { - - } - return "online: " + (threadsJogadores.size() - 1); + return "ONLINE " + (threadsJogadores.size() - 1) + "\n" + + "AGUARDANDO " + Sala.modosAguardandoJogadores(); } } From 6ce6c142af56976dca193eeab58ffdd89121d847 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Sun, 3 Dec 2023 20:32:57 -0500 Subject: [PATCH 06/10] =?UTF-8?q?Garante=20que=20mesmo=20com=20um=20n?= =?UTF-8?q?=C3=BAmero=20grande=20de=20salas/jogadores=20online,=20a=20resp?= =?UTF-8?q?osta=20HTTP=20complete=20ao=20menos=20at=C3=A9=20o=20OK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dessa forma, se o "K nnnn" (keepalive) entrar antes dos outros valores de status serem calculados, ele não atrapalha a resposta. --- .../me/chester/minitruco/server/JogadorConectado.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java b/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java index a5a6295e..ad280306 100644 --- a/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java +++ b/server/src/main/java/me/chester/minitruco/server/JogadorConectado.java @@ -108,14 +108,17 @@ public void run() { // Como o SO_TIMEOUT está em zero, podemos assumir desconexão break; } - if (linha.startsWith("GET /status HTTP/")) { - // Essa parte tem que rodar em menos de 1s, para evitar que o keepalive - // seja enviado antes do header HTTP (vide iniciaThreadAuxiliar()) - LOGGER.info("Status do servidor solicitado"); + if (linha != null && linha.startsWith("GET /status HTTP/")) { + // Essa parte (que envia o OK) tem que rodar em menos de 1s, + // para evitar que o keepalive seja enviado antes do header + // HTTP (vide iniciaThreadAuxiliar()) + LOGGER.info("Status do servidor solicitado (HTTP); enviando e desconectando"); out.println("HTTP/1.1 200 OK"); out.println("Content-Type: text/plain"); out.println(); out.println("OK"); + out.flush(); + out.println(MiniTrucoServer.status()); out.flush(); return; From f386235ba8dda567ceb096a6f3be42f2d0807eea Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Tue, 5 Dec 2023 14:23:45 -0500 Subject: [PATCH 07/10] =?UTF-8?q?Centraliza=20algumas=20prefer=C3=AAncias?= =?UTF-8?q?=20em=20PreferenceUtils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A idéia é centralizar todas, mas vamos começar com essas que eu vou precisar para promover o jogo internet --- .../minitruco/android/PreferenceUtils.java | 27 +++++++++++++++++++ .../minitruco/android/TituloActivity.java | 9 +++---- .../bluetooth/ServidorBluetoothActivity.java | 6 ++--- .../internet/ClienteInternetActivity.java | 16 ++++------- 4 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/me/chester/minitruco/android/PreferenceUtils.java diff --git a/app/src/main/java/me/chester/minitruco/android/PreferenceUtils.java b/app/src/main/java/me/chester/minitruco/android/PreferenceUtils.java new file mode 100644 index 00000000..1ab65de5 --- /dev/null +++ b/app/src/main/java/me/chester/minitruco/android/PreferenceUtils.java @@ -0,0 +1,27 @@ +package me.chester.minitruco.android; + +/* SPDX-License-Identifier: BSD-3-Clause */ +/* Copyright © 2005-2023 Carlos Duarte do Nascimento "Chester" */ + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.preference.PreferenceManager; + +import me.chester.minitruco.R; + +public class PreferenceUtils { + public static String getLetraDoModo(Context context) { + return getPreferences(context).getString("modo", "P"); + } + + public static String getServidor(Context context) { + return getPreferences(context).getBoolean("servidorLocal", false) ? + context.getString(R.string.opcoes_default_servidor_local) : + context.getString(R.string.opcoes_default_servidor); + } + + private static SharedPreferences getPreferences(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context); + } +} diff --git a/app/src/main/java/me/chester/minitruco/android/TituloActivity.java b/app/src/main/java/me/chester/minitruco/android/TituloActivity.java index 189dc0e0..d03b76ba 100644 --- a/app/src/main/java/me/chester/minitruco/android/TituloActivity.java +++ b/app/src/main/java/me/chester/minitruco/android/TituloActivity.java @@ -3,6 +3,8 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static android.provider.Settings.Global.DEVICE_NAME; +import static me.chester.minitruco.android.PreferenceUtils.getLetraDoModo; + import android.app.AlertDialog; import android.bluetooth.BluetoothAdapter; import android.content.DialogInterface.OnClickListener; @@ -96,10 +98,7 @@ public void onCreate(Bundle savedInstanceState) { mostraAlertBox(this.getString(R.string.titulo_sobre), stats_versao + this.getString(R.string.texto_sobre)); }); - // TODO ver se tem um modo mais central de garantir este default (e outros) - // (provavelmente quando migrar esse PreferenceManager deprecado - // e começar a centralizar as preferencias nesta view) - selecionaModo(preferences.getString("modo", "P")); + selecionaModo(getLetraDoModo(this)); } /** @@ -356,7 +355,7 @@ private void selecionaModo(String modo) { @Override public Partida criaNovaPartida(JogadorHumano jogadorHumano) { - String modo = preferences.getString("modo", "P"); + String modo = getLetraDoModo(this); boolean humanoDecide = preferences.getBoolean("humanoDecide", true); boolean jogoAutomatico = preferences.getBoolean("jogoAutomatico", false); Partida novaPartida = new PartidaLocal(humanoDecide, jogoAutomatico, modo); diff --git a/app/src/main/java/me/chester/minitruco/android/multiplayer/bluetooth/ServidorBluetoothActivity.java b/app/src/main/java/me/chester/minitruco/android/multiplayer/bluetooth/ServidorBluetoothActivity.java index d476624a..cde94293 100644 --- a/app/src/main/java/me/chester/minitruco/android/multiplayer/bluetooth/ServidorBluetoothActivity.java +++ b/app/src/main/java/me/chester/minitruco/android/multiplayer/bluetooth/ServidorBluetoothActivity.java @@ -1,5 +1,6 @@ package me.chester.minitruco.android.multiplayer.bluetooth; +import static me.chester.minitruco.android.PreferenceUtils.getLetraDoModo; import static me.chester.minitruco.core.JogadorBot.APELIDO_BOT; import static me.chester.minitruco.core.TrucoUtils.POSICAO_PLACEHOLDER; import static me.chester.minitruco.core.TrucoUtils.montaNotificacaoI; @@ -61,10 +62,7 @@ public void onReceive(Context context, Intent intent) { @Override void iniciaAtividadeBluetooth() { - SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(this); - // TODO titulo poderia passar como extra do intent - modo = preferences.getString("modo", "P"); + modo = getLetraDoModo(this); layoutBotoesGerente.setVisibility(View.VISIBLE); btnIniciar.setOnClickListener(v -> { status = STATUS_EM_JOGO; diff --git a/app/src/main/java/me/chester/minitruco/android/multiplayer/internet/ClienteInternetActivity.java b/app/src/main/java/me/chester/minitruco/android/multiplayer/internet/ClienteInternetActivity.java index d21c8ed4..02773ac2 100644 --- a/app/src/main/java/me/chester/minitruco/android/multiplayer/internet/ClienteInternetActivity.java +++ b/app/src/main/java/me/chester/minitruco/android/multiplayer/internet/ClienteInternetActivity.java @@ -2,6 +2,9 @@ import static android.text.InputType.TYPE_CLASS_NUMBER; +import static me.chester.minitruco.android.PreferenceUtils.getLetraDoModo; +import static me.chester.minitruco.android.PreferenceUtils.getServidor; + import android.app.AlertDialog; import android.content.SharedPreferences; import android.os.Bundle; @@ -12,7 +15,6 @@ import android.widget.EditText; import android.widget.Toast; -import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import java.io.BufferedReader; @@ -27,7 +29,6 @@ import java.util.logging.Logger; import me.chester.minitruco.BuildConfig; -import me.chester.minitruco.R; import me.chester.minitruco.android.CriadorDePartida; import me.chester.minitruco.android.JogadorHumano; import me.chester.minitruco.android.SalaActivity; @@ -187,9 +188,7 @@ public void onBackPressed() { } private boolean conecta() { - String servidor = preferences.getBoolean("servidorLocal", false) ? - this.getString(R.string.opcoes_default_servidor_local) : - this.getString(R.string.opcoes_default_servidor); + String servidor = getServidor(this); try { socket = new Socket(); socket.connect(new InetSocketAddress(servidor, 6912), 10_000); @@ -233,7 +232,7 @@ private void processaNotificacoes() { case 'N': // Nome foi aceito // Já vamos entrar de cara numa sala pública (se a pessoa quiser // fazer outra coisa, ela usa o botão apropriado) - enviaLinha("E PUB " + getModoDasPreferencias()); + enviaLinha("E PUB " + getLetraDoModo(this)); break; case 'I': // Entrou/voltou para uma sala (ou ela foi atualizada) exibeMesaForaDoJogo(line); @@ -287,11 +286,6 @@ private void erroFatalSalaInvalida() { "te convidou e tente novamente."); } - @NonNull - private String getModoDasPreferencias() { - return preferences.getString("modo", "P"); - } - /** * Se estivermos numa sala pública e a mesa estiver cheia, inicia contagem * regressiva para auto-início do jogo. From 007acbcf7ae4ad93b4bfdec6db2324c04585dc4f Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Tue, 5 Dec 2023 16:14:47 -0500 Subject: [PATCH 08/10] =?UTF-8?q?M=C3=A9todo=20que=20verifica=20se=20devem?= =?UTF-8?q?os=20convidar=20a=20pessoa=20para=20jogar=20online?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ele leva em conta se tem gente aguardando na sala e se já teve uma resposta positiva (para não incomodar), mas permite desativar a última checagem (ex.: caso a pessoa troque o modo) --- .../multiplayer/internet/InternetUtils.java | 68 ++++++++++++ .../internet/InternetUtilsTest.java | 104 ++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 app/src/main/java/me/chester/minitruco/android/multiplayer/internet/InternetUtils.java create mode 100644 app/src/test/java/me/chester/minitruco/android/multiplayer/internet/InternetUtilsTest.java diff --git a/app/src/main/java/me/chester/minitruco/android/multiplayer/internet/InternetUtils.java b/app/src/main/java/me/chester/minitruco/android/multiplayer/internet/InternetUtils.java new file mode 100644 index 00000000..5ceba68d --- /dev/null +++ b/app/src/main/java/me/chester/minitruco/android/multiplayer/internet/InternetUtils.java @@ -0,0 +1,68 @@ +package me.chester.minitruco.android.multiplayer.internet; + +/* SPDX-License-Identifier: BSD-3-Clause */ +/* Copyright © 2005-2023 Carlos Duarte do Nascimento "Chester" */ + +import static me.chester.minitruco.android.PreferenceUtils.getLetraDoModo; +import static me.chester.minitruco.android.PreferenceUtils.getServidor; + +import android.content.Context; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; + +/** + * Métodos utilitários relacionados ao jogo online. + */ +public class InternetUtils { + + static boolean mostrouConvite = false; + + /** + * Diz se devemos mostrar um toast convidando a pessoa para jogar na + * internet, verificando se existe sala no servidor no modo de jogo + * selecionado que esteja esperando pessoas. + *

+ * O objetivo é reduzir a espera no servidor, mas sem irritar quem + * não quer jogar online, então a resposta positiva só é retornada + * uma vez por execução do aplicativo, a não ser que repete seja true. + * + * + * @param context Context usado para ler preferências e mostrar toast + * @param repete Se true, esquece toasts anteriores (útil para quando + * a pessoa seleciona um modo de jogo diferente) + */ + public static boolean isPromoveJogoInternet(Context context, boolean repete) { + if (repete) { + mostrouConvite = false; + } + if (mostrouConvite) { + return false; + } + + HttpURLConnection urlConnection = null; + try { + URL url = new URL("http://" + getServidor(context) + ":6912/status"); + urlConnection = (HttpURLConnection) url.openConnection(); + BufferedReader reader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("AGUARDANDO ") && line.substring(11).contains(getLetraDoModo(context))) { + mostrouConvite = true; + return true; + } + } + } catch (IOException e) { + return false; + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + } + return false; + } +} diff --git a/app/src/test/java/me/chester/minitruco/android/multiplayer/internet/InternetUtilsTest.java b/app/src/test/java/me/chester/minitruco/android/multiplayer/internet/InternetUtilsTest.java new file mode 100644 index 00000000..94c12482 --- /dev/null +++ b/app/src/test/java/me/chester/minitruco/android/multiplayer/internet/InternetUtilsTest.java @@ -0,0 +1,104 @@ +package me.chester.minitruco.android.multiplayer.internet; + +/* SPDX-License-Identifier: BSD-3-Clause */ +/* Copyright © 2005-2023 Carlos Duarte do Nascimento "Chester" */ + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; + +import java.io.ByteArrayInputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; + +import me.chester.minitruco.android.PreferenceUtils; + +class InternetUtilsTest { + + private MockedStatic mockPreferenceUtils; + private MockedConstruction urlMockedConstruction; + + @AfterEach + public void afterEach() { + if (mockPreferenceUtils != null) { + mockPreferenceUtils.close(); + } + if (urlMockedConstruction != null) { + urlMockedConstruction.close(); + } + } + + void mockModo(String value, String... values) { + if (mockPreferenceUtils != null) { + mockPreferenceUtils.close(); + } + mockPreferenceUtils = mockStatic(PreferenceUtils.class); + mockPreferenceUtils.when(() -> PreferenceUtils.getLetraDoModo(any())).thenReturn(value, values); + } + + void mockAguardando(String modo) { + if (urlMockedConstruction != null) { + urlMockedConstruction.close(); + } + urlMockedConstruction = mockConstruction(URL.class, (theMock, context) -> { + HttpURLConnection mockHttpURLConnection = mock(HttpURLConnection.class); + when(theMock.openConnection()).thenReturn((URLConnection) mockHttpURLConnection); + when(mockHttpURLConnection.getInputStream()).thenReturn(new ByteArrayInputStream(( + "OK\n" + + "ONLINE 123\n" + + (modo != null ? "AGUARDANDO " + modo + "\n" : "") + + "K 12342342342\n" + ).getBytes())); + }); + } + + @Test + void testPromoveJogoInternetSoETrueSeTemSalaAguardandoNoModoAtual() { + mockAguardando("M"); + mockModo("M", "P", "L"); + assertTrue(InternetUtils.isPromoveJogoInternet(null, true)); + assertFalse(InternetUtils.isPromoveJogoInternet(null, true)); + assertFalse(InternetUtils.isPromoveJogoInternet(null, true)); + + mockAguardando("ML"); + mockModo("M", "P", "L"); + assertTrue(InternetUtils.isPromoveJogoInternet(null, true)); + assertFalse(InternetUtils.isPromoveJogoInternet(null, true)); + assertTrue(InternetUtils.isPromoveJogoInternet(null, true)); + + mockAguardando(""); + mockModo("M", "P", "L"); + assertFalse(InternetUtils.isPromoveJogoInternet(null, true)); + assertFalse(InternetUtils.isPromoveJogoInternet(null, true)); + assertFalse(InternetUtils.isPromoveJogoInternet(null, true)); + } + + @Test + void testPromoveJogoInternetIgnoraSeNaoTiverInfoDeAguardandoNoStatus() { + mockAguardando(null); + mockModo("M", "P", "L"); + assertFalse(InternetUtils.isPromoveJogoInternet(null, true)); + assertFalse(InternetUtils.isPromoveJogoInternet(null, true)); + assertFalse(InternetUtils.isPromoveJogoInternet(null, true)); + } + + @Test + void testPromoveJogoInternetNaoPromoveDuasVezesSeNaoPedirRepeticao() { + mockAguardando("M"); + mockModo("M", "M", "M"); + assertTrue(InternetUtils.isPromoveJogoInternet(null, false)); + assertFalse(InternetUtils.isPromoveJogoInternet(null, false)); + assertFalse(InternetUtils.isPromoveJogoInternet(null, false)); + } +} From e444ea52bff05e7ad37dacedf238c7308e705d84 Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Tue, 5 Dec 2023 18:08:21 -0500 Subject: [PATCH 09/10] =?UTF-8?q?Convida=20para=20jogar=20online=20quando?= =?UTF-8?q?=20abre=20o=20aplicativo=20ou=20quando=20um=20modo=20=C3=A9=20s?= =?UTF-8?q?elecionado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + .../minitruco/android/TituloActivity.java | 18 +++++++++++++++++- .../main/res/xml/network_security_config.xml | 7 +++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/xml/network_security_config.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c3fe258f..f8f53bcb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,7 @@ { selecionaModo("L"); + promoveJogoInternet(true); }) .setPositiveButton("Manilha Velha", (dialog, which) -> { selecionaModo("V"); + promoveJogoInternet(true); }) .show(); } else { selecionaModo((String) view.getTag()); + promoveJogoInternet(true); } } @@ -353,6 +359,16 @@ private void selecionaModo(String modo) { preferences.edit().putString("modo", modo).apply(); } + private void promoveJogoInternet(boolean repete) { + new Thread(() -> { + if (InternetUtils.isPromoveJogoInternet(this, repete)) { + runOnUiThread(() -> { + Toast.makeText(this, "Jogue pela internet agora! Pessoas estão aguardando por você.", Toast.LENGTH_LONG).show(); + }); + } + }).start(); + } + @Override public Partida criaNovaPartida(JogadorHumano jogadorHumano) { String modo = getLetraDoModo(this); diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..5fbea713 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,7 @@ + + + + minitruco.chester.me + 192.168.2.100 + + From 188b265e1b8c3383f76e0d29e2ce12f3d5e7058b Mon Sep 17 00:00:00 2001 From: Carlos Duarte Do Nascimento Date: Tue, 5 Dec 2023 18:29:22 -0500 Subject: [PATCH 10/10] =?UTF-8?q?Convida=20para=20jogar=20online=20ao=20fi?= =?UTF-8?q?nal=20de=20uma=20partida=20single-player=20(mas=20s=C3=B3=20se?= =?UTF-8?q?=20n=C3=A3o=20tiver=20convidado=20antes)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chester/minitruco/android/TrucoActivity.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/app/src/main/java/me/chester/minitruco/android/TrucoActivity.java b/app/src/main/java/me/chester/minitruco/android/TrucoActivity.java index 2485eaef..d001bbc5 100644 --- a/app/src/main/java/me/chester/minitruco/android/TrucoActivity.java +++ b/app/src/main/java/me/chester/minitruco/android/TrucoActivity.java @@ -25,11 +25,13 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; import me.chester.minitruco.R; +import me.chester.minitruco.android.multiplayer.internet.InternetUtils; import me.chester.minitruco.core.Partida; /* SPDX-License-Identifier: BSD-3-Clause */ @@ -367,6 +369,9 @@ public void jogoFechado(int numEquipeVencedora) { // servidor bluetooth (em ambos os casos, estará na posição 1). if (jogadorHumano.getPosicao() == 1) { btnNovaPartida.setVisibility(View.VISIBLE); + if (partida.semJogadoresRemotos()) { + promoveJogoInternet(); + } if (partida.isJogoAutomatico()) { btnNovaPartida.performClick(); } @@ -374,6 +379,16 @@ public void jogoFechado(int numEquipeVencedora) { }); } + private void promoveJogoInternet() { + new Thread(() -> { + if (InternetUtils.isPromoveJogoInternet(this, false)) { + runOnUiThread(() -> { + Toast.makeText(this, "Tem pessoas de verdade aguardando para jogar! Use o botão Internet na tela inicial.", Toast.LENGTH_LONG).show(); + }); + } + }).start(); + } + /** * Determina se o placar de partidas deve ser salvo/recuperado *