diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d6edf06..60b9b3d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,10 @@ + \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/MainActivity.java b/app/src/main/java/legion/muyue/best2048/MainActivity.java index cc7b542..a67f88e 100644 --- a/app/src/main/java/legion/muyue/best2048/MainActivity.java +++ b/app/src/main/java/legion/muyue/best2048/MainActivity.java @@ -61,6 +61,7 @@ public class MainActivity extends AppCompatActivity { private static final int BOARD_SIZE = 4; private static final String NOTIFICATION_CHANNEL_ID = "BEST_2048_CHANNEL"; private boolean notificationsEnabled = true; + private static final String LAST_PLAYED_TIME_KEY = "last_played_time"; // --- State Management --- private boolean statisticsVisible = false; @@ -100,10 +101,14 @@ public class MainActivity extends AppCompatActivity { EdgeToEdge.enable(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + NotificationHelper.createNotificationChannel(this); createNotificationChannel(); findViews(); initializeGameAndStats(); setupListeners(); + if (notificationsEnabled) { + startNotificationService(); + } } @Override @@ -140,6 +145,13 @@ public class MainActivity extends AppCompatActivity { } } + /** Sauvegarde le timestamp actuel comme dernier moment joué. */ + private void saveLastPlayedTime() { + if (preferences != null) { + preferences.edit().putLong(LAST_PLAYED_TIME_KEY, System.currentTimeMillis()).apply(); + } + } + // --- Initialisation --- /** @@ -692,12 +704,27 @@ public class MainActivity extends AppCompatActivity { notificationManager.notify(notificationId, builder.build()); } - /** Affiche la notification d'accomplissement (ex: tuile 2048 atteinte). */ + /** Démarre le NotificationService s'il n'est pas déjà lancé. */ + private void startNotificationService() { + Intent serviceIntent = new Intent(this, NotificationService.class); + // Utiliser startForegroundService pour API 26+ si le service doit faire qqch rapidement + // mais pour une tâche périodique simple startService suffit. + startService(serviceIntent); + } + + /** Arrête le NotificationService. */ + private void stopNotificationService() { + Intent serviceIntent = new Intent(this, NotificationService.class); + stopService(serviceIntent); + } + + /** Affiche la notification d'accomplissement via le NotificationHelper. */ private void showAchievementNotification(int tileValue) { + // Vérifie l'état global avant d'envoyer (au cas où désactivé entre-temps) + if (!notificationsEnabled) return; 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); + NotificationHelper.showNotification(this, title, message, 1); // ID 1 pour achievement } /** Affiche la notification de rappel du meilleur score (pour test). */ diff --git a/app/src/main/java/legion/muyue/best2048/NotificationHelper.java b/app/src/main/java/legion/muyue/best2048/NotificationHelper.java new file mode 100644 index 0000000..7805e67 --- /dev/null +++ b/app/src/main/java/legion/muyue/best2048/NotificationHelper.java @@ -0,0 +1,91 @@ +// Fichier NotificationHelper.java +package legion.muyue.best2048; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; // Pour checkSelfPermission + +/** + * Classe utilitaire pour simplifier la création et l'affichage des notifications + * et la gestion du canal de notification pour l'application Best 2048. + */ +public class NotificationHelper { + + /** Identifiant unique du canal de notification pour cette application. */ + public static final String CHANNEL_ID = "BEST_2048_CHANNEL"; // Doit correspondre à celui utilisé avant + + /** + * Crée le canal de notification requis pour Android 8.0 (API 26) et supérieur. + * Cette méthode est idempotente (l'appeler plusieurs fois n'a pas d'effet négatif). + * Doit être appelée avant d'afficher la première notification sur API 26+. + * + * @param context Contexte applicatif. + */ + public static void createNotificationChannel(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = context.getString(R.string.notification_channel_name); + String description = context.getString(R.string.notification_channel_description); + int importance = NotificationManager.IMPORTANCE_DEFAULT; // Importance par défaut + + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance); + channel.setDescription(description); + // Enregistre le canal auprès du système. + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + if (notificationManager != null) { + notificationManager.createNotificationChannel(channel); + } + } + } + + /** + * Construit et affiche une notification. + * Vérifie la permission POST_NOTIFICATIONS sur Android 13+ avant d'essayer d'afficher. + * + * @param context Contexte (peut être une Activity ou un Service). + * @param title Titre de la notification. + * @param message Contenu texte de la notification. + * @param notificationId ID unique pour cette notification (permet de la mettre à jour ou l'annuler). + */ + public static void showNotification(Context context, String title, String message, int notificationId) { + // Vérification de la permission pour Android 13+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS) != android.content.pm.PackageManager.PERMISSION_GRANTED) { + // Si la permission n'est pas accordée, ne pas tenter d'afficher la notification. + // L'application devrait idéalement gérer la demande de permission avant d'appeler cette méthode + // si elle sait que l'utilisateur a activé les notifications dans les paramètres. + System.err.println("Permission POST_NOTIFICATIONS manquante. Notification non affichée."); + return; + } + } + + // Intent pour ouvrir MainActivity au clic + Intent intent = new Intent(context, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + int flags = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) ? PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE : PendingIntent.FLAG_UPDATE_CURRENT; + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, flags); + + // Construction de la notification via NotificationCompat pour la compatibilité + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_stat_notification_2048) // Votre icône de notification + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) // Priorité normale + .setContentIntent(pendingIntent) // Action au clic + .setAutoCancel(true); // Ferme la notification après le clic + + // Affichage de la notification + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + try { + notificationManager.notify(notificationId, builder.build()); + } catch (SecurityException e){ + // Gérer l'exception de sécurité qui peut survenir même avec la vérification ci-dessus dans certains cas limites + System.err.println("Erreur de sécurité lors de l'affichage de la notification : " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/legion/muyue/best2048/NotificationService.java b/app/src/main/java/legion/muyue/best2048/NotificationService.java new file mode 100644 index 0000000..897e4f7 --- /dev/null +++ b/app/src/main/java/legion/muyue/best2048/NotificationService.java @@ -0,0 +1,135 @@ +// Fichier NotificationService.java +package legion.muyue.best2048; + +import android.app.Service; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; // Important pour créer un Handler sur le Main Thread +import androidx.annotation.Nullable; +import java.util.concurrent.TimeUnit; + +/** + * Service exécuté en arrière-plan pour envoyer des notifications périodiques + * (rappel de meilleur score, rappel d'inactivité). + * Utilise un Handler pour planifier les tâches répétitives. + * NOTE : Pour une robustesse accrue (garantie d'exécution même si l'app est tuée), + * WorkManager serait préférable en production. + */ +public class NotificationService extends Service { + + private static final int NOTIFICATION_ID_HIGHSCORE = 2; // Doit être différent des autres notifs + private static final int NOTIFICATION_ID_INACTIVITY = 3; + // Intervalles (exemples : 1 jour pour HS, 3 jours pour inactivité) + private static final long HIGHSCORE_INTERVAL_MS = TimeUnit.DAYS.toMillis(1); + private static final long INACTIVITY_INTERVAL_MS = TimeUnit.DAYS.toMillis(3); + private static final long CHECK_INTERVAL_MS = TimeUnit.HOURS.toMillis(6); // Intervalle de vérification plus fréquent + + private Handler handler; + private Runnable periodicTaskRunnable; + + // Clés SharedPreferences (doivent correspondre à celles utilisées ailleurs) + private static final String PREFS_NAME = "Best2048_Prefs"; + private static final String HIGH_SCORE_KEY = "high_score"; + private static final String LAST_PLAYED_TIME_KEY = "last_played_time"; // Nouvelle clé + + @Override + public void onCreate() { + super.onCreate(); + // Utilise le Looper principal pour le Handler (simple, mais bloque si tâche longue) + // Pour des tâches plus lourdes, utiliser HandlerThread + handler = new Handler(Looper.getMainLooper()); + // Pas besoin de créer le canal ici si MainActivity le fait déjà au démarrage + // NotificationHelper.createNotificationChannel(this); + + periodicTaskRunnable = new Runnable() { + @Override + public void run() { + // Vérifie périodiquement s'il faut envoyer une notification + checkAndSendNotifications(); + // Replanifie la tâche + handler.postDelayed(this, CHECK_INTERVAL_MS); // Vérifie toutes les X heures + } + }; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // Lance la tâche périodique lors du démarrage du service + handler.removeCallbacks(periodicTaskRunnable); // Assure qu'il n'y a pas de doublon + handler.post(periodicTaskRunnable); // Lance immédiatement la première vérification + + // START_STICKY : Le système essaiera de recréer le service s'il est tué. + return START_STICKY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + // Arrête la planification des tâches lorsque le service est détruit + if (handler != null && periodicTaskRunnable != null) { + handler.removeCallbacks(periodicTaskRunnable); + } + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + // Service non lié (Started Service) + return null; + } + + /** + * Vérifie les conditions et envoie les notifications périodiques si nécessaire. + */ + private void checkAndSendNotifications() { + SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); + boolean notificationsEnabled = prefs.getBoolean("notifications_enabled", true); // Vérifie si activé + + if (!notificationsEnabled) { + // Si désactivé dans les prefs, on arrête potentiellement le service? + // Ou juste ne rien envoyer. Pour l'instant, ne rien envoyer. + // stopSelf(); // Arrêterait le service + return; + } + + // --- Notification High Score (Exemple: envoyer une fois par jour si non joué depuis ?) --- + // Logique simplifiée: on envoie juste le rappel basé sur un flag ou temps (pas implémenté ici) + // Pour une vraie app, il faudrait une logique pour ne pas spammer. + // Exemple: Envoyer si le dernier envoi date de plus de HIGHSCORE_INTERVAL_MS ? + int highScore = prefs.getInt(HIGH_SCORE_KEY, 0); + // Temporairement on l'envoie à chaque check pour test (à modifier!) + // if (shouldSendHighScoreNotification()) { + showHighScoreNotificationNow(highScore); + // } + + + // --- Notification d'Inactivité --- + long lastPlayedTime = prefs.getLong(LAST_PLAYED_TIME_KEY, 0); + if (lastPlayedTime > 0 && (System.currentTimeMillis() - lastPlayedTime > INACTIVITY_INTERVAL_MS)) { + // Si l'inactivité dépasse le seuil + showInactivityNotificationNow(); + // Optionnel: Mettre à jour lastPlayedTime pour ne pas renvoyer immédiatement ? + // Ou attendre que l'utilisateur rejoue pour mettre à jour lastPlayedTime dans onPause. + } + } + + /** Affiche la notification High Score */ + private void showHighScoreNotificationNow(int highScore) { + String title = getString(R.string.notification_title_highscore); + String message = getString(R.string.notification_text_highscore, highScore); + NotificationHelper.showNotification(this, title, message, NOTIFICATION_ID_HIGHSCORE); + } + + /** Affiche la notification d'Inactivité */ + private void showInactivityNotificationNow() { + String title = getString(R.string.notification_title_inactivity); + String message = getString(R.string.notification_text_inactivity); + NotificationHelper.showNotification(this, title, message, NOTIFICATION_ID_INACTIVITY); + } + + // Ajouter ici une logique plus fine si nécessaire pour savoir QUAND envoyer les notifs périodiques + // private boolean shouldSendHighScoreNotification() { ... } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/logo.jpeg b/app/src/main/res/drawable/logo.jpeg new file mode 100644 index 0000000..f88e56a Binary files /dev/null and b/app/src/main/res/drawable/logo.jpeg differ diff --git a/app/src/main/res/drawable/logolegionmuyue.png~ b/app/src/main/res/drawable/logolegionmuyue.png~ new file mode 100644 index 0000000..209d8f4 Binary files /dev/null and b/app/src/main/res/drawable/logolegionmuyue.png~ differ diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml index 6f3b755..0372815 100644 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml index 6f3b755..0372815 100644 --- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file