/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.dialer.app.calllog;

import static com.android.dialer.app.DevicePolicyResources.NOTIFICATION_MISSED_WORK_CALL_TITLE;

import android.app.BroadcastOptions;
import android.app.Notification;
import android.app.Notification.Builder;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.provider.CallLog.Calls;
import android.service.notification.StatusBarNotification;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
import android.support.v4.os.BuildCompat;
import android.support.v4.os.UserManagerCompat;
import android.support.v4.util.Pair;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telephony.PhoneNumberUtils;
import android.text.BidiFormatter;
import android.text.TextDirectionHeuristics;
import android.text.TextUtils;
import android.util.ArraySet;

import com.android.contacts.common.ContactsUtils;
import com.android.dialer.app.MainComponent;
import com.android.dialer.app.R;
import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall;
import com.android.dialer.app.contactinfo.ContactPhotoLoader;
import com.android.dialer.callintent.CallInitiationType;
import com.android.dialer.callintent.CallIntentBuilder;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DialerExecutor.Worker;
import com.android.dialer.compat.android.provider.VoicemailCompat;
import com.android.dialer.duo.DuoComponent;
import com.android.dialer.enrichedcall.FuzzyPhoneNumberMatcher;
import com.android.dialer.notification.DialerNotificationManager;
import com.android.dialer.notification.NotificationChannelId;
import com.android.dialer.notification.missedcalls.MissedCallConstants;
import com.android.dialer.notification.missedcalls.MissedCallNotificationCanceller;
import com.android.dialer.notification.missedcalls.MissedCallNotificationTags;
import com.android.dialer.phonenumbercache.ContactInfo;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
import com.android.dialer.precall.PreCall;
import com.android.dialer.theme.base.ThemeComponent;
import com.android.dialer.util.DialerUtils;
import com.android.dialer.util.IntentUtil;

import java.util.Iterator;
import java.util.List;
import java.util.Set;

/** Creates a notification for calls that the user missed (neither answered nor rejected). */
public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {

  private final Context context;
  private final CallLogNotificationsQueryHelper callLogNotificationsQueryHelper;

  @VisibleForTesting
  MissedCallNotifier(
      Context context, CallLogNotificationsQueryHelper callLogNotificationsQueryHelper) {
    this.context = context;
    this.callLogNotificationsQueryHelper = callLogNotificationsQueryHelper;
  }

  public static MissedCallNotifier getInstance(Context context) {
    return new MissedCallNotifier(context, CallLogNotificationsQueryHelper.getInstance(context));
  }

  @Nullable
  @Override
  public Void doInBackground(@Nullable Pair<Integer, String> input) throws Throwable {
    updateMissedCallNotification(input.first, input.second);
    return null;
  }

  /**
   * Update missed call notifications from the call log. Accepts default information in case call
   * log cannot be accessed.
   *
   * @param count the number of missed calls to display if call log cannot be accessed. May be
   *     {@link CallLogNotificationsService#UNKNOWN_MISSED_CALL_COUNT} if unknown.
   * @param number the phone number of the most recent call to display if the call log cannot be
   *     accessed. May be null if unknown.
   */
  @VisibleForTesting
  @WorkerThread
  void updateMissedCallNotification(int count, @Nullable String number) {
    LogUtil.enterBlock("MissedCallNotifier.updateMissedCallNotification");

    final String titleText;
    CharSequence expandedText; // The text in the notification's line 1 and 2.

    List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls();

    removeSelfManagedCalls(newCalls);

    if ((newCalls != null && newCalls.isEmpty()) || count == 0) {
      // No calls to notify about: clear the notification.
      CallLogNotificationsQueryHelper.markAllMissedCallsInCallLogAsRead(context);
      MissedCallNotificationCanceller.cancelAll(context);
      return;
    }

    if (newCalls != null) {
      if (count != CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT
          && count != newCalls.size()) {
        LogUtil.w(
            "MissedCallNotifier.updateMissedCallNotification",
            "Call count does not match call log count."
                + " count: "
                + count
                + " newCalls.size(): "
                + newCalls.size());
      }
      count = newCalls.size();
    }

    if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) {
      // If the intent did not contain a count, and we are unable to get a count from the
      // call log, then no notification can be shown.
      LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "unknown missed call count");
      return;
    }

    Notification.Builder groupSummary = createNotificationBuilder();
    boolean useCallList = newCalls != null;

    if (count == 1) {
      LogUtil.i(
          "MissedCallNotifier.updateMissedCallNotification",
          "1 missed call, looking up contact info");
      NewCall call =
          useCallList
              ? newCalls.get(0)
              : new NewCall(
                  null,
                  null,
                  number,
                  Calls.PRESENTATION_ALLOWED,
                  null,
                  null,
                  null,
                  null,
                  System.currentTimeMillis(),
                  VoicemailCompat.TRANSCRIPTION_NOT_STARTED);

      // TODO: look up caller ID that is not in contacts.
      ContactInfo contactInfo =
          callLogNotificationsQueryHelper.getContactInfo(
              call.number, call.numberPresentation, call.countryIso);
      if (contactInfo.userType == ContactsUtils.USER_TYPE_WORK) {
        titleText = context.getSystemService(DevicePolicyManager.class).getResources().getString(
                NOTIFICATION_MISSED_WORK_CALL_TITLE,
                () -> context.getString(R.string.notification_missedWorkCallTitle));
      } else {
        titleText = context.getString(R.string.notification_missedCallTitle);
      }

      if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber)
          || TextUtils.equals(contactInfo.name, contactInfo.number)) {
        expandedText =
            PhoneNumberUtils.createTtsSpannable(
                BidiFormatter.getInstance()
                    .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR));
      } else {
        expandedText = contactInfo.name;
      }

      ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo);
      Bitmap photoIcon = loader.loadPhotoIcon();
      if (photoIcon != null) {
        groupSummary.setLargeIcon(photoIcon);
      }
    } else {
      titleText = context.getString(R.string.notification_missedCallsTitle);
      expandedText = context.getString(R.string.notification_missedCallsMsg, count);
    }

    LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "preparing notification");

    // Create a public viewable version of the notification, suitable for display when sensitive
    // notification content is hidden.
    Notification.Builder publicSummaryBuilder = createNotificationBuilder();
    publicSummaryBuilder
        .setContentTitle(titleText)
        .setContentIntent(createCallLogPendingIntent())
        .setDeleteIntent(
            CallLogNotificationsService.createCancelAllMissedCallsPendingIntent(context));

    // Create the notification summary suitable for display when sensitive information is showing.
    groupSummary
        .setContentTitle(titleText)
        .setContentText(expandedText)
        .setContentIntent(createCallLogPendingIntent())
        .setDeleteIntent(
            CallLogNotificationsService.createCancelAllMissedCallsPendingIntent(context))
        .setGroupSummary(useCallList)
        .setOnlyAlertOnce(useCallList)
        .setPublicVersion(publicSummaryBuilder.build());
    if (BuildCompat.isAtLeastO()) {
      groupSummary.setChannelId(NotificationChannelId.MISSED_CALL);
    }

    Notification notification = groupSummary.build();
    configureLedOnNotification(notification);

    LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification");
    DialerNotificationManager.notify(
        context,
        MissedCallConstants.GROUP_SUMMARY_NOTIFICATION_TAG,
        MissedCallConstants.NOTIFICATION_ID,
        notification);

    if (useCallList) {
      // Do not repost active notifications to prevent erasing post call notes.
      Set<String> activeAndThrottledTags = new ArraySet<>();
      for (StatusBarNotification activeNotification :
          DialerNotificationManager.getActiveNotifications(context)) {
        activeAndThrottledTags.add(activeNotification.getTag());
      }
      // Do not repost throttled notifications
      for (StatusBarNotification throttledNotification :
          DialerNotificationManager.getThrottledNotificationSet()) {
        activeAndThrottledTags.add(throttledNotification.getTag());
      }

      for (NewCall call : newCalls) {
        String callTag = getNotificationTagForCall(call);
        if (!activeAndThrottledTags.contains(callTag)) {
          DialerNotificationManager.notify(
              context,
              callTag,
              MissedCallConstants.NOTIFICATION_ID,
              getNotificationForCall(call, null));
        }
      }
    }
  }

  /**
   * Remove self-managed calls from {@code newCalls}. If a {@link PhoneAccount} declared it is
   * {@link PhoneAccount#CAPABILITY_SELF_MANAGED}, it should handle the in call UI and notifications
   * itself, but might still write to call log with {@link
   * PhoneAccount#EXTRA_LOG_SELF_MANAGED_CALLS}.
   */
  private void removeSelfManagedCalls(@Nullable List<NewCall> newCalls) {
    if (newCalls == null) {
      return;
    }

    TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
    Iterator<NewCall> iterator = newCalls.iterator();
    while (iterator.hasNext()) {
      NewCall call = iterator.next();
      if (call.accountComponentName == null || call.accountId == null) {
        continue;
      }
      ComponentName componentName = ComponentName.unflattenFromString(call.accountComponentName);
      if (componentName == null) {
        continue;
      }
      PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(componentName, call.accountId);
      PhoneAccount phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle);
      if (phoneAccount == null) {
        continue;
      }
      if (DuoComponent.get(context).getDuo().isDuoAccount(phoneAccountHandle)) {
        iterator.remove();
        continue;
      }
      if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
        LogUtil.i(
            "MissedCallNotifier.removeSelfManagedCalls",
            "ignoring self-managed call " + call.callsUri);
        iterator.remove();
      }
    }
  }

  private static String getNotificationTagForCall(@NonNull NewCall call) {
    return MissedCallNotificationTags.getNotificationTagForCallUri(call.callsUri);
  }

  @WorkerThread
  public void insertPostCallNotification(@NonNull String number, @NonNull String note) {
    Assert.isWorkerThread();
    LogUtil.enterBlock("MissedCallNotifier.insertPostCallNotification");
    List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls();
    if (newCalls != null && !newCalls.isEmpty()) {
      for (NewCall call : newCalls) {
        if (FuzzyPhoneNumberMatcher.matches(call.number, number.replace("tel:", ""))) {
          LogUtil.i("MissedCallNotifier.insertPostCallNotification", "Notification updated");
          // Update the first notification that matches our post call note sender.
          DialerNotificationManager.notify(
              context,
              getNotificationTagForCall(call),
              MissedCallConstants.NOTIFICATION_ID,
              getNotificationForCall(call, note));
          return;
        }
      }
    }
    LogUtil.i("MissedCallNotifier.insertPostCallNotification", "notification not found");
  }

  private Notification getNotificationForCall(
      @NonNull NewCall call, @Nullable String postCallMessage) {
    ContactInfo contactInfo =
        callLogNotificationsQueryHelper.getContactInfo(
            call.number, call.numberPresentation, call.countryIso);

    // Create a public viewable version of the notification, suitable for display when sensitive
    // notification content is hidden.
    int titleResId =
        contactInfo.userType == ContactsUtils.USER_TYPE_WORK
            ? R.string.notification_missedWorkCallTitle
            : R.string.notification_missedCallTitle;
    Notification.Builder publicBuilder =
        createNotificationBuilder(call).setContentTitle(context.getText(titleResId));

    Notification.Builder builder = createNotificationBuilder(call);
    CharSequence expandedText;
    if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber)
        || TextUtils.equals(contactInfo.name, contactInfo.number)) {
      expandedText =
          PhoneNumberUtils.createTtsSpannable(
              BidiFormatter.getInstance()
                  .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR));
    } else {
      expandedText = contactInfo.name;
    }

    if (postCallMessage != null) {
      expandedText =
          context.getString(R.string.post_call_notification_message, expandedText, postCallMessage);
    }

    ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo);
    Bitmap photoIcon = loader.loadPhotoIcon();
    if (photoIcon != null) {
      builder.setLargeIcon(photoIcon);
    }
    // Create the notification suitable for display when sensitive information is showing.
    builder
        .setContentTitle(context.getText(titleResId))
        .setContentText(expandedText)
        // Include a public version of the notification to be shown when the missed call
        // notification is shown on the user's lock screen and they have chosen to hide
        // sensitive notification information.
        .setPublicVersion(publicBuilder.build());

    // Add additional actions when the user isn't locked
    if (UserManagerCompat.isUserUnlocked(context)) {
      if (!TextUtils.isEmpty(call.number)
          && !TextUtils.equals(call.number, context.getString(R.string.handle_restricted))) {
        builder.addAction(
            new Notification.Action.Builder(
                    Icon.createWithResource(context, R.drawable.ic_phone_24dp),
                    context.getString(R.string.notification_missedCall_call_back),
                    createCallBackPendingIntent(call.number, call.callsUri))
                .build());

        if (!PhoneNumberHelper.isUriNumber(call.number)) {
          builder.addAction(
              new Notification.Action.Builder(
                      Icon.createWithResource(context, R.drawable.quantum_ic_message_white_24),
                      context.getString(R.string.notification_missedCall_message),
                      createSendSmsFromNotificationPendingIntent(call.number, call.callsUri))
                  .build());
        }
      }
    }

    Notification notification = builder.build();
    configureLedOnNotification(notification);
    return notification;
  }

  private Notification.Builder createNotificationBuilder() {
    return new Notification.Builder(context)
        .setGroup(MissedCallConstants.GROUP_KEY)
        .setSmallIcon(android.R.drawable.stat_notify_missed_call)
        .setColor(ThemeComponent.get(context).theme().getColorPrimary())
        .setAutoCancel(true)
        .setOnlyAlertOnce(true)
        .setShowWhen(true)
        .setDefaults(Notification.DEFAULT_VIBRATE);
  }

  private Notification.Builder createNotificationBuilder(@NonNull NewCall call) {
    Builder builder =
        createNotificationBuilder()
            .setWhen(call.dateMs)
            .setDeleteIntent(
                CallLogNotificationsService.createCancelSingleMissedCallPendingIntent(
                    context, call.callsUri))
            .setContentIntent(createCallLogPendingIntent(call.callsUri));
    if (BuildCompat.isAtLeastO()) {
      builder.setChannelId(NotificationChannelId.MISSED_CALL);
    }

    return builder;
  }

  /** Trigger an intent to make a call from a missed call number. */
  @WorkerThread
  public void callBackFromMissedCall(String number, Uri callUri) {
    closeSystemDialogs(context);
    CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri);
    MissedCallNotificationCanceller.cancelSingle(context, callUri);
    DialerUtils.startActivityWithErrorToast(
        context,
        PreCall.getIntent(
                context,
                new CallIntentBuilder(number, CallInitiationType.Type.MISSED_CALL_NOTIFICATION))
            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
  }

  /** Trigger an intent to send an sms from a missed call number. */
  public void sendSmsFromMissedCall(String number, Uri callUri) {
    closeSystemDialogs(context);
    CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri);
    MissedCallNotificationCanceller.cancelSingle(context, callUri);
    DialerUtils.startActivityWithErrorToast(
        context, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
  }

  /**
   * Creates a new pending intent that sends the user to the call log.
   *
   * @return The pending intent.
   */
  private PendingIntent createCallLogPendingIntent() {
    return createCallLogPendingIntent(null);
  }

  /**
   * Creates a new pending intent that sends the user to the call log.
   *
   * @return The pending intent.
   * @param callUri Uri of the call to jump to. May be null
   */
  private PendingIntent createCallLogPendingIntent(@Nullable Uri callUri) {
    Intent contentIntent = MainComponent.getShowCallLogIntent(context);

    // TODO (a bug): scroll to call
    contentIntent.setData(callUri);
    return PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
  }

  private PendingIntent createCallBackPendingIntent(String number, @NonNull Uri callUri) {
    Intent intent = new Intent(context, CallLogNotificationsService.class);
    intent.setAction(CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION);
    intent.putExtra(MissedCallNotificationReceiver.EXTRA_NOTIFICATION_PHONE_NUMBER, number);
    intent.setData(callUri);
    // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
    // extra.
    return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
  }

  private PendingIntent createSendSmsFromNotificationPendingIntent(
      String number, @NonNull Uri callUri) {
    Intent intent = new Intent(context, CallLogNotificationsActivity.class);
    intent.setAction(CallLogNotificationsActivity.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION);
    intent.putExtra(CallLogNotificationsActivity.EXTRA_MISSED_CALL_NUMBER, number);
    intent.setData(callUri);
    // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
    // extra.
    return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
  }

  /** Configures a notification to emit the blinky notification light. */
  private void configureLedOnNotification(Notification notification) {
    notification.flags |= Notification.FLAG_SHOW_LIGHTS;
    notification.defaults |= Notification.DEFAULT_LIGHTS;
  }

  /** Closes open system dialogs and the notification shade. */
  private void closeSystemDialogs(Context context) {
    final Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
            .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
    final Bundle options = BroadcastOptions.makeBasic()
            .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT)
            .setDeferralPolicy(BroadcastOptions.DEFERRAL_POLICY_UNTIL_ACTIVE)
            .toBundle();
    context.sendBroadcast(intent, null /* receiverPermission */, options);
  }
}
