168 lines
7.2 KiB
Java
168 lines
7.2 KiB
Java
package im.rosetta.service.dispatch;
|
||
|
||
import java.io.FileInputStream;
|
||
import java.io.IOException;
|
||
import java.util.HashMap;
|
||
import java.util.List;
|
||
import java.util.concurrent.ExecutorService;
|
||
import java.util.concurrent.Executors;
|
||
|
||
import com.google.auth.oauth2.GoogleCredentials;
|
||
import com.google.firebase.FirebaseApp;
|
||
import com.google.firebase.FirebaseOptions;
|
||
import com.google.firebase.messaging.AndroidConfig;
|
||
import com.google.firebase.messaging.ApnsConfig;
|
||
import com.google.firebase.messaging.Aps;
|
||
import com.google.firebase.messaging.FirebaseMessaging;
|
||
import com.google.firebase.messaging.Message;
|
||
import com.google.firebase.messaging.Notification;
|
||
|
||
import im.rosetta.database.repository.UserRepository;
|
||
import im.rosetta.service.dispatch.runtime.PushType;
|
||
import im.rosetta.service.services.UserService;
|
||
|
||
/**
|
||
* Класс для отправки push-уведомлений пользователям через Firebase Cloud Messaging (FCM).
|
||
*/
|
||
public class FirebaseDispatcher {
|
||
|
||
private UserRepository userRepository = new UserRepository();
|
||
private UserService userService = new UserService(userRepository);
|
||
private final ExecutorService executor = Executors.newFixedThreadPool(10);
|
||
|
||
public FirebaseDispatcher() {
|
||
initializeFirebase();
|
||
}
|
||
|
||
/**
|
||
* Инициализация Firebase Admin SDK
|
||
*/
|
||
private void initializeFirebase() {
|
||
if (FirebaseApp.getApps().isEmpty()) {
|
||
try {
|
||
String firebaseCredentialsPath = System.getenv("FIREBASE_CREDENTIALS_PATH");
|
||
if (firebaseCredentialsPath == null || firebaseCredentialsPath.isEmpty()) {
|
||
throw new IllegalStateException("FIREBASE_CREDENTIALS_PATH environment variable is not set");
|
||
}
|
||
|
||
FileInputStream serviceAccount = new FileInputStream(firebaseCredentialsPath);
|
||
FirebaseOptions options = FirebaseOptions.builder()
|
||
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
|
||
.build();
|
||
FirebaseApp.initializeApp(options);
|
||
} catch (IOException e) {
|
||
throw new RuntimeException("Failed to initialize Firebase", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
private Message buildMessage(String token, HashMap<String, String> data) {
|
||
String type = data.get("type");
|
||
if(type == null){
|
||
throw new IllegalArgumentException("Push notification type is required in data");
|
||
}
|
||
ApnsConfig.Builder apnsConfig = ApnsConfig.builder();
|
||
AndroidConfig.Builder androidConfig = AndroidConfig.builder();
|
||
Message.Builder messageBuilder = Message.builder()
|
||
.setToken(token)
|
||
.putAllData(data);
|
||
switch(type) {
|
||
case PushType.READ:
|
||
/**
|
||
* Тихий тип уведомления для очистки отправленных уведомлений на устройстве,
|
||
* не должен отображаться пользователю, поэтому не задаем звук и ставим contentAvailable для iOS и high priority для Android
|
||
*/
|
||
apnsConfig.setAps(Aps.builder().setContentAvailable(true).setSound("default").build());
|
||
androidConfig.setPriority(AndroidConfig.Priority.HIGH);
|
||
messageBuilder.setApnsConfig(apnsConfig.build());
|
||
messageBuilder.setAndroidConfig(androidConfig.build());
|
||
break;
|
||
case PushType.PERSONAL_MESSAGE:
|
||
case PushType.GROUP_MESSAGE:
|
||
/**
|
||
* Уведомление о новом сообщении, должно отображаться пользователю, поэтому задаем звук и high priority для Android
|
||
*/
|
||
String body = type == PushType.PERSONAL_MESSAGE ? "New message" : "New group message";
|
||
apnsConfig.setAps(Aps.builder().setSound("default").setMutableContent(true).build());
|
||
androidConfig.setPriority(AndroidConfig.Priority.HIGH);
|
||
messageBuilder.setApnsConfig(apnsConfig.build());
|
||
messageBuilder.setNotification(Notification.builder().setTitle(
|
||
data.getOrDefault("title", "Rosetta")
|
||
).setBody(body).build());
|
||
messageBuilder.setAndroidConfig(androidConfig.build());
|
||
break;
|
||
case PushType.CALL:
|
||
/**
|
||
* Звонок для андроид используем high priority, чтобы уведомление доставлялось даже если устройство в режиме Doze,
|
||
* для iOS используем VoIP уведомление, которое доставляется даже если приложение убито
|
||
*/
|
||
androidConfig.setPriority(AndroidConfig.Priority.HIGH);
|
||
messageBuilder.setAndroidConfig(androidConfig.build());
|
||
break;
|
||
}
|
||
return messageBuilder.build();
|
||
}
|
||
|
||
public void sendPushNotification(String publicKey, HashMap<String, String> data) {
|
||
executor.submit(() -> {
|
||
try {
|
||
List<String> tokens = userService.getNotificationsTokens(publicKey);
|
||
if (tokens == null || tokens.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
for (String token : tokens) {
|
||
try {
|
||
Message message = this.buildMessage(token, data);
|
||
FirebaseMessaging.getInstance().send(message);
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Отправляет push-уведомление нескольким пользователям (асинхронно)
|
||
* @param publicKeys список публичных ключей пользователей
|
||
* @param data данные уведомления
|
||
*/
|
||
public void sendPushNotification(List<String> publicKeys, HashMap<String, String> data) {
|
||
executor.submit(() -> {
|
||
for (String publicKey : publicKeys) {
|
||
sendPushNotificationSync(publicKey, data);
|
||
}
|
||
});
|
||
}
|
||
|
||
private void sendPushNotificationSync(String publicKey, HashMap<String, String> data) {
|
||
try {
|
||
List<String> tokens = userService.getNotificationsTokens(publicKey);
|
||
if (tokens == null || tokens.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
for (String token : tokens) {
|
||
try {
|
||
Message message = this.buildMessage(token, data);
|
||
|
||
FirebaseMessaging.getInstance().send(message);
|
||
} catch (Exception e) {
|
||
e.printStackTrace();
|
||
}
|
||
}
|
||
} catch (Exception e) {
|
||
// Логирование ошибки
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Завершить работу executor при остановке приложения
|
||
*/
|
||
public void shutdown() {
|
||
executor.shutdown();
|
||
}
|
||
} |