diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3185668..d6edf06 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + diff --git a/app/src/main/java/legion/muyue/best2048/MainActivity.java b/app/src/main/java/legion/muyue/best2048/MainActivity.java index d894f48..cc7b542 100644 --- a/app/src/main/java/legion/muyue/best2048/MainActivity.java +++ b/app/src/main/java/legion/muyue/best2048/MainActivity.java @@ -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 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,74 +533,191 @@ 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); - }); - - // Listener bouton Permissions - permissionsButton.setOnClickListener(v -> { - 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(); + 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) } - 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) - }); - - // Listener bouton Réinitialiser Stats - resetStatsButton.setOnClickListener(v -> { - dialog.dismiss(); // Ferme d'abord la dialogue des paramètres - showResetStatsConfirmationDialog(); // Ouvre la confirmation - }); - - // Listener bouton Quitter - quitAppButton.setOnClickListener(v -> { - dialog.dismiss(); - finishAffinity(); // Ferme toutes les activités de l'application - }); - - // Listener bouton Fermer + // 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.", Toast.LENGTH_LONG).show(); + } + } + + // --- Gestion des Permissions (Notifications) --- + + /** 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 + } + } + + // --- Logique de Notification --- + + /** + * 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); + } + + /** + * 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 + } + /** * Crée et lance une Intent pour partager les statistiques du joueur. */ diff --git a/app/src/main/res/drawable/ic_stat_notification_2048.xml b/app/src/main/res/drawable/ic_stat_notification_2048.xml new file mode 100644 index 0000000..006a766 --- /dev/null +++ b/app/src/main/res/drawable/ic_stat_notification_2048.xml @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e4019a..864dec5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -76,5 +76,19 @@ My 2048 Statistics 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 Statistics reset. - + Game Updates + Notifications related to the 2048 game + Congratulations! + You reached the %d tile! + New Challenge! + Your best score is %d. Can you do better? + We miss you! + How about a quick game of 2048 to relax? + Permission Required + To receive notifications (reminders, achievements), please allow the application to send notifications in the settings. + Go to Settings + Notifications enabled. + Notifications disabled. + Test High Score Notif + Test Inactivity Notif \ No newline at end of file