gt:         body = SpanUtil.italic(message, italicLength);
context: /*
   * Copyright (C) 2011 Whisper Systems
   *
   * 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 <http://www.gnu.org/licenses/>.
   */
  package org.thoughtcrime.securesms.notifications;
  
  import android.annotation.SuppressLint;
  import android.app.AlarmManager;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.database.Cursor;
  import android.media.AudioAttributes;
  import android.media.AudioManager;
  import android.media.Ringtone;
  import android.media.RingtoneManager;
  import android.net.Uri;
  import android.os.AsyncTask;
  import android.os.Build;
  import android.service.notification.StatusBarNotification;
  import android.support.annotation.NonNull;
  import android.support.v4.app.NotificationManagerCompat;
  import android.text.TextUtils;
  import android.util.Log;
  
  import org.thoughtcrime.securesms.ConversationActivity;
  import org.thoughtcrime.securesms.R;
  import org.thoughtcrime.securesms.contactshare.ContactUtil;
  import org.thoughtcrime.securesms.contactshare.Contact;
  import org.thoughtcrime.securesms.database.DatabaseFactory;
  import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
  import org.thoughtcrime.securesms.database.MmsSmsDatabase;
  import org.thoughtcrime.securesms.database.ThreadDatabase;
  import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
  import org.thoughtcrime.securesms.database.model.MessageRecord;
  import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
  import org.thoughtcrime.securesms.mms.SlideDeck;
  import org.thoughtcrime.securesms.recipients.Recipient;
  import org.thoughtcrime.securesms.service.KeyCachingService;
  import org.thoughtcrime.securesms.service.MessageRetrievalService;
  import org.thoughtcrime.securesms.util.ServiceUtil;
  import org.thoughtcrime.securesms.util.SpanUtil;
  import org.thoughtcrime.securesms.util.TextSecurePreferences;
  import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder;
  import org.whispersystems.signalservice.internal.util.Util;
  
  import java.util.HashSet;
  import java.util.List;
  import java.util.ListIterator;
  import java.util.Set;
  import java.util.concurrent.Executor;
  import java.util.concurrent.Executors;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.atomic.AtomicBoolean;
  
  import me.leolin.shortcutbadger.ShortcutBadger;
  
  
  /**
   * Handles posting system notifications for new messages.
   *
   *
   * @author Moxie Marlinspike
   */
  
  public class MessageNotifier {
  
    private static final String TAG = MessageNotifier.class.getSimpleName();
  
    public static final  String EXTRA_REMOTE_REPLY = "extra_remote_reply";
  
    private static final  int   SUMMARY_NOTIFICATION_ID   = 1338;
    private static final int    PENDING_MESSAGES_ID       = 1111;
    private static final String NOTIFICATION_GROUP        = "messages";
    private static final long   MIN_AUDIBLE_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(2);
    private static final long   DESKTOP_ACTIVITY_PERIOD   = TimeUnit.MINUTES.toMillis(1);
  
    private volatile static       long               visibleThread                = -1;
    private volatile static       long               lastDesktopActivityTimestamp = -1;
    private volatile static       long               lastAudibleNotification      = -1;
    private          static final CancelableExecutor executor                     = new CancelableExecutor();
  
    public static void setVisibleThread(long threadId) {
      visibleThread = threadId;
    }
  
    public static void setLastDesktopActivityTimestamp(long timestamp) {
      lastDesktopActivityTimestamp = timestamp;
    }
  
    public static void notifyMessageDeliveryFailed(Context context, Recipient recipient, long threadId) {
      if (visibleThread == threadId) {
        sendInThreadNotification(context, recipient);
      } else {
        Intent intent = new Intent(context, ConversationActivity.class);
        intent.putExtra(ConversationActivity.ADDRESS_EXTRA, recipient.getAddress());
        intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
        intent.setData((Uri.parse("custom://" + System.currentTimeMillis())));
  
        FailedNotificationBuilder builder = new FailedNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context), intent);
        ((NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE))
          .notify((int)threadId, builder.build());
      }
    }
  
    public static void notifyMessagesPending(Context context) {
      if (!TextSecurePreferences.isNotificationsEnabled(context)) {
        return;
      }
  
      PendingMessageNotificationBuilder builder = new PendingMessageNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context));
      ServiceUtil.getNotificationManager(context).notify(PENDING_MESSAGES_ID, builder.build());
    }
  
    public static void cancelDelayedNotifications() {
      executor.cancel();
    }
  
    private static void cancelActiveNotifications(@NonNull Context context) {
      NotificationManager notifications = ServiceUtil.getNotificationManager(context);
      notifications.cancel(SUMMARY_NOTIFICATION_ID);
  
      if (Build.VERSION.SDK_INT >= 23) {
        try {
          StatusBarNotification[] activeNotifications = notifications.getActiveNotifications();
  
          for (StatusBarNotification activeNotification : activeNotifications) {
            if (activeNotification.getId() != CallNotificationBuilder.WEBRTC_NOTIFICATION) {
              notifications.cancel(activeNotification.getId());
            }
          }
        } catch (Throwable e) {
          // XXX Appears to be a ROM bug, see #6043
          Log.w(TAG, e);
          notifications.cancelAll();
        }
      }
    }
  
    private static void cancelOrphanedNotifications(@NonNull Context context, NotificationState notificationState) {
      if (Build.VERSION.SDK_INT >= 23) {
        try {
          NotificationManager     notifications       = ServiceUtil.getNotificationManager(context);
          StatusBarNotification[] activeNotifications = notifications.getActiveNotifications();
  
          for (StatusBarNotification notification : activeNotifications) {
            boolean validNotification = false;
  
            if (notification.getId() != SUMMARY_NOTIFICATION_ID &&
                notification.getId() != CallNotificationBuilder.WEBRTC_NOTIFICATION   &&
                notification.getId() != KeyCachingService.SERVICE_RUNNING_ID          &&
                notification.getId() != MessageRetrievalService.FOREGROUND_ID         &&
                notification.getId() != PENDING_MESSAGES_ID)
            {
              for (NotificationItem item : notificationState.getNotifications()) {
                if (notification.getId() == (SUMMARY_NOTIFICATION_ID + item.getThreadId())) {
                  validNotification = true;
                  break;
                }
              }
  
              if (!validNotification) {
                notifications.cancel(notification.getId());
              }
            }
          }
        } catch (Throwable e) {
          // XXX Android ROM Bug, see #6043
          Log.w(TAG, e);
        }
      }
    }
  
    public static void updateNotification(@NonNull Context context) {
      if (!TextSecurePreferences.isNotificationsEnabled(context)) {
        return;
      }
  
      updateNotification(context, false, 0);
    }
  
    public static void updateNotification(@NonNull Context context, long threadId)
    {
      if (System.currentTimeMillis() - lastDesktopActivityTimestamp < DESKTOP_ACTIVITY_PERIOD) {
        Log.w(TAG, "Scheduling delayed notification...");
        executor.execute(new DelayedNotification(context, threadId));
      } else {
        updateNotification(context, threadId, true);
      }
    }
  
    public static void updateNotification(@NonNull  Context context,
                                          long      threadId,
                                          boolean   signal)
    {
      boolean    isVisible  = visibleThread == threadId;
  
      ThreadDatabase threads    = DatabaseFactory.getThreadDatabase(context);
      Recipient      recipients = DatabaseFactory.getThreadDatabase(context)
                                                 .getRecipientForThreadId(threadId);
  
      if (isVisible) {
        List<MarkedMessageInfo> messageIds = threads.setRead(threadId, false);
        MarkReadReceiver.process(context, messageIds);
      }
  
      if (!TextSecurePreferences.isNotificationsEnabled(context) ||
          (recipients != null && recipients.isMuted()))
      {
        return;
      }
  
      if (isVisible) {
        sendInThreadNotification(context, threads.getRecipientForThreadId(threadId));
      } else {
        updateNotification(context, signal, 0);
      }
    }
  
    private static void updateNotification(@NonNull Context context,
                                           boolean signal,
                                           int     reminderCount)
    {
      Cursor telcoCursor = null;
      Cursor pushCursor  = null;
  
      try {
        telcoCursor = DatabaseFactory.getMmsSmsDatabase(context).getUnread();
        pushCursor  = DatabaseFactory.getPushDatabase(context).getPending();
  
        if ((telcoCursor == null || telcoCursor.isAfterLast()) &&
            (pushCursor == null || pushCursor.isAfterLast()))
        {
          cancelActiveNotifications(context);
          updateBadge(context, 0);
          clearReminder(context);
          return;
        }
  
        NotificationState notificationState = constructNotificationState(context, telcoCursor);
  
        if (signal && (System.currentTimeMillis() - lastAudibleNotification) < MIN_AUDIBLE_PERIOD_MILLIS) {
          signal = false;
        } else if (signal) {
          lastAudibleNotification = System.currentTimeMillis();
        }
  
        if (notificationState.hasMultipleThreads()) {
          if (Build.VERSION.SDK_INT >= 23) {
            for (long threadId : notificationState.getThreads()) {
              sendSingleThreadNotification(context, new NotificationState(notificationState.getNotificationsForThread(threadId)), false, true);
            }
          }
  
          sendMultipleThreadNotification(context, notificationState, signal);
        } else {
          sendSingleThreadNotification(context, notificationState, signal, false);
        }
  
        cancelOrphanedNotifications(context, notificationState);
        updateBadge(context, notificationState.getMessageCount());
  
        if (signal) {
          scheduleReminder(context, reminderCount);
        }
      } finally {
        if (telcoCursor != null) telcoCursor.close();
        if (pushCursor != null)  pushCursor.close();
      }
    }
  
    private static void sendSingleThreadNotification(@NonNull  Context context,
                                                     @NonNull  NotificationState notificationState,
                                                     boolean signal, boolean bundled)
    {
      if (notificationState.getNotifications().isEmpty()) {
        if (!bundled) cancelActiveNotifications(context);
        return;
      }
  
      SingleRecipientNotificationBuilder builder        = new SingleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context));
      List<NotificationItem>             notifications  = notificationState.getNotifications();
      Recipient                          recipient      = notifications.get(0).getRecipient();
      int                                notificationId = (int) (SUMMARY_NOTIFICATION_ID + (bundled ? notifications.get(0).getThreadId() : 0));
  
  
      builder.setThread(notifications.get(0).getRecipient());
      builder.setMessageCount(notificationState.getMessageCount());
      builder.setPrimaryMessageBody(recipient, notifications.get(0).getIndividualRecipient(),
                                    notifications.get(0).getText(), notifications.get(0).getSlideDeck());
      builder.setContentIntent(notifications.get(0).getPendingIntent(context));
      builder.setGroup(NOTIFICATION_GROUP);
      builder.setDeleteIntent(notificationState.getDeleteIntent(context));
  
      long timestamp = notifications.get(0).getTimestamp();
      if (timestamp != 0) builder.setWhen(timestamp);
  
      builder.addActions(notificationState.getMarkAsReadIntent(context, notificationId),
                         notificationState.getQuickReplyIntent(context, notifications.get(0).getRecipient()),
                         notificationState.getRemoteReplyIntent(context, notifications.get(0).getRecipient()));
  
      builder.addAndroidAutoAction(notificationState.getAndroidAutoReplyIntent(context, notifications.get(0).getRecipient()),
                                   notificationState.getAndroidAutoHeardIntent(context, notificationId), notifications.get(0).getTimestamp());
  
      ListIterator<NotificationItem> iterator = notifications.listIterator(notifications.size());
  
      while(iterator.hasPrevious()) {
        NotificationItem item = iterator.previous();
        builder.addMessageBody(item.getRecipient(), item.getIndividualRecipient(), item.getText());
      }
  
      if (signal) {
        builder.setAlarms(notificationState.getRingtone(), notificationState.getVibrate());
        builder.setTicker(notifications.get(0).getIndividualRecipient(),
                          notifications.get(0).getText());
      }
  
      if (!bundled) {
        builder.setGroupSummary(true);
      }
  
      NotificationManagerCompat.from(context).notify(notificationId, builder.build());
    }
  
    private static void sendMultipleThreadNotification(@NonNull  Context context,
                                                       @NonNull  NotificationState notificationState,
                                                       boolean signal)
    {
      MultipleRecipientNotificationBuilder builder       = new MultipleRecipientNotificationBuilder(context, TextSecurePreferences.getNotificationPrivacy(context));
      List<NotificationItem>               notifications = notificationState.getNotifications();
  
      builder.setMessageCount(notificationState.getMessageCount(), notificationState.getThreadCount());
      builder.setMostRecentSender(notifications.get(0).getIndividualRecipient());
      builder.setGroup(NOTIFICATION_GROUP);
      builder.setDeleteIntent(notificationState.getDeleteIntent(context));
  
      long timestamp = notifications.get(0).getTimestamp();
      if (timestamp != 0) builder.setWhen(timestamp);
  
      builder.addActions(notificationState.getMarkAsReadIntent(context, SUMMARY_NOTIFICATION_ID));
  
      ListIterator<NotificationItem> iterator = notifications.listIterator(notifications.size());
  
      while(iterator.hasPrevious()) {
        NotificationItem item = iterator.previous();
        builder.addMessageBody(item.getIndividualRecipient(), item.getText());
      }
  
      if (signal) {
        builder.setAlarms(notificationState.getRingtone(), notificationState.getVibrate());
        builder.setTicker(notifications.get(0).getIndividualRecipient(),
                          notifications.get(0).getText());
      }
  
      NotificationManagerCompat.from(context).notify(SUMMARY_NOTIFICATION_ID, builder.build());
    }
  
    private static void sendInThreadNotification(Context context, Recipient recipient) {
      if (!TextSecurePreferences.isInThreadNotifications(context) ||
          ServiceUtil.getAudioManager(context).getRingerMode() != AudioManager.RINGER_MODE_NORMAL)
      {
        return;
      }
  
      Uri uri = recipient != null ? recipient.resolve().getMessageRingtone() : null;
  
      if (uri == null) {
        uri = TextSecurePreferences.getNotificationRingtone(context);
      }
  
      if (uri.toString().isEmpty()) {
        Log.d(TAG, "ringtone uri is empty");
        return;
      }
  
      Ringtone ringtone = RingtoneManager.getRingtone(context, uri);
  
      if (ringtone == null) {
        Log.w(TAG, "ringtone is null");
        return;
      }
  
      if (Build.VERSION.SDK_INT >= 21) {
        ringtone.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
                                                                 .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT)
                                                                 .build());
      } else {
        ringtone.setStreamType(AudioManager.STREAM_NOTIFICATION);
      }
  
      ringtone.play();
    }
  
    private static NotificationState constructNotificationState(@NonNull  Context context,
                                                                @NonNull  Cursor cursor)
    {
      NotificationState     notificationState = new NotificationState();
      MmsSmsDatabase.Reader reader            = DatabaseFactory.getMmsSmsDatabase(context).readerFor(cursor);
  
      MessageRecord record;
  
      while ((record = reader.getNext()) != null) {
        long         id                    = record.getId();
        boolean      mms                   = record.isMms() || record.isMmsNotification();
        Recipient    recipient             = record.getIndividualRecipient();
        Recipient    conversationRecipient = record.getRecipient();
        long         threadId              = record.getThreadId();
        CharSequence body                  = record.getDisplayBody();
        Recipient    threadRecipients      = null;
        SlideDeck    slideDeck             = null;
        long         timestamp             = record.getTimestamp();
  
  
        if (threadId != -1) {
          threadRecipients = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
        }
  
        if (KeyCachingService.isLocked(context)) {
          body = SpanUtil.italic(context.getString(R.string.MessageNotifier_locked_message));
        } else if (record.isMms() && !((MmsMessageRecord) record).getSharedContacts().isEmpty()) {
          Contact contact = ((MmsMessageRecord) record).getSharedContacts().get(0);
          body = ContactUtil.getStringSummary(context, contact);
        } else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
          body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
          slideDeck = ((MediaMmsMessageRecord)record).getSlideDeck();
        } else if (record.isMms() && !record.isMmsNotification() && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
          String message      = context.getString(R.string.MessageNotifier_media_message_with_text, body);
          int    italicLength = message.length() - body.length();
  