אנחנו ממשיכים מהיישום בסוף פרק 3. נוסיף כפתור LocalNotif: לחיצה עליו תבקש הרשאה (אם עד כה לא אישרנו), ולאחר אישור תשלח לנו מיד התראה (לאותו טלפון). בהמשך נראה עוד שתי דרכים שייצרו התראה דומה בטלפונים של אחרים, הן מתוך האפליקציה שלנו והן ממקור חיצוני לגמרי.
חזרה ל- 3 - זמן, שיא וסיום המשחק
מה נלמד
- כיצד מצהירים על
POST_NOTIFICATIONSומבקשים אותה בזמן ריצה ב־Android 13 ומעלה. - מהו
NotificationChannelומדוע כל התראה משתייכת לערוץ. - כיצד בונים
Notificationומוסרים אותה ל־Android בעזרתnotify.
לא נשתמש בתזמון או בעבודה ברקע. אין כאן AlarmManager, JobScheduler או WorkManager:
הלחיצה היא האירוע שמציג את ההתראה מיד.
1. מצהירים על הרשאת ההתראות
בחלון Project, פתחו:
app > manifests > AndroidManifest.xml
הוסיפו את ההרשאה בתוך manifest, לפני application:
<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" />
+
<application
android:allowBackup="true"
הצהרה ב־Manifest רק מאפשרת ליישום לבקש את ההרשאה. ב־Android 13 ומעלה המשתמש עדיין צריך לאשר אותה בחלון מערכת בזמן ריצה, ועלינו להחליט מתי לבקש את ההרשאה. לאחר האישור הראשון האפליקציה יודעת לא לבקש שוב.
2. מוסיפים את הכפתור למסך
תזכורת: אם עדיין לא תרגלתם עבודה ב-constraint layout באמצעות הממשק הגרפי, עדיין כדאי לעשות זאת בקישור זה. אם לא כעת, זכרו למצוא זמן מתאים ולתרגל יצירת מסכים כאלו.
בחלון Project, פתחו:
app > res > layout > activity_main.xml
בתוך ConstraintLayout, אחרי gameBoard ולפני תגית הסיום, הוסיפו את הכפתור. באותו שינוי
עדכנו את ה־constraint של הלוח, כדי שהלוח יתחיל מתחת לכפתור החדש:
<com.example.collectcircles.GameBoardView
android:id="@+id/gameBoard"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="20dp"
android:minHeight="320dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/startButton"
+ app:layout_constraintTop_toBottomOf="@id/localNotifButton"
tools:background="@color/board_background" />
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/localNotifButton"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/local_notification"
+ app:cornerRadius="14dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/startButton" />
+
</androidx.constraintlayout.widget.ConstraintLayout>
כעת פתחו:
app > res > values > strings.xml
הוסיפו את טקסט הכפתור אחרי start, ואת טקסטי ההתראה לפני </resources>:
<string name="start">Start</string>
+ <string name="local_notification">Permission request + Local Notif</string>
<string name="restart">Restart</string>
ובהמשך אותו קובץ:
<string name="completion_message">You collected every circle in %1$.3f seconds.</string>
<string name="ok">OK</string>
+ <string name="notification_channel_name">Circle invitations</string>
+ <string name="notification_channel_description">Notifications about Collect Circles invitations</string>
+ <string name="notification_title">Collect Circles</string>
+ <string name="notification_text">Come collect circles with me!</string>
+ <string name="notification_permission_denied">Notification permission is required</string>
</resources>
3. יוצרים את מחלקת העזר Notifications
בחלון Project, עברו אל:
app > java > com.example.collectcircles
לחצו לחיצה ימנית על החבילה com.example.collectcircles, בחרו
New > Java Class, הקלידו Notifications ובחרו Class. Android Studio ייצור בדיוק את:
app/src/main/java/com/example/collectcircles/Notifications.java
החליפו את תוכן הקובץ בקוד הבא:
package com.example.collectcircles;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
public final class Notifications {
private static final String CHANNEL_ID = "circle_invitations";
private static final int NOTIFICATION_ID = 1;
private Notifications() {
// זוהי מחלקת עזר בלבד. אין צורך ליצור ממנה אובייקטים.
}
public static void createChannel(Context context) {
NotificationManager manager = context.getSystemService(NotificationManager.class);
// כל התראה חייבת להשתייך לערוץ. יצירת ערוץ שכבר קיים אינה יוצרת כפילות,
// ולכן בטוח לבצע את הפעולה לפני כל הצגה. חשיבות HIGH מאפשרת למערכת
// להציג את ההתראה באופן בולט, בהתאם להגדרות שבחר המשתמש במכשיר.
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
context.getString(R.string.notification_channel_name),
NotificationManager.IMPORTANCE_HIGH
);
channel.setDescription(
context.getString(R.string.notification_channel_description)
);
manager.createNotificationChannel(channel);
}
public static void show(Context context) {
createChannel(context);
NotificationManager manager = context.getSystemService(NotificationManager.class);
// ה-Builder רק מתאר את ההתראה. הקריאה notify היא הפעולה שמוסרת אותה
// למערכת Android, ומאותו רגע המערכת אחראית להציג אותה למשתמש.
Notification notification = new Notification.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle(context.getString(R.string.notification_title))
.setContentText(context.getString(R.string.notification_text))
.build();
manager.notify(NOTIFICATION_ID, notification);
}
}
CHANNEL_ID הוא המזהה הפנימי הקבוע. שם הערוץ והתיאור הם הטקסטים שהמשתמש רואה בהגדרות.
יצירת ערוץ שכבר קיים בטוחה; Android אינו יוצר עותק נוסף. IMPORTANCE_HIGH מאפשר הצגה
בולטת, אבל המשתמש עדיין רשאי לשנות את הגדרות הערוץ.
שימוש חוזר ב־NOTIFICATION_ID מעדכן את אותה התראה במקום למלא את מגירת ההתראות בעותקים.
4. מבקשים הרשאה ומחברים את הכפתור
בחלון Project, פתחו:
app > java > com.example.collectcircles > MainActivity
זהו הקובץ:
app/src/main/java/com/example/collectcircles/MainActivity.java
א. מוסיפים imports
בראש הקובץ הוסיפו את ה־imports הבאים במקומות המתאימים. Android Studio יכול להוסיף אותם אוטומטית בעזרת Alt+Enter:
package com.example.collectcircles;
+import android.Manifest;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
+import android.widget.Toast;
import androidx.activity.EdgeToEdge;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
ב. מוסיפים שדה
ליד שדה ה־binding הוסיפו launcher שיקבל את תשובת המשתמש:
private ActivityMainBinding binding;
+ private ActivityResultLauncher<String> notificationPermissionLauncher;
private SharedPreferences preferences;
ג. רושמים את בקשת ההרשאה
בתוך onCreate, מיד אחרי בלוק ה־insets ולפני getSharedPreferences, הוסיפו:
ViewCompat.setOnApplyWindowInsetsListener(binding.main, (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
+ // החל מ-Android 13 צריך אישור מפורש כדי להציג התראות.
+ notificationPermissionLauncher = registerForActivityResult(
+ new ActivityResultContracts.RequestPermission(),
+ permissionGranted -> { // callback כאן מוצהר מנגנון
+ // זוהי פונקציה אנונימית - קטע קוד ללא שם - שאנחנו מבקשים שירוץ
+ if (permissionGranted) {
+ Notifications.show(this);
+ } else {
+ Toast.makeText(
+ this,
+ R.string.notification_permission_denied,
+ Toast.LENGTH_SHORT
+ ).show();
+ }
+ }
+ );
+
preferences = getSharedPreferences(PREFERENCES_NAME, MODE_PRIVATE);
חשוב להפריד בין שתי פעולות שמתרחשות אצלנו באותה לחיצה. הכפתור LocalNotif גם מתחיל
את תהליך בקשת ההרשאה וגם, לאחר שההרשאה קיימת, מבקש להציג התראה. אלה אינן אותה פעולה:
notificationPermissionLauncher.launch(...)פותחת חלון מערכת ומבקשת מן המשתמש רשות להציג התראות. רק קוד שרץ באפליקציה כשהמשתמש משתמש בה יכול לפתוח את החלון הזה.Notifications.show(...)יוצרת התראה ומוסרת אותה ל־Android. היא תצליח להציג אותה רק אם ההרשאה כבר ניתנה.
אותה הרשאה חלה גם על התראות שמגיעות מאוחר יותר מ־FCM. אם ב־Android 13 ומעלה האפליקציה מעולם
לא ביקשה מן המשתמש POST_NOTIFICATIONS, או שהמשתמש דחה אותה, שרת החברה עדיין יכול לשלוח
הודעת FCM והמכשיר אף עשוי לקבל אותה — אבל Android לא יציג אותה כהתראה למשתמש. השרת וה־curl
אינם יכולים לפתוח מרחוק את חלון ההרשאה או לאשר אותו במקום המשתמש.
ד. מוסיפים את הפעולה
מיד אחרי onCreate ולפני startGame, הוסיפו:
}
+ private void requestPermissionAndShowNotification() {
+ // ומטה אין הרשאת התראות בזמן ריצה Android 12 -ב
+ // מבקשים אותה אם עדיין לא ניתנה Android 13 -ב
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
+ && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
+ != PackageManager.PERMISSION_GRANTED) {
+ notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
+ return; // Notifications.show כלומר, אם עכשיו שלחנו בקשה לא נמשיך לשורה של ה
+ }
+
+ Notifications.show(this); // מציג התראות רק אם זו לא הלחיצה הראשונה
+ // המופיע למעלה במדריך callback בלחיצה הראשונה תתקבל ההתראה בזכות מנגנון
+ }
+
private void startGame() {
ה. מחברים את הלחיצה
אחרי ה־listener של startButton הוסיפו את ה־listener החדש:
binding.startButton.setOnClickListener(view -> startGame());
+ binding.localNotifButton.setOnClickListener(
+ view -> requestPermissionAndShowNotification()
+ );
binding.gameBoard.setOnGameFinishedListener(this::finishGame);
בקשת הרשאה היא פעולה אסינכרונית: הקוד שמציג את חלון ההרשאה מסתיים לפני שהמשתמש בוחר אם לאשר או לדחות. לכן אי־אפשר לקרוא בנוחות ל־Notifications.show(this) מיד אחרי בקשת ההרשאה בתוך המאזין של הכפתור—הקריאה תתבצע לפני שהתקבלה תשובת המשתמש. את ההצגה לאחר האישור מבצעים ב־callback של notificationPermissionLauncher; אם ההרשאה כבר קיימת, אפשר להציג את ההתראה מיד.
ב־Android 13 ומעלה הזרימה בלחיצה הראשונה היא:
LocalNotif ⟶ בקשת הרשאה ⟶ אישור המשתמש ⟶ Notifications.show ⟶ Android notification
בפעמים הבאות ההרשאה כבר קיימת ולכן ההצגה מיידית. ב־Android 12 ומטה אין חלון הרשאה זה.
היכן מבקשים הרשאה באפליקציה שאין בה כפתור LocalNotif?
באפליקציה אמיתית אין צורך בכפתור שמציג התראה מקומית. עדיין חייב להיות רגע בצד האפליקציה
שבו מסבירים למשתמש מדוע ההתראות מועילות ומפעילים את
notificationPermissionLauncher. המקום המתאים תלוי במוצר:
- במסך היכרות, לאחר הסבר קצר כגון “נודיע לך כשמגיעה הודעה חדשה” וכפתור הפעלת התראות;
- כאשר המשתמש מפעיל לראשונה תכונה שתלויה בהתראות, למשל תזכורות, משלוחים או הודעות חדשות;
- במסך Settings של האפליקציה, כאשר המשתמש מפעיל מתג קבלת התראות;
- לאחר כניסה לחשבון, אם רק משתמש מחובר יכול לקבל את ההתראות התקופתיות של החברה.
מומלץ לא לפתוח את חלון המערכת מיד וללא הקשר בפתיחה הראשונה. קודם מציגים הסבר קצר במסך האפליקציה, ורק בעקבות בחירה ברורה של המשתמש מפעילים את בקשת המערכת. כך המשתמש מבין מה יקבל ומדוע כדאי לאשר.
הזרימה באפליקציה שמקבלת התראות תקופתיות משרתי חברה תהיה:
פתיחת האפליקציה בפעם הראשונה
→ הסבר על יתרון ההתראות
→ המשתמש בוחר "הפעלת התראות"
→ בקשת POST_NOTIFICATIONS ואישור המשתמש
→ רישום המכשיר ב-FCM ושמירת המנוי
→ בעתיד השרת שולח הודעות גם כשהמסך אינו פתוח
→ Android מציג אותן באמצעות ההרשאה שכבר ניתנה
הרשאה היא החלטה של המשתמש במכשיר, ואילו רישום ל־FCM הוא חיבור טכני לשירות ההודעות. צריך את שניהם: מנוי FCM בלי הרשאה יכול לקבל מידע בלי להציג התראה; הרשאה בלי רישום FCM מאפשרת להציג התראות מקומיות, אבל אין לשרת כתובת או topic שדרכם ישלח הודעה. בפרק 6 נוסיף את צד ה־FCM.
5. בודקים את השלב
- בצעו Build > Make Project והפעילו את היישום.
- לחצו
LocalNotif. - ב־Android 13 ומעלה אשרו את בקשת ההתראות.
- ודאו שמופיעה התראה בשם Collect Circles עם הטקסט Come collect circles with me!.
- לחצו שוב וודאו שההתראה מתעדכנת מיד.
אם דחיתם את ההרשאה, היישום יציג Toast ולא יוכל להציג התראה. אפשר לאשר אותה דרך הגדרות היישום במכשיר ולנסות שוב.
הטלפון כבר ביקש הרשאת התראות ויכול להציג התראה מקומית. בפרק 6 נרשום אותו לשני נושאי FCM; גם ההתראות שמגיעות מן השרת כפופות לאותה הרשאה.