/*
* Copyright (c) 2010-2019 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.linphone.notifications;
import static android.content.Context.NOTIFICATION_SERVICE;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.service.notification.StatusBarNotification;
import java.io.File;
import java.util.HashMap;
import org.linphone.LinphoneManager;
import org.linphone.R;
import org.linphone.call.CallActivity;
import org.linphone.call.CallIncomingActivity;
import org.linphone.call.CallOutgoingActivity;
import org.linphone.chat.ChatActivity;
import org.linphone.compatibility.Compatibility;
import org.linphone.contacts.ContactsManager;
import org.linphone.contacts.LinphoneContact;
import org.linphone.core.Address;
import org.linphone.core.Call;
import org.linphone.core.ChatMessage;
import org.linphone.core.ChatMessageListenerStub;
import org.linphone.core.ChatRoom;
import org.linphone.core.ChatRoomCapabilities;
import org.linphone.core.Content;
import org.linphone.core.Core;
import org.linphone.core.CoreListenerStub;
import org.linphone.core.Reason;
import org.linphone.core.tools.Log;
import org.linphone.dialer.DialerActivity;
import org.linphone.history.HistoryActivity;
import org.linphone.service.LinphoneService;
import org.linphone.settings.LinphonePreferences;
import org.linphone.utils.DeviceUtils;
import org.linphone.utils.FileUtils;
import org.linphone.utils.ImageUtils;
import org.linphone.utils.LinphoneUtils;
import org.linphone.utils.MediaScannerListener;
public class NotificationsManager {
private static final int SERVICE_NOTIF_ID = 1;
private static final int MISSED_CALLS_NOTIF_ID = 2;
private final Context mContext;
private final NotificationManager mNM;
private final HashMap mChatNotifMap;
private final HashMap mCallNotifMap;
private int mLastNotificationId;
private final Notification mServiceNotification;
private int mCurrentForegroundServiceNotification;
private String mCurrentChatRoomAddress;
private CoreListenerStub mListener;
private ChatMessageListenerStub mMessageListener;
public NotificationsManager(Context context) {
mContext = context;
mChatNotifMap = new HashMap<>();
mCallNotifMap = new HashMap<>();
mCurrentForegroundServiceNotification = 0;
mCurrentChatRoomAddress = null;
mNM = (NotificationManager) mContext.getSystemService(NOTIFICATION_SERVICE);
if (mContext.getResources().getBoolean(R.bool.keep_missed_call_notification_upon_restart)) {
StatusBarNotification[] notifs = Compatibility.getActiveNotifications(mNM);
if (notifs != null && notifs.length > 1) {
for (StatusBarNotification notif : notifs) {
if (notif.getId() != MISSED_CALLS_NOTIF_ID) {
dismissNotification(notif.getId());
}
}
}
} else {
mNM.cancelAll();
}
mLastNotificationId = 5; // Do not conflict with hardcoded notifications ids !
Compatibility.createNotificationChannels(mContext);
Bitmap bm = null;
try {
bm = BitmapFactory.decodeResource(mContext.getResources(), R.mipmap.ic_launcher);
} catch (Exception e) {
Log.e(e);
}
Intent notifIntent = new Intent(mContext, DialerActivity.class);
notifIntent.putExtra("Notification", true);
addFlagsToIntent(notifIntent);
PendingIntent pendingIntent =
PendingIntent.getActivity(
mContext, SERVICE_NOTIF_ID, notifIntent, PendingIntent.FLAG_UPDATE_CURRENT);
mServiceNotification =
Compatibility.createNotification(
mContext,
mContext.getString(R.string.service_name),
"",
R.drawable.linphone_notification_icon,
R.mipmap.ic_launcher,
bm,
pendingIntent,
Notification.PRIORITY_MIN,
true);
mListener =
new CoreListenerStub() {
@Override
public void onMessageSent(Core core, ChatRoom room, ChatMessage message) {
if (room.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
Compatibility.createChatShortcuts(mContext);
}
}
@Override
public void onMessageReceived(
Core core, final ChatRoom cr, final ChatMessage message) {
if (message.isOutgoing()
|| mContext.getResources().getBoolean(R.bool.disable_chat)
|| mContext.getResources()
.getBoolean(R.bool.disable_chat_message_notification)) {
return;
}
if (mCurrentChatRoomAddress != null
&& mCurrentChatRoomAddress.equals(
cr.getPeerAddress().asStringUriOnly())) {
Log.i(
"[Notifications Manager] Message received for currently displayed chat room, do not make a notification");
return;
}
if (message.getErrorInfo() != null
&& message.getErrorInfo().getReason()
== Reason.UnsupportedContent) {
Log.w(
"[Notifications Manager] Message received but content is unsupported, do not notify it");
return;
}
if (!message.hasTextContent()
&& message.getFileTransferInformation() == null) {
Log.w(
"[Notifications Manager] Message has no text or file transfer information to display, ignoring it...");
return;
}
final Address from = message.getFromAddress();
final LinphoneContact contact =
ContactsManager.getInstance().findContactFromAddress(from);
final String textMessage =
(message.hasTextContent())
? message.getTextContent()
: mContext.getString(
R.string.content_description_incoming_file);
String file = null;
for (Content c : message.getContents()) {
if (c.isFile()) {
file = c.getFilePath();
LinphoneManager.getInstance()
.getMediaScanner()
.scanFile(
new File(file),
new MediaScannerListener() {
@Override
public void onMediaScanned(
String path, Uri uri) {
createNotification(
cr,
contact,
from,
textMessage,
message.getTime(),
uri,
FileUtils.getMimeFromFile(path));
}
});
break;
}
}
if (file == null) {
createNotification(
cr, contact, from, textMessage, message.getTime(), null, null);
}
if (cr.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
Compatibility.createChatShortcuts(mContext);
}
}
};
mMessageListener =
new ChatMessageListenerStub() {
@Override
public void onMsgStateChanged(ChatMessage msg, ChatMessage.State state) {
if (msg.getUserData() == null) return;
int notifId = (int) msg.getUserData();
Log.i(
"[Notifications Manager] Reply message state changed ("
+ state.name()
+ ") for notif id "
+ notifId);
if (state != ChatMessage.State.InProgress) {
// There is no need to be called here twice
msg.removeListener(this);
}
if (state == ChatMessage.State.Delivered
|| state == ChatMessage.State.Displayed) {
Notifiable notif =
mChatNotifMap.get(
msg.getChatRoom().getPeerAddress().asStringUriOnly());
if (notif == null) {
Log.e(
"[Notifications Manager] Couldn't find message notification for SIP URI "
+ msg.getChatRoom()
.getPeerAddress()
.asStringUriOnly());
dismissNotification(notifId);
return;
} else if (notif.getNotificationId() != notifId) {
Log.w(
"[Notifications Manager] Notif ID doesn't match: "
+ notifId
+ " != "
+ notif.getNotificationId());
}
displayReplyMessageNotification(msg, notif);
} else if (state == ChatMessage.State.NotDelivered) {
Log.e(
"[Notifications Manager] Couldn't send reply, message is not delivered");
dismissNotification(notifId);
}
}
};
}
public void onCoreReady() {
Core core = LinphoneManager.getCore();
if (core != null) {
core.addListener(mListener);
}
}
public void destroy() {
// mNM.cancelAll();
// Don't use cancelAll to keep message notifications !
// When a message is received by a push, it will create a LinphoneService
// but it might be getting killed quite quickly after that
// causing the notification to be missed by the user...
Log.i("[Notifications Manager] Getting destroyed, clearing Service & Call notifications");
if (mCurrentForegroundServiceNotification > 0) {
mNM.cancel(mCurrentForegroundServiceNotification);
}
for (Notifiable notifiable : mCallNotifMap.values()) {
mNM.cancel(notifiable.getNotificationId());
}
Core core = LinphoneManager.getCore();
if (core != null) {
core.removeListener(mListener);
}
}
private void addFlagsToIntent(Intent intent) {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
}
public void startForeground() {
if (LinphoneService.isReady()) {
Log.i("[Notifications Manager] Starting Service as foreground");
LinphoneService.instance().startForeground(SERVICE_NOTIF_ID, mServiceNotification);
mCurrentForegroundServiceNotification = SERVICE_NOTIF_ID;
}
}
private void startForeground(Notification notification, int id) {
if (LinphoneService.isReady()) {
Log.i("[Notifications Manager] Starting Service as foreground while in call");
LinphoneService.instance().startForeground(id, notification);
mCurrentForegroundServiceNotification = id;
}
}
public void stopForeground() {
if (LinphoneService.isReady()) {
Log.i("[Notifications Manager] Stopping Service as foreground");
LinphoneService.instance().stopForeground(true);
mCurrentForegroundServiceNotification = 0;
}
}
public void removeForegroundServiceNotificationIfPossible() {
if (LinphoneService.isReady()) {
if (mCurrentForegroundServiceNotification == SERVICE_NOTIF_ID
&& !isServiceNotificationDisplayed()) {
Log.i(
"[Notifications Manager] Linphone has started after device boot, stopping Service as foreground");
stopForeground();
}
}
}
public void setCurrentlyDisplayedChatRoom(String address) {
mCurrentChatRoomAddress = address;
if (address != null) {
resetMessageNotifCount(address);
}
}
public void dismissMissedCallNotification() {
dismissNotification(MISSED_CALLS_NOTIF_ID);
}
public void sendNotification(int id, Notification notif) {
Log.i("[Notifications Manager] Notifying " + id);
mNM.notify(id, notif);
}
public void dismissNotification(int notifId) {
Log.i("[Notifications Manager] Dismissing " + notifId);
mNM.cancel(notifId);
}
public void resetMessageNotifCount(String address) {
Notifiable notif = mChatNotifMap.get(address);
if (notif != null) {
notif.resetMessages();
mNM.cancel(notif.getNotificationId());
}
}
public ChatMessageListenerStub getMessageListener() {
return mMessageListener;
}
private boolean isServiceNotificationDisplayed() {
return LinphonePreferences.instance().getServiceNotificationVisibility();
}
public String getSipUriForNotificationId(int notificationId) {
for (String addr : mChatNotifMap.keySet()) {
if (mChatNotifMap.get(addr).getNotificationId() == notificationId) {
return addr;
}
}
return null;
}
private void displayMessageNotificationFromNotifiable(
Notifiable notif, String remoteSipUri, String localSipUri) {
Intent notifIntent = new Intent(mContext, ChatActivity.class);
notifIntent.putExtra("RemoteSipUri", remoteSipUri);
notifIntent.putExtra("LocalSipUri", localSipUri);
addFlagsToIntent(notifIntent);
PendingIntent pendingIntent =
PendingIntent.getActivity(
mContext,
notif.getNotificationId(),
notifIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
NotifiableMessage lastNotifiable = notif.getMessages().get(notif.getMessages().size() - 1);
String from = lastNotifiable.getSender();
String message = lastNotifiable.getMessage();
Bitmap bm = lastNotifiable.getSenderBitmap();
if (notif.isGroup()) {
message =
mContext.getString(R.string.group_chat_notif)
.replace("%1", from)
.replace("%2", message);
from = notif.getGroupTitle();
}
Notification notification =
Compatibility.createMessageNotification(
mContext, notif, from, message, bm, pendingIntent);
sendNotification(notif.getNotificationId(), notification);
}
private void displayReplyMessageNotification(ChatMessage msg, Notifiable notif) {
if (msg == null || notif == null) return;
Log.i(
"[Notifications Manager] Updating message notification with reply for notif "
+ notif.getNotificationId());
NotifiableMessage notifMessage =
new NotifiableMessage(
msg.getTextContent(),
notif.getMyself(),
System.currentTimeMillis(),
null,
null);
notif.addMessage(notifMessage);
ChatRoom cr = msg.getChatRoom();
displayMessageNotificationFromNotifiable(
notif,
cr.getPeerAddress().asStringUriOnly(),
cr.getLocalAddress().asStringUriOnly());
}
public void displayGroupChatMessageNotification(
String subject,
String conferenceAddress,
String fromName,
Uri fromPictureUri,
String message,
Address localIdentity,
long timestamp,
Uri filePath,
String fileMime) {
Bitmap bm = ImageUtils.getRoundBitmapFromUri(mContext, fromPictureUri);
Notifiable notif = mChatNotifMap.get(conferenceAddress);
NotifiableMessage notifMessage =
new NotifiableMessage(message, fromName, timestamp, filePath, fileMime);
if (notif == null) {
notif = new Notifiable(mLastNotificationId);
mLastNotificationId += 1;
mChatNotifMap.put(conferenceAddress, notif);
}
Log.i("[Notifications Manager] Creating group chat message notifiable " + notif);
notifMessage.setSenderBitmap(bm);
notif.addMessage(notifMessage);
notif.setIsGroup(true);
notif.setGroupTitle(subject);
notif.setMyself(LinphoneUtils.getAddressDisplayName(localIdentity));
notif.setLocalIdentity(localIdentity.asString());
displayMessageNotificationFromNotifiable(
notif, conferenceAddress, localIdentity.asStringUriOnly());
}
public void displayMessageNotification(
String fromSipUri,
String fromName,
Uri fromPictureUri,
String message,
Address localIdentity,
long timestamp,
Uri filePath,
String fileMime) {
if (fromName == null) {
fromName = fromSipUri;
}
Bitmap bm = ImageUtils.getRoundBitmapFromUri(mContext, fromPictureUri);
Notifiable notif = mChatNotifMap.get(fromSipUri);
NotifiableMessage notifMessage =
new NotifiableMessage(message, fromName, timestamp, filePath, fileMime);
if (notif == null) {
notif = new Notifiable(mLastNotificationId);
mLastNotificationId += 1;
mChatNotifMap.put(fromSipUri, notif);
}
Log.i("[Notifications Manager] Creating chat message notifiable " + notif);
notifMessage.setSenderBitmap(bm);
notif.addMessage(notifMessage);
notif.setIsGroup(false);
notif.setMyself(LinphoneUtils.getAddressDisplayName(localIdentity));
notif.setLocalIdentity(localIdentity.asString());
displayMessageNotificationFromNotifiable(
notif, fromSipUri, localIdentity.asStringUriOnly());
}
public void displayMissedCallNotification(Call call) {
Intent missedCallNotifIntent = new Intent(mContext, HistoryActivity.class);
addFlagsToIntent(missedCallNotifIntent);
PendingIntent pendingIntent =
PendingIntent.getActivity(
mContext,
MISSED_CALLS_NOTIF_ID,
missedCallNotifIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
int missedCallCount = LinphoneManager.getCore().getMissedCallsCount();
String body;
if (missedCallCount > 1) {
body =
mContext.getString(R.string.missed_calls_notif_body)
.replace("%i", String.valueOf(missedCallCount));
Log.i("[Notifications Manager] Creating missed calls notification");
} else {
Address address = call.getRemoteAddress();
LinphoneContact c = ContactsManager.getInstance().findContactFromAddress(address);
if (c != null) {
body = c.getFullName();
} else {
body = address.getDisplayName();
if (body == null) {
body = address.asStringUriOnly();
}
}
Log.i("[Notifications Manager] Creating missed call notification");
}
Notification notif =
Compatibility.createMissedCallNotification(
mContext,
mContext.getString(R.string.missed_calls_notif_title),
body,
pendingIntent,
missedCallCount);
sendNotification(MISSED_CALLS_NOTIF_ID, notif);
}
public void displayCallNotification(Call call) {
if (call == null) return;
Class callNotifIntentClass = CallActivity.class;
if (call.getState() == Call.State.IncomingReceived
|| call.getState() == Call.State.IncomingEarlyMedia) {
callNotifIntentClass = CallIncomingActivity.class;
} else if (call.getState() == Call.State.OutgoingInit
|| call.getState() == Call.State.OutgoingProgress
|| call.getState() == Call.State.OutgoingRinging
|| call.getState() == Call.State.OutgoingEarlyMedia) {
callNotifIntentClass = CallOutgoingActivity.class;
}
Intent callNotifIntent = new Intent(mContext, callNotifIntentClass);
callNotifIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent =
PendingIntent.getActivity(
mContext, 0, callNotifIntent, PendingIntent.FLAG_UPDATE_CURRENT);
Address address = call.getRemoteAddress();
String addressAsString = address.asStringUriOnly();
Notifiable notif = mCallNotifMap.get(addressAsString);
if (notif == null) {
notif = new Notifiable(mLastNotificationId);
mLastNotificationId += 1;
mCallNotifMap.put(addressAsString, notif);
}
int notificationTextId;
int iconId;
switch (call.getState()) {
case Released:
case End:
if (mCurrentForegroundServiceNotification == notif.getNotificationId()) {
Log.i(
"[Notifications Manager] Call ended, stopping notification used to keep service alive");
// Call is released, remove service notification to allow for an other call to
// be service notification
stopForeground();
}
mNM.cancel(notif.getNotificationId());
mCallNotifMap.remove(addressAsString);
return;
case Paused:
case PausedByRemote:
case Pausing:
iconId = R.drawable.topbar_call_notification;
notificationTextId = R.string.incall_notif_paused;
break;
case IncomingEarlyMedia:
case IncomingReceived:
iconId = R.drawable.topbar_call_notification;
notificationTextId = R.string.incall_notif_incoming;
break;
case OutgoingEarlyMedia:
case OutgoingInit:
case OutgoingProgress:
case OutgoingRinging:
iconId = R.drawable.topbar_call_notification;
notificationTextId = R.string.incall_notif_outgoing;
break;
default:
if (call.getCurrentParams().videoEnabled()) {
iconId = R.drawable.topbar_videocall_notification;
notificationTextId = R.string.incall_notif_video;
} else {
iconId = R.drawable.topbar_call_notification;
notificationTextId = R.string.incall_notif_active;
}
break;
}
if (notif.getIconResourceId() == iconId
&& notif.getTextResourceId() == notificationTextId) {
// Notification hasn't changed, do not "update" it to avoid blinking
return;
} else if (notif.getTextResourceId() == R.string.incall_notif_incoming) {
// If previous notif was incoming call, as we will switch channels, dismiss it first
dismissNotification(notif.getNotificationId());
}
notif.setIconResourceId(iconId);
notif.setTextResourceId(notificationTextId);
Log.i(
"[Notifications Manager] Call notification notifiable is "
+ notif
+ ", pending intent "
+ callNotifIntentClass);
LinphoneContact contact = ContactsManager.getInstance().findContactFromAddress(address);
Uri pictureUri = contact != null ? contact.getThumbnailUri() : null;
Bitmap bm = ImageUtils.getRoundBitmapFromUri(mContext, pictureUri);
String name =
contact != null
? contact.getFullName()
: LinphoneUtils.getAddressDisplayName(address);
boolean isIncoming = callNotifIntentClass == CallIncomingActivity.class;
Notification notification;
if (isIncoming) {
notification =
Compatibility.createIncomingCallNotification(
mContext,
notif.getNotificationId(),
bm,
name,
addressAsString,
pendingIntent);
} else {
notification =
Compatibility.createInCallNotification(
mContext,
notif.getNotificationId(),
mContext.getString(notificationTextId),
iconId,
bm,
name,
pendingIntent);
}
// Don't use incoming call notification as foreground service notif !
if (!isServiceNotificationDisplayed() && !isIncoming) {
if (call.getCore().getCallsNb() == 0) {
Log.i(
"[Notifications Manager] Foreground service mode is disabled, stopping call notification used to keep it alive");
stopForeground();
} else {
if (mCurrentForegroundServiceNotification == 0) {
if (DeviceUtils.isAppUserRestricted(mContext)) {
Log.w(
"[Notifications Manager] App has been restricted, can't use call notification to keep service alive !");
sendNotification(notif.getNotificationId(), notification);
} else {
Log.i(
"[Notifications Manager] Foreground service mode is disabled, using call notification to keep it alive");
startForeground(notification, notif.getNotificationId());
}
} else {
sendNotification(notif.getNotificationId(), notification);
}
}
} else {
sendNotification(notif.getNotificationId(), notification);
}
}
public String getSipUriForCallNotificationId(int notificationId) {
for (String addr : mCallNotifMap.keySet()) {
if (mCallNotifMap.get(addr).getNotificationId() == notificationId) {
return addr;
}
}
return null;
}
private void createNotification(
ChatRoom cr,
LinphoneContact contact,
Address from,
String textMessage,
long time,
Uri file,
String mime) {
if (cr.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
if (contact != null) {
displayMessageNotification(
cr.getPeerAddress().asStringUriOnly(),
contact.getFullName(),
contact.getThumbnailUri(),
textMessage,
cr.getLocalAddress(),
time,
file,
mime);
} else {
displayMessageNotification(
cr.getPeerAddress().asStringUriOnly(),
from.getUsername(),
null,
textMessage,
cr.getLocalAddress(),
time,
file,
mime);
}
} else {
String subject = cr.getSubject();
if (contact != null) {
displayGroupChatMessageNotification(
subject,
cr.getPeerAddress().asStringUriOnly(),
contact.getFullName(),
contact.getThumbnailUri(),
textMessage,
cr.getLocalAddress(),
time,
file,
mime);
} else {
displayGroupChatMessageNotification(
subject,
cr.getPeerAddress().asStringUriOnly(),
from.getUsername(),
null,
textMessage,
cr.getLocalAddress(),
time,
file,
mime);
}
}
}
}