Feat: Implémentation de base des notifications

- Ajout permission POST_NOTIFICATIONS dans AndroidManifest.xml (Android 13+).
- Création d'un canal de notification ('BEST_2048_CHANNEL') dans MainActivity.onCreate.
- Ajout d'une icône de notification simple (ic_stat_notification_2048.xml).
- Ajout de strings pour les notifications et la gestion des permissions.
- Modification de MainActivity :
  - Implémentation de la demande de permission POST_NOTIFICATIONS via ActivityResultLauncher,
    déclenchée par l'activation du switch dans les paramètres.
  - Ajout méthode utilitaire 'showNotification' utilisant NotificationCompat.Builder.
  - Ajout méthodes 'showAchievementNotification', 'showHighScoreNotification' (test), 'showInactivityNotification' (test).
  - Déclenchement de 'showAchievementNotification' dans handleSwipe lors de la première victoire.
  - Activation du switch 'Notifications' dans le dialogue des paramètres et gestion de son état
    via SharedPreferences et demande de permission.
  - Ajout (commenté/optionnel) boutons de test pour notifications HighScore/Inactivité.
- NOTE: Notifications périodiques (HighScore, Inactivité) non planifiées, déclenchées
  manuellement pour test dans ce commit. Nécessite WorkManager/AlarmManager pour implémentation réelle.
This commit is contained in:
Augustin ROUX 2025-04-04 15:20:14 +02:00
parent a61f110992
commit 7dc7360e14
4 changed files with 241 additions and 50 deletions

View File

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH" />

View File

@ -9,7 +9,13 @@ package legion.muyue.best2048;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Context;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.pm.PackageManager;
import android.os.Build;
import android.provider.Settings;
import com.google.android.material.switchmaterial.SwitchMaterial;
import android.content.Intent;
@ -23,9 +29,13 @@ import android.view.View;
import android.view.ViewStub;
import android.view.animation.AnimationUtils;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.gridlayout.widget.GridLayout;
import android.widget.Button;
@ -49,6 +59,8 @@ public class MainActivity extends AppCompatActivity {
private Game game;
private GameStats gameStats;
private static final int BOARD_SIZE = 4;
private static final String NOTIFICATION_CHANNEL_ID = "BEST_2048_CHANNEL";
private boolean notificationsEnabled = true;
// --- State Management ---
private boolean statisticsVisible = false;
@ -63,12 +75,32 @@ public class MainActivity extends AppCompatActivity {
// --- Activity Lifecycle ---
private final ActivityResultLauncher<String> requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
// La permission est accordée. On peut activer/planifier les notifications.
notificationsEnabled = true;
saveNotificationPreference(true);
Toast.makeText(this, R.string.notifications_enabled, Toast.LENGTH_SHORT).show();
// Ici, on pourrait (re)planifier les notifications périodiques avec WorkManager/AlarmManager
} else {
// La permission est refusée. L'utilisateur ne recevra pas de notifications.
notificationsEnabled = false;
saveNotificationPreference(false);
// Désactive le switch dans les paramètres si l'utilisateur vient de refuser
updateNotificationSwitchState(false);
Toast.makeText(this, R.string.notifications_disabled, Toast.LENGTH_SHORT).show();
// Afficher une explication si nécessaire
// showNotificationPermissionRationale();
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
EdgeToEdge.enable(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
createNotificationChannel();
findViews();
initializeGameAndStats();
setupListeners();
@ -132,6 +164,7 @@ public class MainActivity extends AppCompatActivity {
private void initializeGameAndStats() {
preferences = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
gameStats = new GameStats(this);
loadNotificationPreference();
loadGame(); // Charge jeu et met à jour high score
updateUI();
if (game == null) {
@ -141,6 +174,22 @@ public class MainActivity extends AppCompatActivity {
// L'état (currentGameState) est défini dans loadGame ou startNewGame
}
/** Crée le canal de notification nécessaire pour Android 8.0+. */
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.notification_channel_name);
String description = getString(R.string.notification_channel_description);
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance);
channel.setDescription(description);
// Enregistre le canal avec le système; ne peut pas être changé après ça
NotificationManager notificationManager = getSystemService(NotificationManager.class);
if (notificationManager != null) {
notificationManager.createNotificationChannel(channel);
}
}
}
/**
* Configure les listeners pour les boutons et le plateau de jeu (swipes).
* Mise à jour pour le bouton Menu.
@ -299,6 +348,7 @@ public class MainActivity extends AppCompatActivity {
currentGameState = GameFlowState.WON_DIALOG_SHOWN;
long timeTaken = System.currentTimeMillis() - gameStats.getCurrentGameStartTimeMs();
gameStats.recordWin(timeTaken);
showAchievementNotification(2048);
showGameWonKeepPlayingDialog();
} else if (game.isGameOver()) {
currentGameState = GameFlowState.GAME_OVER;
@ -483,72 +533,189 @@ public class MainActivity extends AppCompatActivity {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
LayoutInflater inflater = getLayoutInflater();
View dialogView = inflater.inflate(R.layout.dialog_settings, null);
builder.setView(dialogView);
builder.setCancelable(true); // Permet de fermer en cliquant à côté
builder.setView(dialogView).setCancelable(true);
// Récupération des vues
// Vues
SwitchMaterial switchSound = dialogView.findViewById(R.id.switchSound);
SwitchMaterial switchNotifications = dialogView.findViewById(R.id.switchNotifications);
SwitchMaterial switchNotifications = dialogView.findViewById(R.id.switchNotifications); // Activé
Button permissionsButton = dialogView.findViewById(R.id.buttonManagePermissions);
Button shareStatsButton = dialogView.findViewById(R.id.buttonShareStats);
Button resetStatsButton = dialogView.findViewById(R.id.buttonResetStats);
Button quitAppButton = dialogView.findViewById(R.id.buttonQuitApp);
Button closeButton = dialogView.findViewById(R.id.buttonCloseSettings);
// Ajout boutons de test (optionnel, pour débugger)
Button testNotifHS = new Button(this); // Crée programmatiquement
testNotifHS.setText(R.string.settings_test_notif_highscore);
Button testNotifInactiv = new Button(this);
testNotifInactiv.setText(R.string.settings_test_notif_inactivity);
// Ajouter ces boutons au layout 'dialogView' si nécessaire (ex: ((LinearLayout)dialogView).addView(...) )
final AlertDialog dialog = builder.create();
// Configuration initiale des switches (désactivés pour l'instant)
// Config Son (Placeholder)
switchSound.setEnabled(false);
switchSound.setChecked(false); // Mettre à jour avec la vraie valeur si implémenté
switchSound.setOnCheckedChangeListener((buttonView, isChecked) -> {
// TODO: Implémenter la logique Son ON/OFF
Toast.makeText(this, "Gestion du son à venir.", Toast.LENGTH_SHORT).show();
// switchSound.setChecked(!isChecked); // Revenir en arrière car pas implémenté
});
switchSound.setChecked(false);
switchSound.setOnCheckedChangeListener((v, i) -> Toast.makeText(this, "Gestion du son à venir.", Toast.LENGTH_SHORT).show());
switchNotifications.setEnabled(false);
switchNotifications.setChecked(false); // Mettre à jour avec la vraie valeur si implémenté
// Config Notifications (Activé + Gestion Permission)
switchNotifications.setEnabled(true); // Activé
switchNotifications.setChecked(notificationsEnabled); // État actuel
switchNotifications.setOnCheckedChangeListener((buttonView, isChecked) -> {
// TODO: Implémenter la logique Notifications ON/OFF
Toast.makeText(this, "Gestion des notifications à venir.", Toast.LENGTH_SHORT).show();
// switchNotifications.setChecked(!isChecked);
if (isChecked) {
// L'utilisateur VEUT activer les notifications
requestNotificationPermission(); // Demande la permission si nécessaire
} else {
// L'utilisateur désactive les notifications
notificationsEnabled = false;
saveNotificationPreference(false);
Toast.makeText(this, R.string.notifications_disabled, Toast.LENGTH_SHORT).show();
// Ici, annuler les éventuelles notifications planifiées (WorkManager/AlarmManager)
}
});
// Listener bouton Permissions
permissionsButton.setOnClickListener(v -> {
// Listeners autres boutons (Permissions, Share, Reset, Quit, Close)
permissionsButton.setOnClickListener(v -> { openAppSettings(); dialog.dismiss(); });
shareStatsButton.setOnClickListener(v -> { shareStats(); dialog.dismiss(); });
resetStatsButton.setOnClickListener(v -> { dialog.dismiss(); showResetStatsConfirmationDialog(); });
quitAppButton.setOnClickListener(v -> { dialog.dismiss(); finishAffinity(); });
closeButton.setOnClickListener(v -> dialog.dismiss());
// Listeners boutons de test (si ajoutés)
testNotifHS.setOnClickListener(v -> { showHighScoreNotification(gameStats.getOverallHighScore()); });
testNotifInactiv.setOnClickListener(v -> { showInactivityNotification(); });
dialog.show();
}
/** Met à jour l'état du switch notification (utile si permission refusée). */
private void updateNotificationSwitchState(boolean isEnabled) {
// Si la vue des paramètres est actuellement affichée, met à jour le switch
View settingsDialogView = getLayoutInflater().inflate(R.layout.dialog_settings, null); // Attention, regonfler n'est pas idéal
// Mieux: Garder une référence à la vue ou au switch si la dialog est affichée.
// Pour la simplicité ici, on suppose qu'il faut rouvrir les paramètres pour voir le changement.
}
/** Sauvegarde la préférence d'activation des notifications. */
private void saveNotificationPreference(boolean enabled) {
if (preferences != null) {
preferences.edit().putBoolean("notifications_enabled", enabled).apply();
}
}
/** Charge la préférence d'activation des notifications. */
private void loadNotificationPreference() {
if (preferences != null) {
notificationsEnabled = preferences.getBoolean("notifications_enabled", true); // Activé par défaut
}
}
/** Ouvre les paramètres système de l'application. */
private void openAppSettings() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(this, "Impossible d'ouvrir les paramètres de l'application.", Toast.LENGTH_LONG).show();
Toast.makeText(this, "Impossible d'ouvrir les paramètres.", Toast.LENGTH_LONG).show();
}
}
dialog.dismiss(); // Ferme les paramètres après clic
});
// Listener bouton Partager Stats
shareStatsButton.setOnClickListener(v -> {
shareStats();
dialog.dismiss(); // Ferme après partage (ou tentative)
});
// --- Gestion des Permissions (Notifications) ---
// Listener bouton Réinitialiser Stats
resetStatsButton.setOnClickListener(v -> {
dialog.dismiss(); // Ferme d'abord la dialogue des paramètres
showResetStatsConfirmationDialog(); // Ouvre la confirmation
});
/** Demande la permission POST_NOTIFICATIONS si nécessaire (Android 13+). */
private void requestNotificationPermission() {
// Vérifie si on est sur Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// Vérifie si la permission n'est PAS déjà accordée
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
// Demande la permission
requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS);
// Le résultat sera géré dans le callback du requestPermissionLauncher
} else {
// La permission est déjà accordée, on peut activer directement
notificationsEnabled = true;
saveNotificationPreference(true);
Toast.makeText(this, R.string.notifications_enabled, Toast.LENGTH_SHORT).show();
// Planifier les notifications ici si ce n'est pas déjà fait
}
} else {
// Sur les versions antérieures à Android 13, pas besoin de demander la permission
notificationsEnabled = true;
saveNotificationPreference(true);
Toast.makeText(this, R.string.notifications_enabled, Toast.LENGTH_SHORT).show();
// Planifier les notifications ici
}
}
// Listener bouton Quitter
quitAppButton.setOnClickListener(v -> {
dialog.dismiss();
finishAffinity(); // Ferme toutes les activités de l'application
});
// --- Logique de Notification ---
// Listener bouton Fermer
closeButton.setOnClickListener(v -> dialog.dismiss());
/**
* Crée l'Intent qui sera lancé au clic sur une notification (ouvre MainActivity).
* @return PendingIntent configuré.
*/
private PendingIntent createNotificationTapIntent() {
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); // Ouvre l'app ou la ramène devant
// FLAG_IMMUTABLE est requis pour Android 12+
int flags = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_UPDATE_CURRENT;
return PendingIntent.getActivity(this, 0, intent, flags);
}
dialog.show();
/**
* Construit et affiche une notification simple.
* @param context Contexte.
* @param title Titre de la notification.
* @param message Corps du message de la notification.
* @param notificationId ID unique pour cette notification.
*/
private void showNotification(Context context, String title, String message, int notificationId) {
// Vérifie si les notifications sont activées et si la permission est accordée (pour Android 13+)
if (!notificationsEnabled || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ActivityCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED)) {
// Ne pas envoyer la notification si désactivé ou permission manquante
return;
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_notification_2048) // Votre icône
.setContentTitle(title)
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(createNotificationTapIntent()) // Action au clic
.setAutoCancel(true); // Ferme la notif après clic
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
// L'ID notificationId est utilisé pour mettre à jour une notif existante ou en afficher une nouvelle
notificationManager.notify(notificationId, builder.build());
}
/** Affiche la notification d'accomplissement (ex: tuile 2048 atteinte). */
private void showAchievementNotification(int tileValue) {
String title = getString(R.string.notification_title_achievement);
String message = getString(R.string.notification_text_achievement, tileValue);
// Utiliser un ID spécifique pour ce type de notif (ex: 1)
showNotification(this, title, message, 1);
}
/** Affiche la notification de rappel du meilleur score (pour test). */
private void showHighScoreNotification(int highScore) {
String title = getString(R.string.notification_title_highscore);
String message = getString(R.string.notification_text_highscore, highScore);
// Utiliser un ID spécifique (ex: 2)
showNotification(this, title, message, 2);
// NOTE: La planification réelle utiliserait WorkManager/AlarmManager
}
/** Affiche la notification de rappel d'inactivité (pour test). */
private void showInactivityNotification() {
String title = getString(R.string.notification_title_inactivity);
String message = getString(R.string.notification_text_inactivity);
// Utiliser un ID spécifique (ex: 3)
showNotification(this, title, message, 3);
// NOTE: La planification réelle utiliserait WorkManager/AlarmManager et suivi du temps
}
/**

View File

@ -0,0 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal"> <path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/> </vector>

View File

@ -76,5 +76,19 @@
<string name="share_stats_subject">My 2048 Statistics</string>
<string name="share_stats_body">Here are my stats on Best 2048:\n- Best Score: %d\n- Highest Tile: %d\n- Games Won: %d / %d\n- Total Time: %s\n- Total Moves: %d</string>
<string name="stats_reset_confirmation">Statistics reset.</string>
<string name="notification_channel_name">Game Updates</string>
<string name="notification_channel_description">Notifications related to the 2048 game</string>
<string name="notification_title_achievement">Congratulations!</string>
<string name="notification_text_achievement">You reached the %d tile!</string>
<string name="notification_title_highscore">New Challenge!</string>
<string name="notification_text_highscore">Your best score is %d. Can you do better?</string>
<string name="notification_title_inactivity">We miss you!</string>
<string name="notification_text_inactivity">How about a quick game of 2048 to relax?</string>
<string name="notifications_permission_required_title">Permission Required</string>
<string name="notifications_permission_required_message">To receive notifications (reminders, achievements), please allow the application to send notifications in the settings.</string>
<string name="go_to_settings">Go to Settings</string>
<string name="notifications_enabled">Notifications enabled.</string>
<string name="notifications_disabled">Notifications disabled.</string>
<string name="settings_test_notif_highscore">Test High Score Notif</string>
<string name="settings_test_notif_inactivity">Test Inactivity Notif</string>
</resources>