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:
parent
a61f110992
commit
7dc7360e14
@ -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" />
|
||||
|
@ -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,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.
|
||||
*/
|
||||
|
8
app/src/main/res/drawable/ic_stat_notification_2048.xml
Normal file
8
app/src/main/res/drawable/ic_stat_notification_2048.xml
Normal 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>
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user