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