/** * Copyright (C) 2010-2012 Regis Montoya (aka r3gis - www.r3gis.fr) * This file is part of CSipSimple. * * CSipSimple 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. * If you own a pjsip commercial license you can also redistribute it * and/or modify it under the terms of the GNU Lesser General Public License * as an android library. * * CSipSimple 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 CSipSimple. If not, see <http://www.gnu.org/licenses/>. */ package com.csipsimple.service; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.graphics.Typeface; import android.provider.CallLog; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.StyleSpan; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; import com.csipsimple.R; import com.csipsimple.api.SipCallSession; import com.csipsimple.api.SipManager; import com.csipsimple.api.SipMessage; import com.csipsimple.api.SipProfile; import com.csipsimple.api.SipProfileState; import com.csipsimple.api.SipUri; import com.csipsimple.models.CallerInfo; import com.csipsimple.utils.Compatibility; import com.csipsimple.utils.CustomDistribution; import com.csipsimple.utils.Log; import com.csipsimple.widgets.RegistrationNotification; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; public class SipNotifications { private final NotificationManager notificationManager; private final Context context; private Builder inCallNotification; private Builder missedCallNotification; private Builder messageNotification; private Builder messageVoicemail; private boolean resolveContacts = true; public static final int REGISTER_NOTIF_ID = 1; public static final int CALL_NOTIF_ID = REGISTER_NOTIF_ID + 1; public static final int CALLLOG_NOTIF_ID = REGISTER_NOTIF_ID + 2; public static final int MESSAGE_NOTIF_ID = REGISTER_NOTIF_ID + 3; public static final int VOICEMAIL_NOTIF_ID = REGISTER_NOTIF_ID + 4; private static boolean isInit = false; public SipNotifications(Context aContext) { context = aContext; notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (!isInit) { cancelAll(); cancelCalls(); isInit = true; } if( ! Compatibility.isCompatible(9) ) { searchNotificationPrimaryText(aContext); } } private Integer notificationPrimaryTextColor = null; private static String TO_SEARCH = "Search"; // Retrieve notification textColor with android < 2.3 @SuppressWarnings("deprecation") private void searchNotificationPrimaryText(Context aContext) { try { Notification ntf = new Notification(); ntf.setLatestEventInfo(aContext, TO_SEARCH, "", null); LinearLayout group = new LinearLayout(aContext); ViewGroup event = (ViewGroup) ntf.contentView.apply(aContext, group); recurseSearchNotificationPrimaryText(event); group.removeAllViews(); } catch (Exception e) { Log.e(THIS_FILE, "Can't retrieve the color", e); } } private boolean recurseSearchNotificationPrimaryText(ViewGroup gp) { final int count = gp.getChildCount(); for (int i = 0; i < count; ++i) { if (gp.getChildAt(i) instanceof TextView){ final TextView text = (TextView) gp.getChildAt(i); final String szText = text.getText().toString(); if (TO_SEARCH.equals(szText)) { notificationPrimaryTextColor = text.getTextColors().getDefaultColor(); return true; } } else if (gp.getChildAt(i) instanceof ViewGroup) { if(recurseSearchNotificationPrimaryText((ViewGroup) gp.getChildAt(i))) { return true; } } } return false; } // Foreground api private static final Class<?>[] SET_FG_SIG = new Class[] { boolean.class }; private static final Class<?>[] START_FG_SIG = new Class[] { int.class, Notification.class }; private static final Class<?>[] STOP_FG_SIG = new Class[] { boolean.class }; private static final String THIS_FILE = "Notifications"; private Method mSetForeground; private Method mStartForeground; private Method mStopForeground; private Object[] mSetForegroundArgs = new Object[1]; private Object[] mStartForegroundArgs = new Object[2]; private Object[] mStopForegroundArgs = new Object[1]; private void invokeMethod(Method method, Object[] args) { try { method.invoke(context, args); } catch (InvocationTargetException e) { // Should not happen. Log.w(THIS_FILE, "Unable to invoke method", e); } catch (IllegalAccessException e) { // Should not happen. Log.w(THIS_FILE, "Unable to invoke method", e); } } /** * This is a wrapper around the new startForeground method, using the older * APIs if it is not available. */ private void startForegroundCompat(int id, Notification notification) { // If we have the new startForeground API, then use it. if (mStartForeground != null) { mStartForegroundArgs[0] = Integer.valueOf(id); mStartForegroundArgs[1] = notification; invokeMethod(mStartForeground, mStartForegroundArgs); return; } // Fall back on the old API. mSetForegroundArgs[0] = Boolean.TRUE; invokeMethod(mSetForeground, mSetForegroundArgs); notificationManager.notify(id, notification); } /** * This is a wrapper around the new stopForeground method, using the older * APIs if it is not available. */ private void stopForegroundCompat(int id) { // If we have the new stopForeground API, then use it. if (mStopForeground != null) { mStopForegroundArgs[0] = Boolean.TRUE; invokeMethod(mStopForeground, mStopForegroundArgs); return; } // Fall back on the old API. Note to cancel BEFORE changing the // foreground state, since we could be killed at that point. notificationManager.cancel(id); mSetForegroundArgs[0] = Boolean.FALSE; invokeMethod(mSetForeground, mSetForegroundArgs); } private boolean isServiceWrapper = false; public void onServiceCreate() { try { mStartForeground = context.getClass().getMethod("startForeground", START_FG_SIG); mStopForeground = context.getClass().getMethod("stopForeground", STOP_FG_SIG); isServiceWrapper = true; return; } catch (NoSuchMethodException e) { // Running on an older platform. mStartForeground = mStopForeground = null; } try { mSetForeground = context.getClass().getMethod("setForeground", SET_FG_SIG); } catch (NoSuchMethodException e) { throw new IllegalStateException("OS doesn't have Service.startForeground OR Service.setForeground!"); } isServiceWrapper = true; } public void onServiceDestroy() { // Make sure our notification is gone. cancelAll(); cancelCalls(); } // Announces // Register public synchronized void notifyRegisteredAccounts(ArrayList<SipProfileState> activeAccountsInfos, boolean showNumbers) { if (!isServiceWrapper) { Log.e(THIS_FILE, "Trying to create a service notification from outside the service"); return; } int icon = R.drawable.ic_stat_sipok; CharSequence tickerText = context.getString(R.string.service_ticker_registered_text); long when = System.currentTimeMillis(); Builder nb = new NotificationCompat.Builder(context); nb.setSmallIcon(icon); nb.setTicker(tickerText); nb.setWhen(when); Intent notificationIntent = new Intent(SipManager.ACTION_SIP_DIALER); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); RegistrationNotification contentView = new RegistrationNotification(context.getPackageName()); contentView.clearRegistrations(); if(!Compatibility.isCompatible(9)) { contentView.setTextsColor(notificationPrimaryTextColor); } contentView.addAccountInfos(context, activeAccountsInfos); // notification.setLatestEventInfo(context, contentTitle, // contentText, contentIntent); nb.setOngoing(true); nb.setOnlyAlertOnce(true); nb.setContentIntent(contentIntent); nb.setContent(contentView); Notification notification = nb.build(); notification.flags |= Notification.FLAG_NO_CLEAR; // We have to re-write content view because getNotification setLatestEventInfo implicitly notification.contentView = contentView; if (showNumbers) { // This only affects android 2.3 and lower notification.number = activeAccountsInfos.size(); } startForegroundCompat(REGISTER_NOTIF_ID, notification); } /** * Format the remote contact name for the call info * @param callInfo the callinfo to format * @return the name to display for the contact */ private String formatRemoteContactString(String remoteContact) { String formattedRemoteContact = remoteContact; if(resolveContacts) { CallerInfo callerInfo = CallerInfo.getCallerInfoFromSipUri(context, formattedRemoteContact); if (callerInfo != null && callerInfo.contactExists) { StringBuilder remoteInfo = new StringBuilder(); remoteInfo.append(callerInfo.name); remoteInfo.append(" <"); remoteInfo.append(SipUri.getCanonicalSipContact(remoteContact)); remoteInfo.append(">"); formattedRemoteContact = remoteInfo.toString(); } } return formattedRemoteContact; } /** * Format the notification title for a call info * @param title * @param callInfo * @return */ private String formatNotificationTitle(int title, long accId) { StringBuilder notifTitle = new StringBuilder(context.getText(title)); SipProfile acc = SipProfile.getProfileFromDbId(context, accId, new String[] {SipProfile.FIELD_DISPLAY_NAME}); if ((acc != null) && !TextUtils.isEmpty(acc.display_name)) { notifTitle.append(" - "); notifTitle.append(acc.display_name); } return notifTitle.toString(); } // Calls public void showNotificationForCall(SipCallSession callInfo) { // This is the pending call notification // int icon = R.drawable.ic_incall_ongoing; @SuppressWarnings("deprecation") int icon = android.R.drawable.stat_sys_phone_call; CharSequence tickerText = context.getText(R.string.ongoing_call); long when = System.currentTimeMillis(); if(inCallNotification == null) { inCallNotification = new NotificationCompat.Builder(context); inCallNotification.setSmallIcon(icon); inCallNotification.setTicker(tickerText); inCallNotification.setWhen(when); inCallNotification.setOngoing(true); } Intent notificationIntent = SipService.buildCallUiIntent(context, callInfo); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); inCallNotification.setContentTitle(formatNotificationTitle(R.string.ongoing_call, callInfo.getAccId())); inCallNotification.setContentText(formatRemoteContactString(callInfo.getRemoteContact())); inCallNotification.setContentIntent(contentIntent); Notification notification = inCallNotification.build(); notification.flags |= Notification.FLAG_NO_CLEAR; notificationManager.notify(CALL_NOTIF_ID, notification); } public void showNotificationForMissedCall(ContentValues callLog) { int icon = android.R.drawable.stat_notify_missed_call; CharSequence tickerText = context.getText(R.string.missed_call); long when = System.currentTimeMillis(); if (missedCallNotification == null) { missedCallNotification = new NotificationCompat.Builder(context); missedCallNotification.setSmallIcon(icon); missedCallNotification.setTicker(tickerText); missedCallNotification.setWhen(when); missedCallNotification.setOnlyAlertOnce(true); missedCallNotification.setAutoCancel(true); missedCallNotification.setDefaults(Notification.DEFAULT_ALL); } Intent notificationIntent = new Intent(SipManager.ACTION_SIP_CALLLOG); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); String remoteContact = callLog.getAsString(CallLog.Calls.NUMBER); long accId = callLog.getAsLong(SipManager.CALLLOG_PROFILE_ID_FIELD); missedCallNotification.setContentTitle(formatNotificationTitle(R.string.missed_call, accId)); missedCallNotification.setContentText(formatRemoteContactString(remoteContact)); missedCallNotification.setContentIntent(contentIntent); notificationManager.notify(CALLLOG_NOTIF_ID, missedCallNotification.build()); } public void showNotificationForMessage(SipMessage msg) { if (!CustomDistribution.supportMessaging()) { return; } // CharSequence tickerText = context.getText(R.string.instance_message); if (!msg.getFrom().equalsIgnoreCase(viewingRemoteFrom)) { String from = formatRemoteContactString(msg.getFullFrom()); if(from.equalsIgnoreCase(msg.getFullFrom()) && !from.equals(msg.getDisplayName())) { from = msg.getDisplayName() + " " + from; } CharSequence tickerText = buildTickerMessage(context, from, msg.getBody()); if (messageNotification == null) { messageNotification = new NotificationCompat.Builder(context); messageNotification.setSmallIcon(SipUri.isPhoneNumber(from) ? R.drawable.stat_notify_sms : android.R.drawable.stat_notify_chat); messageNotification.setTicker(tickerText); messageNotification.setWhen(System.currentTimeMillis()); messageNotification.setDefaults(Notification.DEFAULT_ALL); messageNotification.setAutoCancel(true); messageNotification.setOnlyAlertOnce(true); } Intent notificationIntent = new Intent(SipManager.ACTION_SIP_MESSAGES); notificationIntent.putExtra(SipMessage.FIELD_FROM, msg.getFrom()); notificationIntent.putExtra(SipMessage.FIELD_BODY, msg.getBody()); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); messageNotification.setContentTitle(from); messageNotification.setContentText(msg.getBody()); messageNotification.setContentIntent(contentIntent); notificationManager.notify(MESSAGE_NOTIF_ID, messageNotification.build()); } } public void showNotificationForVoiceMail(SipProfile acc, int numberOfMessages) { if (messageVoicemail == null) { messageVoicemail = new NotificationCompat.Builder(context); messageVoicemail.setSmallIcon(android.R.drawable.stat_notify_voicemail); messageVoicemail.setTicker(context.getString(R.string.voice_mail)); messageVoicemail.setWhen(System.currentTimeMillis()); messageVoicemail.setDefaults(Notification.DEFAULT_ALL); messageVoicemail.setAutoCancel(true); messageVoicemail.setOnlyAlertOnce(true); } PendingIntent contentIntent = null; Intent notificationIntent; if (acc != null && !TextUtils.isEmpty(acc.vm_nbr) && acc.vm_nbr != "null") { notificationIntent = new Intent(Intent.ACTION_CALL); notificationIntent.setData(SipUri.forgeSipUri(SipManager.PROTOCOL_CSIP, acc.vm_nbr + "@" + acc.getDefaultDomain())); notificationIntent.putExtra(SipProfile.FIELD_ACC_ID, acc.id); } else { notificationIntent = new Intent(SipManager.ACTION_SIP_DIALER); } notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); contentIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); String messageText = ""; if (acc != null) { messageText += acc.getProfileName(); if(numberOfMessages>0) { messageText += " : "; } } if(numberOfMessages > 0) { messageText += Integer.toString(numberOfMessages); } messageVoicemail.setContentTitle(context.getString(R.string.voice_mail)); messageVoicemail.setContentText(messageText); if (contentIntent != null) { messageVoicemail.setContentIntent(contentIntent); notificationManager.notify(VOICEMAIL_NOTIF_ID, messageVoicemail.build()); } } private static String viewingRemoteFrom = null; public void setViewingMessageFrom(String remoteFrom) { viewingRemoteFrom = remoteFrom; } protected static CharSequence buildTickerMessage(Context context, String address, String body) { String displayAddress = address; StringBuilder buf = new StringBuilder(displayAddress == null ? "" : displayAddress.replace('\n', ' ').replace('\r', ' ')); buf.append(':').append(' '); int offset = buf.length(); if (!TextUtils.isEmpty(body)) { body = body.replace('\n', ' ').replace('\r', ' '); buf.append(body); } SpannableString spanText = new SpannableString(buf.toString()); spanText.setSpan(new StyleSpan(Typeface.BOLD), 0, offset, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); return spanText; } // Cancels public final void cancelRegisters() { if (!isServiceWrapper) { Log.e(THIS_FILE, "Trying to cancel a service notification from outside the service"); return; } stopForegroundCompat(REGISTER_NOTIF_ID); } public final void cancelCalls() { notificationManager.cancel(CALL_NOTIF_ID); } public final void cancelMissedCalls() { notificationManager.cancel(CALLLOG_NOTIF_ID); } public final void cancelMessages() { notificationManager.cancel(MESSAGE_NOTIF_ID); } public final void cancelVoicemails() { notificationManager.cancel(VOICEMAIL_NOTIF_ID); } public final void cancelAll() { // Do not cancel calls notification since it's possible that there is // still an ongoing call. if (isServiceWrapper) { cancelRegisters(); } cancelMessages(); cancelMissedCalls(); cancelVoicemails(); } }