/*
 * Copyright (C) 2019 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.server.telecom.ui;

import android.annotation.NonNull;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Binder;
import android.os.UserHandle;
import android.provider.CallLog;
import android.telecom.DisconnectCause;
import android.telecom.Log;
import android.telecom.PhoneAccount;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.BidiFormatter;
import android.text.TextDirectionHeuristics;
import android.text.TextUtils;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.telecom.Call;
import com.android.server.telecom.CallState;
import com.android.server.telecom.CallsManager;
import com.android.server.telecom.CallsManagerListenerBase;
import com.android.server.telecom.Constants;
import com.android.server.telecom.R;
import com.android.server.telecom.TelecomBroadcastIntentProcessor;
import com.android.server.telecom.components.TelecomBroadcastReceiver;

import java.util.Locale;

/**
 * Handles notifications generated by Telecom for the case that a call was disconnected in order to
 * connect another "higher priority" emergency call and gives the user the choice to call or
 * message that user back after, similar to the missed call notifier.
 */
public class DisconnectedCallNotifier extends CallsManagerListenerBase {

    public interface Factory {
        DisconnectedCallNotifier create(Context context, CallsManager manager);
    }

    public static class Default implements Factory {

        @Override
        public DisconnectedCallNotifier create(Context context, CallsManager manager) {
            return new DisconnectedCallNotifier(context, manager);
        }
    }

    private static class CallInfo {
        public final UserHandle userHandle;
        public final Uri handle;
        public final long endTimeMs;
        public final Bitmap callerInfoIcon;
        public final Drawable callerInfoPhoto;
        public final String callerInfoName;
        public final boolean isEmergency;

        public CallInfo(UserHandle userHandle, Uri handle, long endTimeMs, Bitmap callerInfoIcon,
                Drawable callerInfoPhoto, String callerInfoName, boolean isEmergency) {
            this.userHandle = userHandle;
            this.handle = handle;
            this.endTimeMs = endTimeMs;
            this.callerInfoIcon = callerInfoIcon;
            this.callerInfoPhoto = callerInfoPhoto;
            this.callerInfoName = callerInfoName;
            this.isEmergency = isEmergency;
        }

        @Override
        public String toString() {
            return "CallInfo{" +
                    "userHandle=" + userHandle +
                    ", handle=" + handle +
                    ", isEmergency=" + isEmergency +
                    ", endTimeMs=" + endTimeMs +
                    ", callerInfoIcon=" + callerInfoIcon +
                    ", callerInfoPhoto=" + callerInfoPhoto +
                    ", callerInfoName='" + callerInfoName + '\'' +
                    '}';
        }
    }

    private static final String NOTIFICATION_TAG =
            DisconnectedCallNotifier.class.getSimpleName();
    private static final int DISCONNECTED_CALL_NOTIFICATION_ID = 1;

    private final Context mContext;
    private final CallsManager mCallsManager;
    private final NotificationManager mNotificationManager;
    // The pending info to display to the user after they have ended the emergency call.
    private CallInfo mPendingCallNotification;

    public DisconnectedCallNotifier(Context context, CallsManager callsManager) {
        mContext = context;
        mNotificationManager =
                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        mCallsManager = callsManager;
    }

    @Override
    public void onCallRemoved(Call call) {
        // Wait until the emergency call is ended before showing the notification.
        if (mCallsManager.getCalls().isEmpty() && mPendingCallNotification != null) {
            showDisconnectedNotification(mPendingCallNotification);
            mPendingCallNotification = null;
        }
    }

    @Override
    public void onCallStateChanged(Call call, int oldState, int newState) {
        DisconnectCause cause = call.getDisconnectCause();
        if (cause == null) {
            Log.w(this, "onCallStateChanged: unexpected null disconnect cause.");
            return;
        }
        // Call disconnected in favor of an emergency call. Place the call into a pending queue.
        if ((newState == CallState.DISCONNECTED) && (cause.getCode() == DisconnectCause.LOCAL) &&
                DisconnectCause.REASON_EMERGENCY_CALL_PLACED.equals(cause.getReason())) {
            // Clear any existing notification.
            clearNotification(mCallsManager.getCurrentUserHandle());
            UserHandle userHandle = call.getAssociatedUser();
            // As a last resort, use the current user to display the notification.
            if (userHandle == null) userHandle = mCallsManager.getCurrentUserHandle();
            mPendingCallNotification = new CallInfo(userHandle, call.getHandle(),
                    call.getCreationTimeMillis() + call.getAgeMillis(), call.getPhotoIcon(),
                    call.getPhoto(), call.getName(), call.isEmergencyCall());
        }
    }

    private void showDisconnectedNotification(@NonNull CallInfo call) {
        Log.i(this, "showDisconnectedNotification: userHandle=%d", call.userHandle.getIdentifier());

        final int titleResId = R.string.notification_disconnectedCall_title;
        final CharSequence expandedText = call.isEmergency
                ? mContext.getText(R.string.notification_disconnectedCall_generic_body)
                : mContext.getString(R.string.notification_disconnectedCall_body,
                        getNameForCallNotification(call));

        // Create a public viewable version of the notification, suitable for display when sensitive
        // notification content is hidden.
        // We use user's context here to make sure notification is badged if it is a managed user.
        Context contextForUser = getContextForUser(call.userHandle);
        Notification.Builder publicBuilder = new Notification.Builder(contextForUser,
                NotificationChannelManager.CHANNEL_ID_DISCONNECTED_CALLS);
        publicBuilder.setSmallIcon(android.R.drawable.stat_notify_error)
                .setColor(mContext.getResources().getColor(R.color.theme_color, null /*theme*/))
                // Set when the call was disconnected.
                .setWhen(call.endTimeMs)
                .setShowWhen(true)
                // Show "Phone" for notification title.
                .setContentTitle(mContext.getText(R.string.userCallActivityLabel))
                // Notification details shows that there are disconnected call(s), but does not
                // reveal the caller information.
                .setContentText(mContext.getText(titleResId))
                .setAutoCancel(true);

        if (!call.isEmergency) {
            publicBuilder.setContentIntent(createCallLogPendingIntent(call.userHandle));
        }

        // Create the notification suitable for display when sensitive information is showing.
        Notification.Builder builder = new Notification.Builder(contextForUser,
                NotificationChannelManager.CHANNEL_ID_DISCONNECTED_CALLS);
        builder.setSmallIcon(android.R.drawable.stat_notify_error)
                .setColor(mContext.getResources().getColor(R.color.theme_color, null /*theme*/))
                .setWhen(call.endTimeMs)
                .setShowWhen(true)
                .setContentTitle(mContext.getText(titleResId))
                //Only show expanded text for sensitive information
                .setStyle(new Notification.BigTextStyle().bigText(expandedText))
                .setAutoCancel(true)
                // Include a public version of the notification to be shown when the call
                // notification is shown on the user's lock screen and they have chosen to hide
                // sensitive notification information.
                .setPublicVersion(publicBuilder.build())
                .setChannelId(NotificationChannelManager.CHANNEL_ID_DISCONNECTED_CALLS);

        if (!call.isEmergency) {
            builder.setContentIntent(createCallLogPendingIntent(call.userHandle));
        }

        String handle = call.handle != null ? call.handle.getSchemeSpecificPart() : null;

        if (!TextUtils.isEmpty(handle)
                && !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted))
                && !call.isEmergency) {
            builder.addAction(new Notification.Action.Builder(
                    Icon.createWithResource(contextForUser, R.drawable.ic_phone_24dp),
                    // Reuse missed call "Call back"
                    mContext.getString(R.string.notification_missedCall_call_back),
                    createCallBackPendingIntent(call.handle, call.userHandle)).build());

            if (canRespondViaSms(call)) {
                builder.addAction(new Notification.Action.Builder(
                        Icon.createWithResource(contextForUser, R.drawable.ic_message_24dp),
                        // Reuse missed call "Call back"
                        mContext.getString(R.string.notification_missedCall_message),
                        createSendSmsFromNotificationPendingIntent(call.handle,
                                call.userHandle)).build());
            }
        }

        if (call.callerInfoIcon != null) {
            builder.setLargeIcon(call.callerInfoIcon);
        } else {
            if (call.callerInfoPhoto instanceof BitmapDrawable) {
                builder.setLargeIcon(((BitmapDrawable) call.callerInfoPhoto).getBitmap());
            }
        }

        Notification notification = builder.build();

        Log.i(this, "Adding missed call notification for %s.", Log.pii(call.handle));
        long token = Binder.clearCallingIdentity();
        try {
            // TODO: Only support one notification right now, so if multiple are hung up, we only
            // show the last one. Support multiple in the future.
            mNotificationManager.notifyAsUser(NOTIFICATION_TAG, DISCONNECTED_CALL_NOTIFICATION_ID,
                    notification, call.userHandle);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    /**
     * Returns the name to use in the call notification.
     */
    private String getNameForCallNotification(@NonNull CallInfo call) {
        String number = call.handle != null ? call.handle.getSchemeSpecificPart() : null;

        if (!TextUtils.isEmpty(number)) {
            String formattedNumber = PhoneNumberUtils.formatNumber(number,
                    getCurrentCountryIso(mContext));

            // The formatted number will be null if there was a problem formatting it, but we can
            // default to using the unformatted number instead (e.g. a SIP URI may not be able to
            // be formatted.
            if (!TextUtils.isEmpty(formattedNumber)) {
                number = formattedNumber;
            }
        }

        if (!TextUtils.isEmpty(call.callerInfoName) && TextUtils.isGraphic(call.callerInfoName)) {
            return call.callerInfoName;
        }
        if (!TextUtils.isEmpty(number)) {
            // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the
            // content of the rest of the notification.
            // TODO: Does this apply to SIP addresses?
            BidiFormatter bidiFormatter = BidiFormatter.getInstance();
            return bidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR);
        } else {
            // Use "unknown" if the call is unidentifiable.
            return mContext.getString(R.string.unknown);
        }
    }

    /**
     * @return The ISO 3166-1 two letters country code of the country the user is in based on the
     *      network location.  If the network location does not exist, fall back to the locale
     *      setting.
     */
    @VisibleForTesting
    public String getCurrentCountryIso(Context context) {
        // Without framework function calls, this seems to be the most accurate location service
        // we can rely on.
        final TelephonyManager telephonyManager =
                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
        String countryIso;
        try {
            countryIso = telephonyManager.getNetworkCountryIso().toUpperCase();
        } catch (UnsupportedOperationException ignored) {
            countryIso = null;
        }

        if (countryIso == null) {
            countryIso = Locale.getDefault().getCountry();
            Log.w(this, "No CountryDetector; falling back to countryIso based on locale: "
                    + countryIso);
        }
        return countryIso;
    }

    private Context getContextForUser(UserHandle user) {
        try {
            return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
        } catch (PackageManager.NameNotFoundException e) {
            // Default to mContext, not finding the package system is running as is unlikely.
            return mContext;
        }
    }

    /**
     * Creates an intent to be invoked when the user opts to "call back" from the disconnected call
     * notification.
     *
     * @param handle The handle to call back.
     */
    private PendingIntent createCallBackPendingIntent(Uri handle, UserHandle userHandle) {
        return createTelecomPendingIntent(
                TelecomBroadcastIntentProcessor.ACTION_DISCONNECTED_CALL_BACK_FROM_NOTIFICATION,
                handle, userHandle);
    }

    /**
     * Creates generic pending intent from the specified parameters to be received by
     * {@link TelecomBroadcastIntentProcessor}.
     *
     * @param action The intent action.
     * @param data The intent data.
     */
    private PendingIntent createTelecomPendingIntent(String action, Uri data,
            UserHandle userHandle) {
        Intent intent = new Intent(action, data, mContext, TelecomBroadcastReceiver.class);
        intent.putExtra(TelecomBroadcastIntentProcessor.EXTRA_USERHANDLE, userHandle);
        return PendingIntent.getBroadcast(mContext, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
    }

    private boolean canRespondViaSms(@NonNull CallInfo call) {
        // Only allow respond-via-sms for "tel:" calls.
        return call.handle != null &&
                PhoneAccount.SCHEME_TEL.equals(call.handle.getScheme());
    }

    /**
     * Creates a new pending intent that sends the user to the call log.
     *
     * @return The pending intent.
     */
    private PendingIntent createCallLogPendingIntent(UserHandle userHandle) {
        Intent intent = new Intent(Intent.ACTION_VIEW, null);
        intent.setType(CallLog.Calls.CONTENT_TYPE);

        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext);
        taskStackBuilder.addNextIntent(intent);

        return taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE, null, userHandle);
    }

    /**
     * Creates an intent to be invoked when the user opts to "send sms" from the missed call
     * notification.
     */
    private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle,
            UserHandle userHandle) {
        return createTelecomPendingIntent(
                TelecomBroadcastIntentProcessor.ACTION_DISCONNECTED_SEND_SMS_FROM_NOTIFICATION,
                Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null),
                userHandle);
    }

    /**
     * Clear any of the active notifications.
     * @param userHandle The user to clear the notifications for.
     */
    public void clearNotification(UserHandle userHandle) {
        long token = Binder.clearCallingIdentity();
        try {
            mNotificationManager.cancelAsUser(NOTIFICATION_TAG, DISCONNECTED_CALL_NOTIFICATION_ID,
                    userHandle);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }
}
