/*
 * Copyright (C) 2021 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.systemui.people;

import static android.app.Notification.CATEGORY_MISSED_CALL;
import static android.app.people.ConversationStatus.ACTIVITY_ANNIVERSARY;
import static android.app.people.ConversationStatus.ACTIVITY_AUDIO;
import static android.app.people.ConversationStatus.ACTIVITY_BIRTHDAY;
import static android.app.people.ConversationStatus.ACTIVITY_GAME;
import static android.app.people.ConversationStatus.ACTIVITY_LOCATION;
import static android.app.people.ConversationStatus.ACTIVITY_NEW_STORY;
import static android.app.people.ConversationStatus.ACTIVITY_UPCOMING_BIRTHDAY;
import static android.app.people.ConversationStatus.ACTIVITY_VIDEO;
import static android.app.people.ConversationStatus.AVAILABILITY_AVAILABLE;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH;
import static android.appwidget.AppWidgetManager.OPTION_APPWIDGET_SIZES;
import static android.util.TypedValue.COMPLEX_UNIT_DIP;
import static android.util.TypedValue.COMPLEX_UNIT_PX;

import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter;
import static com.android.systemui.people.PeopleSpaceUtils.STARRED_CONTACT;
import static com.android.systemui.people.PeopleSpaceUtils.VALID_CONTACT;
import static com.android.systemui.people.PeopleSpaceUtils.convertDrawableToBitmap;
import static com.android.systemui.people.PeopleSpaceUtils.getUserId;

import android.annotation.Nullable;
import android.app.PendingIntent;
import android.app.people.ConversationStatus;
import android.app.people.PeopleSpaceTile;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.ImageDecoder;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.graphics.text.LineBreaker;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.IconDrawableFactory;
import android.util.Log;
import android.util.Pair;
import android.util.Size;
import android.util.SizeF;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.RemoteViews;
import android.widget.TextView;

import androidx.annotation.DimenRes;
import androidx.annotation.Px;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import androidx.core.math.MathUtils;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.res.R;
import com.android.systemui.people.data.model.PeopleTileModel;
import com.android.systemui.people.widget.LaunchConversationActivity;
import com.android.systemui.people.widget.PeopleSpaceWidgetProvider;
import com.android.systemui.people.widget.PeopleTileKey;

import java.io.IOException;
import java.text.NumberFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/** Variables and functions that is related to Emoji. */
class EmojiHelper {
    static final CharSequence EMOJI_CAKE = "\ud83c\udf82";

    // This regex can be used to match Unicode emoji characters and character sequences. It's from
    // the official Unicode site (https://unicode.org/reports/tr51/#EBNF_and_Regex) with minor
    // changes to fit our needs. It should be updated once new emoji categories are added.
    //
    // Emoji categories that can be matched by this regex:
    // - Country flags. "\p{RI}\p{RI}" matches country flags since they always consist of 2 Unicode
    //   scalars.
    // - Single-Character Emoji. "\p{Emoji}" matches Single-Character Emojis.
    // - Emoji with modifiers. E.g. Emojis with different skin tones. "\p{Emoji}\p{EMod}" matches
    //   them.
    // - Emoji Presentation. Those are characters which can normally be drawn as either text or as
    //   Emoji. "\p{Emoji}\x{FE0F}" matches them.
    // - Emoji Keycap. E.g. Emojis for number 0 to 9. "\p{Emoji}\x{FE0F}\x{20E3}" matches them.
    // - Emoji tag sequence. "\p{Emoji}[\x{E0020}-\x{E007E}]+\x{E007F}" matches them.
    // - Emoji Zero-Width Joiner (ZWJ) Sequence. A ZWJ emoji is actually multiple emojis joined by
    //   the jointer "0x200D".
    //
    // Note: since "\p{Emoji}" also matches some ASCII characters like digits 0-9, we use
    // "\p{Emoji}&&\p{So}" to exclude them. This is the change we made from the official emoji
    // regex.
    private static final String UNICODE_EMOJI_REGEX =
            "\\p{RI}\\p{RI}|"
                    + "("
                    + "\\p{Emoji}(\\p{EMod}|\\x{FE0F}\\x{20E3}?|[\\x{E0020}-\\x{E007E}]+\\x{E007F})"
                    + "|[\\p{Emoji}&&\\p{So}]"
                    + ")"
                    + "("
                    + "\\x{200D}"
                    + "\\p{Emoji}(\\p{EMod}|\\x{FE0F}\\x{20E3}?|[\\x{E0020}-\\x{E007E}]+\\x{E007F})"
                    + "?)*";

    // Not all JDKs support emoji patterns, including the one errorprone runs under, which
    // makes it think that this is an invalid pattern.
    @SuppressWarnings("InvalidPatternSyntax")
    static final Pattern EMOJI_PATTERN = Pattern.compile(UNICODE_EMOJI_REGEX);
}

/** Functions that help creating the People tile layouts. */
public class PeopleTileViewHelper {
    /** Turns on debugging information about People Space. */
    private static final boolean DEBUG = PeopleSpaceUtils.DEBUG;
    private static final String TAG = "PeopleTileView";

    private static final int DAYS_IN_A_WEEK = 7;
    private static final int ONE_DAY = 1;

    public static final int LAYOUT_SMALL = 0;
    public static final int LAYOUT_MEDIUM = 1;
    public static final int LAYOUT_LARGE = 2;

    private static final int MIN_CONTENT_MAX_LINES = 2;
    private static final int NAME_MAX_LINES_WITHOUT_LAST_INTERACTION = 3;
    private static final int NAME_MAX_LINES_WITH_LAST_INTERACTION = 1;

    private static final int FIXED_HEIGHT_DIMENS_FOR_LARGE_NOTIF_CONTENT = 16 + 22 + 8 + 16;
    private static final int FIXED_HEIGHT_DIMENS_FOR_LARGE_STATUS_CONTENT = 16 + 16 + 24 + 4 + 16;
    private static final int MIN_MEDIUM_VERTICAL_PADDING = 4;
    private static final int MAX_MEDIUM_PADDING = 16;
    private static final int FIXED_HEIGHT_DIMENS_FOR_MEDIUM_CONTENT_BEFORE_PADDING = 8 + 4;
    private static final int FIXED_HEIGHT_DIMENS_FOR_SMALL_VERTICAL = 6 + 4 + 8;
    private static final int FIXED_WIDTH_DIMENS_FOR_SMALL_VERTICAL = 4 + 4;
    private static final int FIXED_HEIGHT_DIMENS_FOR_SMALL_HORIZONTAL = 6 + 4;
    private static final int FIXED_WIDTH_DIMENS_FOR_SMALL_HORIZONTAL = 8 + 8;

    private static final int MESSAGES_COUNT_OVERFLOW = 6;

    private static final Pattern DOUBLE_EXCLAMATION_PATTERN = Pattern.compile("[!][!]+");
    private static final Pattern DOUBLE_QUESTION_PATTERN = Pattern.compile("[?][?]+");
    private static final Pattern ANY_DOUBLE_MARK_PATTERN = Pattern.compile("[!?][!?]+");
    private static final Pattern MIXED_MARK_PATTERN = Pattern.compile("![?].*|.*[?]!");

    static final String BRIEF_PAUSE_ON_TALKBACK = "\n\n";

    public static final String EMPTY_STRING = "";

    private int mMediumVerticalPadding;

    private Context mContext;
    @Nullable
    private PeopleSpaceTile mTile;
    private PeopleTileKey mKey;
    private float mDensity;
    private int mAppWidgetId;
    private int mWidth;
    private int mHeight;
    private int mLayoutSize;
    private boolean mIsLeftToRight;

    private Locale mLocale;
    private NumberFormat mIntegerFormat;

    PeopleTileViewHelper(Context context, @Nullable PeopleSpaceTile tile,
            int appWidgetId, int width, int height, PeopleTileKey key) {
        mContext = context;
        mTile = tile;
        mKey = key;
        mAppWidgetId = appWidgetId;
        mDensity = mContext.getResources().getDisplayMetrics().density;
        mWidth = width;
        mHeight = height;
        mLayoutSize = getLayoutSize();
        mIsLeftToRight = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
                == View.LAYOUT_DIRECTION_LTR;
    }

    /**
     * Creates a {@link RemoteViews} for the specified arguments. The RemoteViews will support all
     * the sizes present in {@code options.}.
     */
    public static RemoteViews createRemoteViews(Context context, @Nullable PeopleSpaceTile tile,
            int appWidgetId, Bundle options, PeopleTileKey key) {
        List<SizeF> widgetSizes = getWidgetSizes(context, options);
        Map<SizeF, RemoteViews> sizeToRemoteView =
                widgetSizes
                        .stream()
                        .distinct()
                        .collect(Collectors.toMap(
                                Function.identity(),
                                size -> new PeopleTileViewHelper(
                                        context, tile, appWidgetId,
                                        (int) size.getWidth(),
                                        (int) size.getHeight(),
                                        key)
                                        .getViews()));
        return new RemoteViews(sizeToRemoteView);
    }

    private static List<SizeF> getWidgetSizes(Context context, Bundle options) {
        float density = context.getResources().getDisplayMetrics().density;
        List<SizeF> widgetSizes = options.getParcelableArrayList(OPTION_APPWIDGET_SIZES);
        // If the full list of sizes was provided in the options bundle, use that.
        if (widgetSizes != null && !widgetSizes.isEmpty()) return widgetSizes;

        // Otherwise, create a list using the portrait/landscape sizes.
        int defaultWidth = getSizeInDp(context, R.dimen.default_width, density);
        int defaultHeight = getSizeInDp(context, R.dimen.default_height, density);
        widgetSizes = new ArrayList<>(2);

        int portraitWidth = options.getInt(OPTION_APPWIDGET_MIN_WIDTH, defaultWidth);
        int portraitHeight = options.getInt(OPTION_APPWIDGET_MAX_HEIGHT, defaultHeight);
        widgetSizes.add(new SizeF(portraitWidth, portraitHeight));

        int landscapeWidth = options.getInt(OPTION_APPWIDGET_MAX_WIDTH, defaultWidth);
        int landscapeHeight = options.getInt(OPTION_APPWIDGET_MIN_HEIGHT, defaultHeight);
        widgetSizes.add(new SizeF(landscapeWidth, landscapeHeight));

        return widgetSizes;
    }

    @VisibleForTesting
    RemoteViews getViews() {
        RemoteViews viewsForTile = getViewForTile();
        int maxAvatarSize = getMaxAvatarSize(viewsForTile);
        RemoteViews views = setCommonRemoteViewsFields(viewsForTile, maxAvatarSize);
        return setLaunchIntents(views);
    }

    /**
     * The prioritization for the {@code mTile} content is missed calls, followed by notification
     * content, then birthdays, then the most recent status, and finally last interaction.
     */
    private RemoteViews getViewForTile() {
        if (DEBUG) Log.d(TAG, "Creating view for tile key: " + mKey.toString());
        if (mTile == null || mTile.isPackageSuspended() || mTile.isUserQuieted()) {
            if (DEBUG) Log.d(TAG, "Create suppressed view: " + mTile);
            return createSuppressedView();
        }

        if (isDndBlockingTileData(mTile)) {
            if (DEBUG) Log.d(TAG, "Create dnd view");
            return createDndRemoteViews().mRemoteViews;
        }

        if (Objects.equals(mTile.getNotificationCategory(), CATEGORY_MISSED_CALL)) {
            if (DEBUG) Log.d(TAG, "Create missed call view");
            return createMissedCallRemoteViews();
        }

        if (mTile.getNotificationKey() != null) {
            if (DEBUG) Log.d(TAG, "Create notification view");
            return createNotificationRemoteViews();
        }

        // TODO: Add sorting when we expose timestamp of statuses.
        List<ConversationStatus> statusesForEntireView =
                mTile.getStatuses() == null ? Arrays.asList() : mTile.getStatuses().stream().filter(
                        c -> isStatusValidForEntireStatusView(c)).collect(Collectors.toList());
        ConversationStatus birthdayStatus = getBirthdayStatus(statusesForEntireView);
        if (birthdayStatus != null) {
            if (DEBUG) Log.d(TAG, "Create birthday view");
            return createStatusRemoteViews(birthdayStatus);
        }

        if (!statusesForEntireView.isEmpty()) {
            if (DEBUG) {
                Log.d(TAG,
                        "Create status view for: " + statusesForEntireView.get(0).getActivity());
            }
            ConversationStatus mostRecentlyStartedStatus = statusesForEntireView.stream().max(
                    Comparator.comparing(s -> s.getStartTimeMillis())).get();
            return createStatusRemoteViews(mostRecentlyStartedStatus);
        }

        return createLastInteractionRemoteViews();
    }

    /** Whether the conversation associated with {@code tile} can bypass DND. */
    public static boolean isDndBlockingTileData(@Nullable PeopleSpaceTile tile) {
        if (tile == null) return false;

        int notificationPolicyState = tile.getNotificationPolicyState();
        if ((notificationPolicyState & PeopleSpaceTile.SHOW_CONVERSATIONS) != 0) {
            // Not in DND, or all conversations
            if (DEBUG) Log.d(TAG, "Tile can show all data: " + tile.getUserName());
            return false;
        }
        if ((notificationPolicyState & PeopleSpaceTile.SHOW_IMPORTANT_CONVERSATIONS) != 0
                && tile.isImportantConversation()) {
            if (DEBUG) Log.d(TAG, "Tile can show important: " + tile.getUserName());
            return false;
        }
        if ((notificationPolicyState & PeopleSpaceTile.SHOW_STARRED_CONTACTS) != 0
                && tile.getContactAffinity() == STARRED_CONTACT) {
            if (DEBUG) Log.d(TAG, "Tile can show starred: " + tile.getUserName());
            return false;
        }
        if ((notificationPolicyState & PeopleSpaceTile.SHOW_CONTACTS) != 0
                && (tile.getContactAffinity() == VALID_CONTACT
                || tile.getContactAffinity() == STARRED_CONTACT)) {
            if (DEBUG) Log.d(TAG, "Tile can show contacts: " + tile.getUserName());
            return false;
        }
        if (DEBUG) Log.d(TAG, "Tile can show if can bypass DND: " + tile.getUserName());
        return !tile.canBypassDnd();
    }

    private RemoteViews createSuppressedView() {
        RemoteViews views;
        if (mTile != null && mTile.isUserQuieted()) {
            views = new RemoteViews(mContext.getPackageName(),
                    R.layout.people_tile_work_profile_quiet_layout);
        } else {
            views = new RemoteViews(mContext.getPackageName(),
                    R.layout.people_tile_suppressed_layout);
        }
        Drawable appIcon = mContext.getDrawable(R.drawable.ic_conversation_icon).mutate();
        appIcon.setColorFilter(getDisabledColorFilter());
        Bitmap disabledBitmap = convertDrawableToBitmap(appIcon);
        views.setImageViewBitmap(R.id.icon, disabledBitmap);
        return views;
    }

    private void setMaxLines(RemoteViews views, boolean showSender) {
        int textSizeResId;
        int nameHeight;
        if (mLayoutSize == LAYOUT_LARGE) {
            textSizeResId = R.dimen.content_text_size_for_large;
            nameHeight = getLineHeightFromResource(R.dimen.name_text_size_for_large_content);
        } else {
            textSizeResId = R.dimen.content_text_size_for_medium;
            nameHeight = getLineHeightFromResource(R.dimen.name_text_size_for_medium_content);
        }
        boolean isStatusLayout =
                views.getLayoutId() == R.layout.people_tile_large_with_status_content;
        int contentHeight = getContentHeightForLayout(nameHeight, isStatusLayout);
        int lineHeight = getLineHeightFromResource(textSizeResId);
        int maxAdaptiveLines = Math.floorDiv(contentHeight, lineHeight);
        int maxLines = Math.max(MIN_CONTENT_MAX_LINES, maxAdaptiveLines);

        // Save a line for sender's name, if present.
        if (showSender) maxLines--;
        views.setInt(R.id.text_content, "setMaxLines", maxLines);
    }

    private int getLineHeightFromResource(int resId) {
        try {
            TextView text = new TextView(mContext);
            text.setTextSize(TypedValue.COMPLEX_UNIT_PX,
                    mContext.getResources().getDimension(resId));
            text.setTextAppearance(android.R.style.TextAppearance_DeviceDefault);
            int lineHeight = (int) (text.getLineHeight() / mDensity);
            return lineHeight;
        } catch (Exception e) {
            Log.e(TAG, "Could not create text view: " + e);
            return getSizeInDp(
                    R.dimen.content_text_size_for_medium);
        }
    }

    private int getSizeInDp(int dimenResourceId) {
        return getSizeInDp(mContext, dimenResourceId, mDensity);
    }

    public static int getSizeInDp(Context context, int dimenResourceId, float density) {
        return (int) (context.getResources().getDimension(dimenResourceId) / density);
    }

    private int getContentHeightForLayout(int lineHeight, boolean hasPredefinedIcon) {
        switch (mLayoutSize) {
            case LAYOUT_MEDIUM:
                return mHeight - (lineHeight + FIXED_HEIGHT_DIMENS_FOR_MEDIUM_CONTENT_BEFORE_PADDING
                        + mMediumVerticalPadding * 2);
            case LAYOUT_LARGE:
                int fixedHeight = hasPredefinedIcon ? FIXED_HEIGHT_DIMENS_FOR_LARGE_STATUS_CONTENT
                        : FIXED_HEIGHT_DIMENS_FOR_LARGE_NOTIF_CONTENT;
                return mHeight - (getSizeInDp(
                        R.dimen.max_people_avatar_size_for_large_content) + lineHeight
                        + fixedHeight);
            default:
                return -1;
        }
    }

    /** Calculates the best layout relative to the size in {@code options}. */
    private int getLayoutSize() {
        if (mHeight >= getSizeInDp(R.dimen.required_height_for_large)
                && mWidth >= getSizeInDp(R.dimen.required_width_for_large)) {
            if (DEBUG) Log.d(TAG, "Large view for mWidth: " + mWidth + " mHeight: " + mHeight);
            return LAYOUT_LARGE;
        }
        // Small layout used below a certain minimum mWidth with any mHeight.
        if (mHeight >= getSizeInDp(R.dimen.required_height_for_medium)
                && mWidth >= getSizeInDp(R.dimen.required_width_for_medium)) {
            int spaceAvailableForPadding =
                    mHeight - (getSizeInDp(R.dimen.avatar_size_for_medium)
                            + 4 + getLineHeightFromResource(
                            R.dimen.name_text_size_for_medium_content));
            if (DEBUG) {
                Log.d(TAG, "Medium view for mWidth: " + mWidth + " mHeight: " + mHeight
                        + " with padding space: " + spaceAvailableForPadding);
            }
            int maxVerticalPadding = Math.min(Math.floorDiv(spaceAvailableForPadding, 2),
                    MAX_MEDIUM_PADDING);
            mMediumVerticalPadding = Math.max(MIN_MEDIUM_VERTICAL_PADDING, maxVerticalPadding);
            return LAYOUT_MEDIUM;
        }
        // Small layout can always handle our minimum mWidth and mHeight for our widget.
        if (DEBUG) Log.d(TAG, "Small view for mWidth: " + mWidth + " mHeight: " + mHeight);
        return LAYOUT_SMALL;
    }

    /** Returns the max avatar size for {@code views} under the current {@code options}. */
    private int getMaxAvatarSize(RemoteViews views) {
        int layoutId = views.getLayoutId();
        int avatarSize = getSizeInDp(R.dimen.avatar_size_for_medium);
        if (layoutId == R.layout.people_tile_medium_empty) {
            return getSizeInDp(
                    R.dimen.max_people_avatar_size_for_large_content);
        }
        if (layoutId == R.layout.people_tile_medium_with_content) {
            return getSizeInDp(R.dimen.avatar_size_for_medium);
        }

        // Calculate adaptive avatar size for remaining layouts.
        if (layoutId == R.layout.people_tile_small) {
            int avatarHeightSpace = mHeight - (FIXED_HEIGHT_DIMENS_FOR_SMALL_VERTICAL + Math.max(18,
                    getLineHeightFromResource(
                            R.dimen.name_text_size_for_small)));
            int avatarWidthSpace = mWidth - FIXED_WIDTH_DIMENS_FOR_SMALL_VERTICAL;
            avatarSize = Math.min(avatarHeightSpace, avatarWidthSpace);
        }
        if (layoutId == R.layout.people_tile_small_horizontal) {
            int avatarHeightSpace = mHeight - FIXED_HEIGHT_DIMENS_FOR_SMALL_HORIZONTAL;
            int avatarWidthSpace = mWidth - FIXED_WIDTH_DIMENS_FOR_SMALL_HORIZONTAL;
            avatarSize = Math.min(avatarHeightSpace, avatarWidthSpace);
        }

        if (layoutId == R.layout.people_tile_large_with_notification_content) {
            avatarSize = mHeight - (FIXED_HEIGHT_DIMENS_FOR_LARGE_NOTIF_CONTENT + (
                    getLineHeightFromResource(
                            R.dimen.content_text_size_for_large)
                            * 3));
            return Math.min(avatarSize, getSizeInDp(
                    R.dimen.max_people_avatar_size_for_large_content));
        } else if (layoutId == R.layout.people_tile_large_with_status_content) {
            avatarSize = mHeight - (FIXED_HEIGHT_DIMENS_FOR_LARGE_STATUS_CONTENT + (
                    getLineHeightFromResource(R.dimen.content_text_size_for_large)
                            * 3));
            return Math.min(avatarSize, getSizeInDp(
                    R.dimen.max_people_avatar_size_for_large_content));
        }

        if (layoutId == R.layout.people_tile_large_empty) {
            int avatarHeightSpace = mHeight - (14 + 14 + getLineHeightFromResource(
                    R.dimen.name_text_size_for_large)
                    + getLineHeightFromResource(R.dimen.content_text_size_for_large)
                    + 16 + 10 + 16);
            int avatarWidthSpace = mWidth - (14 + 14);
            avatarSize = Math.min(avatarHeightSpace, avatarWidthSpace);
        }

        if (isDndBlockingTileData(mTile) && mLayoutSize != LAYOUT_SMALL) {
            avatarSize = createDndRemoteViews().mAvatarSize;
        }

        return Math.min(avatarSize,
                getSizeInDp(R.dimen.max_people_avatar_size));
    }

    private RemoteViews setCommonRemoteViewsFields(RemoteViews views,
            int maxAvatarSize) {
        try {
            if (mTile == null) {
                return views;
            }
            boolean isAvailable =
                    mTile.getStatuses() != null && mTile.getStatuses().stream().anyMatch(
                            c -> c.getAvailability() == AVAILABILITY_AVAILABLE);

            int startPadding;
            if (isAvailable) {
                views.setViewVisibility(R.id.availability, View.VISIBLE);
                startPadding = mContext.getResources().getDimensionPixelSize(
                        R.dimen.availability_dot_shown_padding);
                views.setContentDescription(R.id.availability,
                        mContext.getString(R.string.person_available));
            } else {
                views.setViewVisibility(R.id.availability, View.GONE);
                startPadding = mContext.getResources().getDimensionPixelSize(
                        R.dimen.availability_dot_missing_padding);
            }
            boolean isLeftToRight = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
                    == View.LAYOUT_DIRECTION_LTR;
            views.setViewPadding(R.id.padding_before_availability,
                    isLeftToRight ? startPadding : 0, 0, isLeftToRight ? 0 : startPadding,
                    0);

            boolean hasNewStory = getHasNewStory(mTile);
            views.setImageViewBitmap(R.id.person_icon,
                    getPersonIconBitmap(mContext, mTile, maxAvatarSize, hasNewStory));
            if (hasNewStory) {
                views.setContentDescription(R.id.person_icon,
                        mContext.getString(R.string.new_story_status_content_description,
                                mTile.getUserName()));
            } else {
                views.setContentDescription(R.id.person_icon, null);
            }
            return views;
        } catch (Exception e) {
            Log.e(TAG, "Failed to set common fields: " + e);
        }
        return views;
    }

    /** Whether {@code tile} has a new story. */
    public static boolean getHasNewStory(PeopleSpaceTile tile) {
        return tile.getStatuses() != null && tile.getStatuses().stream().anyMatch(
                c -> c.getActivity() == ACTIVITY_NEW_STORY);
    }

    private RemoteViews setLaunchIntents(RemoteViews views) {
        if (!PeopleTileKey.isValid(mKey) || mTile == null) {
            if (DEBUG) Log.d(TAG, "Skipping launch intent, Null tile or invalid key: " + mKey);
            return views;
        }

        try {
            Intent activityIntent = new Intent(mContext, LaunchConversationActivity.class);
            activityIntent.addFlags(
                    Intent.FLAG_ACTIVITY_NEW_TASK
                            | Intent.FLAG_ACTIVITY_CLEAR_TASK
                            | Intent.FLAG_ACTIVITY_NO_HISTORY
                            | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
            activityIntent.putExtra(PeopleSpaceWidgetProvider.EXTRA_TILE_ID, mKey.getShortcutId());
            activityIntent.putExtra(
                    PeopleSpaceWidgetProvider.EXTRA_PACKAGE_NAME, mKey.getPackageName());
            activityIntent.putExtra(PeopleSpaceWidgetProvider.EXTRA_USER_HANDLE,
                    new UserHandle(mKey.getUserId()));
            if (mTile != null) {
                activityIntent.putExtra(
                        PeopleSpaceWidgetProvider.EXTRA_NOTIFICATION_KEY,
                        mTile.getNotificationKey());
            }
            views.setOnClickPendingIntent(android.R.id.background, PendingIntent.getActivity(
                    mContext,
                    mAppWidgetId,
                    activityIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE));
            return views;
        } catch (Exception e) {
            Log.e(TAG, "Failed to add launch intents: " + e);
        }

        return views;
    }

    private RemoteViewsAndSizes createDndRemoteViews() {
        RemoteViews views = new RemoteViews(mContext.getPackageName(), getViewForDndRemoteViews());

        int mediumAvatarSize = getSizeInDp(R.dimen.avatar_size_for_medium_empty);
        int maxAvatarSize = getSizeInDp(R.dimen.max_people_avatar_size);

        String text = mContext.getString(R.string.paused_by_dnd);
        views.setTextViewText(R.id.text_content, text);

        int textSizeResId =
                mLayoutSize == LAYOUT_LARGE
                        ? R.dimen.content_text_size_for_large
                        : R.dimen.content_text_size_for_medium;
        float textSizePx = mContext.getResources().getDimension(textSizeResId);
        views.setTextViewTextSize(R.id.text_content, COMPLEX_UNIT_PX, textSizePx);
        int lineHeight = getLineHeightFromResource(textSizeResId);

        int avatarSize;
        if (mLayoutSize == LAYOUT_MEDIUM) {
            int maxTextHeight = mHeight - 16;
            views.setInt(R.id.text_content, "setMaxLines", maxTextHeight / lineHeight);
            avatarSize = mediumAvatarSize;
        } else {
            int outerPadding = 16;
            int outerPaddingTop = outerPadding - 2;
            int outerPaddingPx = dpToPx(outerPadding);
            int outerPaddingTopPx = dpToPx(outerPaddingTop);
            int iconSize =
                    getSizeInDp(
                            mLayoutSize == LAYOUT_SMALL
                                    ? R.dimen.regular_predefined_icon
                                    : R.dimen.largest_predefined_icon);
            int heightWithoutIcon = mHeight - 2 * outerPadding - iconSize;
            int paddingBetweenElements =
                    getSizeInDp(R.dimen.padding_between_suppressed_layout_items);
            int maxTextWidth = mWidth - outerPadding * 2;
            int maxTextHeight = heightWithoutIcon - mediumAvatarSize - paddingBetweenElements * 2;

            int availableAvatarHeight;
            int textHeight = estimateTextHeight(text, textSizeResId, maxTextWidth);
            if (textHeight <= maxTextHeight && mLayoutSize == LAYOUT_LARGE) {
                // If the text will fit, then display it and deduct its height from the space we
                // have for the avatar.
                availableAvatarHeight = heightWithoutIcon - textHeight - paddingBetweenElements * 2;
                views.setViewVisibility(R.id.text_content, View.VISIBLE);
                views.setInt(R.id.text_content, "setMaxLines", maxTextHeight / lineHeight);
                views.setContentDescription(R.id.predefined_icon, null);
                int availableAvatarWidth = mWidth - outerPadding * 2;
                avatarSize =
                        MathUtils.clamp(
                                /* value= */ Math.min(availableAvatarWidth, availableAvatarHeight),
                                /* min= */ dpToPx(10),
                                /* max= */ maxAvatarSize);
                views.setViewPadding(
                        android.R.id.background,
                        outerPaddingPx,
                        outerPaddingTopPx,
                        outerPaddingPx,
                        outerPaddingPx);
                views.setViewLayoutWidth(R.id.predefined_icon, iconSize, COMPLEX_UNIT_DIP);
                views.setViewLayoutHeight(R.id.predefined_icon, iconSize, COMPLEX_UNIT_DIP);
            } else {
                // If expected to use LAYOUT_LARGE, but we found we do not have space for the
                // text as calculated above, re-assign the view to the small layout.
                if (mLayoutSize != LAYOUT_SMALL) {
                    views = new RemoteViews(mContext.getPackageName(), R.layout.people_tile_small);
                }
                avatarSize = getMaxAvatarSize(views);
                views.setViewVisibility(R.id.messages_count, View.GONE);
                views.setViewVisibility(R.id.name, View.GONE);
                // If we don't show the dnd text, set it as the content description on the icon
                // for a11y.
                views.setContentDescription(R.id.predefined_icon, text);
            }
            views.setViewVisibility(R.id.predefined_icon, View.VISIBLE);
            views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_qs_dnd_on);
        }

        return new RemoteViewsAndSizes(views, avatarSize);
    }

    private RemoteViews createMissedCallRemoteViews() {
        RemoteViews views = setViewForContentLayout(new RemoteViews(mContext.getPackageName(),
                getLayoutForContent()));
        setPredefinedIconVisible(views);
        views.setViewVisibility(R.id.text_content, View.VISIBLE);
        views.setViewVisibility(R.id.messages_count, View.GONE);
        setMaxLines(views, false);
        CharSequence content = mTile.getNotificationContent();
        views.setTextViewText(R.id.text_content, content);
        setContentDescriptionForNotificationTextContent(views, content, mTile.getUserName());
        views.setColorAttr(R.id.text_content, "setTextColor", android.R.attr.colorError);
        views.setColorAttr(R.id.predefined_icon, "setColorFilter", android.R.attr.colorError);
        views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_phone_missed);
        if (mLayoutSize == LAYOUT_LARGE) {
            views.setInt(R.id.content, "setGravity", Gravity.BOTTOM);
            views.setViewLayoutHeightDimen(R.id.predefined_icon, R.dimen.larger_predefined_icon);
            views.setViewLayoutWidthDimen(R.id.predefined_icon, R.dimen.larger_predefined_icon);
        }
        setAvailabilityDotPadding(views, R.dimen.availability_dot_notification_padding);
        return views;
    }

    private void setPredefinedIconVisible(RemoteViews views) {
        views.setViewVisibility(R.id.predefined_icon, View.VISIBLE);
        if (mLayoutSize == LAYOUT_MEDIUM) {
            int endPadding = mContext.getResources().getDimensionPixelSize(
                    R.dimen.before_predefined_icon_padding);
            views.setViewPadding(R.id.name, mIsLeftToRight ? 0 : endPadding, 0,
                    mIsLeftToRight ? endPadding : 0,
                    0);
        }
    }

    private RemoteViews createNotificationRemoteViews() {
        RemoteViews views = setViewForContentLayout(new RemoteViews(mContext.getPackageName(),
                getLayoutForNotificationContent()));
        CharSequence sender = mTile.getNotificationSender();
        Uri imageUri = mTile.getNotificationDataUri();
        if (imageUri != null) {
            String newImageDescription = mContext.getString(
                    R.string.new_notification_image_content_description, mTile.getUserName());
            views.setContentDescription(R.id.image, newImageDescription);
            views.setViewVisibility(R.id.image, View.VISIBLE);
            views.setViewVisibility(R.id.text_content, View.GONE);
            try {
                Drawable drawable = resolveImage(imageUri, mContext);
                Bitmap bitmap = convertDrawableToBitmap(drawable);
                views.setImageViewBitmap(R.id.image, bitmap);
            } catch (IOException | SecurityException e) {
                Log.e(TAG, "Could not decode image: " + e);
                // If we couldn't load the image, show text that we have a new image.
                views.setTextViewText(R.id.text_content, newImageDescription);
                views.setViewVisibility(R.id.text_content, View.VISIBLE);
                views.setViewVisibility(R.id.image, View.GONE);
            }
        } else {
            setMaxLines(views, !TextUtils.isEmpty(sender));
            CharSequence content = mTile.getNotificationContent();
            setContentDescriptionForNotificationTextContent(views, content,
                    sender != null ? sender : mTile.getUserName());
            views = decorateBackground(views, content);
            views.setColorAttr(R.id.text_content, "setTextColor", android.R.attr.textColorPrimary);
            views.setTextViewText(R.id.text_content, mTile.getNotificationContent());
            if (mLayoutSize == LAYOUT_LARGE) {
                views.setViewPadding(R.id.name, 0, 0, 0,
                        mContext.getResources().getDimensionPixelSize(
                                R.dimen.above_notification_text_padding));
            }
            views.setViewVisibility(R.id.image, View.GONE);
            views.setImageViewResource(R.id.predefined_icon, R.drawable.ic_message);
        }
        if (mTile.getMessagesCount() > 1) {
            if (mLayoutSize == LAYOUT_MEDIUM) {
                int endPadding = mContext.getResources().getDimensionPixelSize(
                        R.dimen.before_messages_count_padding);
                views.setViewPadding(R.id.name, mIsLeftToRight ? 0 : endPadding, 0,
                        mIsLeftToRight ? endPadding : 0,
                        0);
            }
            views.setViewVisibility(R.id.messages_count, View.VISIBLE);
            views.setTextViewText(R.id.messages_count,
                    getMessagesCountText(mTile.getMessagesCount()));
            if (mLayoutSize == LAYOUT_SMALL) {
                views.setViewVisibility(R.id.predefined_icon, View.GONE);
            }
        }
        if (!TextUtils.isEmpty(sender)) {
            views.setViewVisibility(R.id.subtext, View.VISIBLE);
            views.setTextViewText(R.id.subtext, sender);
        } else {
            views.setViewVisibility(R.id.subtext, View.GONE);
        }
        setAvailabilityDotPadding(views, R.dimen.availability_dot_notification_padding);
        return views;
    }

     Drawable resolveImage(Uri uri, Context context) throws IOException {
        final ImageDecoder.Source source =
                ImageDecoder.createSource(context.getContentResolver(), uri);
        final Drawable drawable =
                ImageDecoder.decodeDrawable(source, (decoder, info, s) -> {
                    onHeaderDecoded(decoder, info, s);
                });
        return drawable;
    }

    private static int getPowerOfTwoForSampleRatio(double ratio) {
        final int k = Integer.highestOneBit((int) Math.floor(ratio));
        return Math.max(1, k);
    }

    private void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
            ImageDecoder.Source source) {
        int widthInPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mWidth,
                mContext.getResources().getDisplayMetrics());
        int heightInPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, mHeight,
                mContext.getResources().getDisplayMetrics());
        int maxIconSizeInPx = Math.max(widthInPx, heightInPx);
        int minDimen = (int) (1.5 * Math.min(widthInPx, heightInPx));
        if (minDimen < maxIconSizeInPx) {
            maxIconSizeInPx = minDimen;
        }
        final Size size = info.getSize();
        final int originalSize = Math.max(size.getHeight(), size.getWidth());
        final double ratio = (originalSize > maxIconSizeInPx)
                ? originalSize * 1f / maxIconSizeInPx
                : 1.0;
        decoder.setTargetSampleSize(getPowerOfTwoForSampleRatio(ratio));
    }

    private void setContentDescriptionForNotificationTextContent(RemoteViews views,
            CharSequence content, CharSequence sender) {
        String newTextDescriptionWithNotificationContent = mContext.getString(
                R.string.new_notification_text_content_description, sender, content);
        int idForContentDescription =
                mLayoutSize == LAYOUT_SMALL ? R.id.predefined_icon : R.id.text_content;
        views.setContentDescription(idForContentDescription,
                newTextDescriptionWithNotificationContent);
    }

    // Some messaging apps only include up to 6 messages in their notifications.
    private String getMessagesCountText(int count) {
        if (count >= MESSAGES_COUNT_OVERFLOW) {
            return mContext.getResources().getString(
                    R.string.messages_count_overflow_indicator, MESSAGES_COUNT_OVERFLOW);
        }

        // Cache the locale-appropriate NumberFormat.  Configuration locale is guaranteed
        // non-null, so the first time this is called we will always get the appropriate
        // NumberFormat, then never regenerate it unless the locale changes on the fly.
        final Locale curLocale = mContext.getResources().getConfiguration().getLocales().get(0);
        if (!curLocale.equals(mLocale)) {
            mLocale = curLocale;
            mIntegerFormat = NumberFormat.getIntegerInstance(curLocale);
        }
        return mIntegerFormat.format(count);
    }

    private RemoteViews createStatusRemoteViews(ConversationStatus status) {
        RemoteViews views = setViewForContentLayout(new RemoteViews(mContext.getPackageName(),
                getLayoutForContent()));
        CharSequence statusText = status.getDescription();
        if (TextUtils.isEmpty(statusText)) {
            statusText = getStatusTextByType(status.getActivity());
        }
        setPredefinedIconVisible(views);
        views.setTextViewText(R.id.text_content, statusText);

        if (status.getActivity() == ACTIVITY_BIRTHDAY
                || status.getActivity() == ACTIVITY_UPCOMING_BIRTHDAY) {
            setEmojiBackground(views, EmojiHelper.EMOJI_CAKE);
        }

        Icon statusIcon = status.getIcon();
        if (statusIcon != null) {
            // No text content styled text on medium or large.
            views.setViewVisibility(R.id.scrim_layout, View.VISIBLE);
            views.setImageViewIcon(R.id.status_icon, statusIcon);
            // Show 1-line subtext on large layout with status images.
            if (mLayoutSize == LAYOUT_LARGE) {
                if (DEBUG) Log.d(TAG, "Remove name for large");
                views.setInt(R.id.content, "setGravity", Gravity.BOTTOM);
                views.setViewVisibility(R.id.name, View.GONE);
                views.setColorAttr(R.id.text_content, "setTextColor",
                        android.R.attr.textColorPrimary);
            } else if (mLayoutSize == LAYOUT_MEDIUM) {
                views.setViewVisibility(R.id.text_content, View.GONE);
                views.setTextViewText(R.id.name, statusText);
            }
        } else {
            // Secondary text color for statuses without icons.
            views.setColorAttr(R.id.text_content, "setTextColor",
                    android.R.attr.textColorSecondary);
            setMaxLines(views, false);
        }
        setAvailabilityDotPadding(views, R.dimen.availability_dot_status_padding);
        views.setImageViewResource(R.id.predefined_icon, getDrawableForStatus(status));
        CharSequence descriptionForStatus =
                getContentDescriptionForStatus(status);
        CharSequence customContentDescriptionForStatus = mContext.getString(
                R.string.new_status_content_description, mTile.getUserName(), descriptionForStatus);
        switch (mLayoutSize) {
            case LAYOUT_LARGE:
                views.setContentDescription(R.id.text_content,
                        customContentDescriptionForStatus);
                break;
            case LAYOUT_MEDIUM:
                views.setContentDescription(statusIcon == null ? R.id.text_content : R.id.name,
                        customContentDescriptionForStatus);
                break;
            case LAYOUT_SMALL:
                views.setContentDescription(R.id.predefined_icon,
                        customContentDescriptionForStatus);
                break;
        }
        return views;
    }

    private CharSequence getContentDescriptionForStatus(ConversationStatus status) {
        CharSequence name = mTile.getUserName();
        if (!TextUtils.isEmpty(status.getDescription())) {
            return status.getDescription();
        }
        switch (status.getActivity()) {
            case ACTIVITY_NEW_STORY:
                return mContext.getString(R.string.new_story_status_content_description,
                        name);
            case ACTIVITY_ANNIVERSARY:
                return mContext.getString(R.string.anniversary_status_content_description, name);
            case ACTIVITY_UPCOMING_BIRTHDAY:
                return mContext.getString(R.string.upcoming_birthday_status_content_description,
                        name);
            case ACTIVITY_BIRTHDAY:
                return mContext.getString(R.string.birthday_status_content_description, name);
            case ACTIVITY_LOCATION:
                return mContext.getString(R.string.location_status_content_description, name);
            case ACTIVITY_GAME:
                return mContext.getString(R.string.game_status);
            case ACTIVITY_VIDEO:
                return mContext.getString(R.string.video_status);
            case ACTIVITY_AUDIO:
                return mContext.getString(R.string.audio_status);
            default:
                return EMPTY_STRING;
        }
    }

    private int getDrawableForStatus(ConversationStatus status) {
        switch (status.getActivity()) {
            case ACTIVITY_NEW_STORY:
                return R.drawable.ic_pages;
            case ACTIVITY_ANNIVERSARY:
                return R.drawable.ic_celebration;
            case ACTIVITY_UPCOMING_BIRTHDAY:
                return R.drawable.ic_gift;
            case ACTIVITY_BIRTHDAY:
                return R.drawable.ic_cake;
            case ACTIVITY_LOCATION:
                return R.drawable.ic_location;
            case ACTIVITY_GAME:
                return R.drawable.ic_play_games;
            case ACTIVITY_VIDEO:
                return R.drawable.ic_video;
            case ACTIVITY_AUDIO:
                return R.drawable.ic_music_note;
            default:
                return R.drawable.ic_person;
        }
    }

    /**
     * Update the padding of the availability dot. The padding on the availability dot decreases
     * on the status layouts compared to all other layouts.
     */
    private void setAvailabilityDotPadding(RemoteViews views, int resId) {
        int startPadding = mContext.getResources().getDimensionPixelSize(resId);
        int bottomPadding = mContext.getResources().getDimensionPixelSize(
                R.dimen.medium_content_padding_above_name);
        views.setViewPadding(R.id.medium_content,
                mIsLeftToRight ? startPadding : 0, 0, mIsLeftToRight ? 0 : startPadding,
                bottomPadding);
    }

    @Nullable
    private ConversationStatus getBirthdayStatus(
            List<ConversationStatus> statuses) {
        Optional<ConversationStatus> birthdayStatus = statuses.stream().filter(
                c -> c.getActivity() == ACTIVITY_BIRTHDAY).findFirst();
        if (birthdayStatus.isPresent()) {
            return birthdayStatus.get();
        }
        if (!TextUtils.isEmpty(mTile.getBirthdayText())) {
            return new ConversationStatus.Builder(mTile.getId(), ACTIVITY_BIRTHDAY).build();
        }

        return null;
    }

    /**
     * Returns whether a {@code status} should have its own entire templated view.
     *
     * <p>A status may still be shown on the view (for example, as a new story ring) even if it's
     * not valid to compose an entire view.
     */
    private boolean isStatusValidForEntireStatusView(ConversationStatus status) {
        switch (status.getActivity()) {
            // Birthday & Anniversary don't require text provided or icon provided.
            case ACTIVITY_BIRTHDAY:
            case ACTIVITY_ANNIVERSARY:
                return true;
            default:
                // For future birthday, location, new story, video, music, game, and other, the
                // app must provide either text or an icon.
                return !TextUtils.isEmpty(status.getDescription())
                        || status.getIcon() != null;
        }
    }

    private String getStatusTextByType(int activity) {
        switch (activity) {
            case ACTIVITY_BIRTHDAY:
                return mContext.getString(R.string.birthday_status);
            case ACTIVITY_UPCOMING_BIRTHDAY:
                return mContext.getString(R.string.upcoming_birthday_status);
            case ACTIVITY_ANNIVERSARY:
                return mContext.getString(R.string.anniversary_status);
            case ACTIVITY_LOCATION:
                return mContext.getString(R.string.location_status);
            case ACTIVITY_NEW_STORY:
                return mContext.getString(R.string.new_story_status);
            case ACTIVITY_VIDEO:
                return mContext.getString(R.string.video_status);
            case ACTIVITY_AUDIO:
                return mContext.getString(R.string.audio_status);
            case ACTIVITY_GAME:
                return mContext.getString(R.string.game_status);
            default:
                return EMPTY_STRING;
        }
    }

    private RemoteViews decorateBackground(RemoteViews views, CharSequence content) {
        CharSequence emoji = getDoubleEmoji(content);
        if (!TextUtils.isEmpty(emoji)) {
            setEmojiBackground(views, emoji);
            setPunctuationBackground(views, null);
            return views;
        }

        CharSequence punctuation = getDoublePunctuation(content);
        setEmojiBackground(views, null);
        setPunctuationBackground(views, punctuation);
        return views;
    }

    private RemoteViews setEmojiBackground(RemoteViews views, CharSequence content) {
        if (TextUtils.isEmpty(content)) {
            views.setViewVisibility(R.id.emojis, View.GONE);
            return views;
        }
        views.setTextViewText(R.id.emoji1, content);
        views.setTextViewText(R.id.emoji2, content);
        views.setTextViewText(R.id.emoji3, content);

        views.setViewVisibility(R.id.emojis, View.VISIBLE);
        return views;
    }

    private RemoteViews setPunctuationBackground(RemoteViews views, CharSequence content) {
        if (TextUtils.isEmpty(content)) {
            views.setViewVisibility(R.id.punctuations, View.GONE);
            return views;
        }
        views.setTextViewText(R.id.punctuation1, content);
        views.setTextViewText(R.id.punctuation2, content);
        views.setTextViewText(R.id.punctuation3, content);
        views.setTextViewText(R.id.punctuation4, content);
        views.setTextViewText(R.id.punctuation5, content);
        views.setTextViewText(R.id.punctuation6, content);

        views.setViewVisibility(R.id.punctuations, View.VISIBLE);
        return views;
    }

    /** Returns punctuation character(s) if {@code message} has double punctuation ("!" or "?"). */
    @VisibleForTesting
    CharSequence getDoublePunctuation(CharSequence message) {
        if (!ANY_DOUBLE_MARK_PATTERN.matcher(message).find()) {
            return null;
        }
        if (MIXED_MARK_PATTERN.matcher(message).find()) {
            return "!?";
        }
        Matcher doubleQuestionMatcher = DOUBLE_QUESTION_PATTERN.matcher(message);
        if (!doubleQuestionMatcher.find()) {
            return "!";
        }
        Matcher doubleExclamationMatcher = DOUBLE_EXCLAMATION_PATTERN.matcher(message);
        if (!doubleExclamationMatcher.find()) {
            return "?";
        }
        // If we have both "!!" and "??", return the one that comes first.
        if (doubleQuestionMatcher.start() < doubleExclamationMatcher.start()) {
            return "?";
        }
        return "!";
    }

    /** Returns emoji if {@code message} has two of the same emoji in sequence. */
    @VisibleForTesting
    CharSequence getDoubleEmoji(CharSequence message) {
        Matcher unicodeEmojiMatcher = EmojiHelper.EMOJI_PATTERN.matcher(message);
        // Stores the start and end indices of each matched emoji.
        List<Pair<Integer, Integer>> emojiIndices = new ArrayList<>();
        // Stores each emoji text.
        List<CharSequence> emojiTexts = new ArrayList<>();

        // Scan message for emojis
        while (unicodeEmojiMatcher.find()) {
            int start = unicodeEmojiMatcher.start();
            int end = unicodeEmojiMatcher.end();
            emojiIndices.add(new Pair(start, end));
            emojiTexts.add(message.subSequence(start, end));
        }

        if (DEBUG) Log.d(TAG, "Number of emojis in the message: " + emojiIndices.size());
        if (emojiIndices.size() < 2) {
            return null;
        }

        for (int i = 1; i < emojiIndices.size(); ++i) {
            Pair<Integer, Integer> second = emojiIndices.get(i);
            Pair<Integer, Integer> first = emojiIndices.get(i - 1);

            // Check if second emoji starts right after first starts
            if (Objects.equals(second.first, first.second)) {
                // Check if emojis in sequence are the same
                if (Objects.equals(emojiTexts.get(i), emojiTexts.get(i - 1))) {
                    if (DEBUG) {
                        Log.d(TAG, "Two of the same emojis in sequence: " + emojiTexts.get(i));
                    }
                    return emojiTexts.get(i);
                }
            }
        }

        // No equal emojis in sequence.
        return null;
    }

    private RemoteViews setViewForContentLayout(RemoteViews views) {
        views = decorateBackground(views, "");
        views.setContentDescription(R.id.predefined_icon, null);
        views.setContentDescription(R.id.text_content, null);
        views.setContentDescription(R.id.name, null);
        views.setContentDescription(R.id.image, null);
        views.setAccessibilityTraversalAfter(R.id.text_content, R.id.name);
        if (mLayoutSize == LAYOUT_SMALL) {
            views.setViewVisibility(R.id.predefined_icon, View.VISIBLE);
            views.setViewVisibility(R.id.name, View.GONE);
        } else {
            views.setViewVisibility(R.id.predefined_icon, View.GONE);
            views.setViewVisibility(R.id.name, View.VISIBLE);
            views.setViewVisibility(R.id.text_content, View.VISIBLE);
            views.setViewVisibility(R.id.subtext, View.GONE);
            views.setViewVisibility(R.id.image, View.GONE);
            views.setViewVisibility(R.id.scrim_layout, View.GONE);
        }

        if (mLayoutSize == LAYOUT_MEDIUM) {
            // Maximize vertical padding with an avatar size of 48dp and name on medium.
            if (DEBUG) Log.d(TAG, "Set vertical padding: " + mMediumVerticalPadding);
            int horizontalPadding = (int) Math.floor(MAX_MEDIUM_PADDING * mDensity);
            int verticalPadding = (int) Math.floor(mMediumVerticalPadding * mDensity);
            views.setViewPadding(R.id.content, horizontalPadding, verticalPadding,
                    horizontalPadding,
                    verticalPadding);
            views.setViewPadding(R.id.name, 0, 0, 0, 0);
            // Expand the name font on medium if there's space.
            int heightRequiredForMaxContentText = (int) (mContext.getResources().getDimension(
                    R.dimen.medium_height_for_max_name_text_size) / mDensity);
            if (mHeight > heightRequiredForMaxContentText) {
                views.setTextViewTextSize(R.id.name, TypedValue.COMPLEX_UNIT_PX,
                        (int) mContext.getResources().getDimension(
                                R.dimen.max_name_text_size_for_medium));
            }
        }

        if (mLayoutSize == LAYOUT_LARGE) {
            // Decrease the view padding below the name on all layouts besides notification "text".
            views.setViewPadding(R.id.name, 0, 0, 0,
                    mContext.getResources().getDimensionPixelSize(
                            R.dimen.below_name_text_padding));
            // All large layouts besides missed calls & statuses with images, have gravity top.
            views.setInt(R.id.content, "setGravity", Gravity.TOP);
        }

        // For all layouts except Missed Calls, ensure predefined icon is regular sized.
        views.setViewLayoutHeightDimen(R.id.predefined_icon, R.dimen.regular_predefined_icon);
        views.setViewLayoutWidthDimen(R.id.predefined_icon, R.dimen.regular_predefined_icon);

        views.setViewVisibility(R.id.messages_count, View.GONE);
        if (mTile.getUserName() != null) {
            views.setTextViewText(R.id.name, mTile.getUserName());
        }

        return views;
    }

    private RemoteViews createLastInteractionRemoteViews() {
        RemoteViews views = new RemoteViews(mContext.getPackageName(), getEmptyLayout());
        views.setInt(R.id.name, "setMaxLines", NAME_MAX_LINES_WITH_LAST_INTERACTION);
        if (mLayoutSize == LAYOUT_SMALL) {
            views.setViewVisibility(R.id.name, View.VISIBLE);
            views.setViewVisibility(R.id.predefined_icon, View.GONE);
            views.setViewVisibility(R.id.messages_count, View.GONE);
        }
        if (mTile.getUserName() != null) {
            views.setTextViewText(R.id.name, mTile.getUserName());
        }
        String status = getLastInteractionString(mContext,
                mTile.getLastInteractionTimestamp());
        if (status != null) {
            if (DEBUG) Log.d(TAG, "Show last interaction");
            views.setViewVisibility(R.id.last_interaction, View.VISIBLE);
            views.setTextViewText(R.id.last_interaction, status);
        } else {
            if (DEBUG) Log.d(TAG, "Hide last interaction");
            views.setViewVisibility(R.id.last_interaction, View.GONE);
            if (mLayoutSize == LAYOUT_MEDIUM) {
                views.setInt(R.id.name, "setMaxLines", NAME_MAX_LINES_WITHOUT_LAST_INTERACTION);
            }
        }
        return views;
    }

    private int getEmptyLayout() {
        switch (mLayoutSize) {
            case LAYOUT_MEDIUM:
                return R.layout.people_tile_medium_empty;
            case LAYOUT_LARGE:
                return R.layout.people_tile_large_empty;
            case LAYOUT_SMALL:
            default:
                return getLayoutSmallByHeight();
        }
    }

    private int getLayoutForNotificationContent() {
        switch (mLayoutSize) {
            case LAYOUT_MEDIUM:
                return R.layout.people_tile_medium_with_content;
            case LAYOUT_LARGE:
                return R.layout.people_tile_large_with_notification_content;
            case LAYOUT_SMALL:
            default:
                return getLayoutSmallByHeight();
        }
    }

    private int getLayoutForContent() {
        switch (mLayoutSize) {
            case LAYOUT_MEDIUM:
                return R.layout.people_tile_medium_with_content;
            case LAYOUT_LARGE:
                return R.layout.people_tile_large_with_status_content;
            case LAYOUT_SMALL:
            default:
                return getLayoutSmallByHeight();
        }
    }

    private int getViewForDndRemoteViews() {
        switch (mLayoutSize) {
            case LAYOUT_MEDIUM:
                return R.layout.people_tile_with_suppression_detail_content_horizontal;
            case LAYOUT_LARGE:
                return R.layout.people_tile_with_suppression_detail_content_vertical;
            case LAYOUT_SMALL:
            default:
                return getLayoutSmallByHeight();
        }
    }

    private int getLayoutSmallByHeight() {
        if (mHeight >= getSizeInDp(R.dimen.required_height_for_medium)) {
            return R.layout.people_tile_small;
        }
        return R.layout.people_tile_small_horizontal;
    }

    /** Returns a bitmap with the user icon and package icon. */
    public static Bitmap getPersonIconBitmap(Context context, PeopleTileModel tile,
            int maxAvatarSize) {
        return getPersonIconBitmap(context, maxAvatarSize, tile.getHasNewStory(),
                tile.getUserIcon(), tile.getKey().getPackageName(), tile.getKey().getUserId(),
                tile.isImportant(),  tile.isDndBlocking());
    }

    /** Returns a bitmap with the user icon and package icon. */
    private static Bitmap getPersonIconBitmap(
            Context context, PeopleSpaceTile tile, int maxAvatarSize, boolean hasNewStory) {
        return getPersonIconBitmap(context, maxAvatarSize, hasNewStory, tile.getUserIcon(),
                tile.getPackageName(), getUserId(tile),
                tile.isImportantConversation(), isDndBlockingTileData(tile));
    }

    private static Bitmap getPersonIconBitmap(
            Context context, int maxAvatarSize, boolean hasNewStory, Icon icon, String packageName,
            int userId, boolean importantConversation, boolean dndBlockingTileData) {
        if (icon == null) {
            Drawable placeholder = context.getDrawable(R.drawable.ic_avatar_with_badge).mutate();
            placeholder.setColorFilter(getDisabledColorFilter());
            return convertDrawableToBitmap(placeholder);
        }
        PeopleStoryIconFactory storyIcon = new PeopleStoryIconFactory(context,
                context.getPackageManager(),
                IconDrawableFactory.newInstance(context, false),
                maxAvatarSize);
        RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create(
                context.getResources(), icon.getBitmap());
        Drawable personDrawable = storyIcon.getPeopleTileDrawable(roundedDrawable,
                packageName, userId, importantConversation,
                hasNewStory);

        if (dndBlockingTileData) {
            personDrawable.setColorFilter(getDisabledColorFilter());
        }

        return convertDrawableToBitmap(personDrawable);
    }

    /** Returns a readable status describing the {@code lastInteraction}. */
    @Nullable
    public static String getLastInteractionString(Context context, long lastInteraction) {
        if (lastInteraction == 0L) {
            Log.e(TAG, "Could not get valid last interaction");
            return null;
        }
        long now = System.currentTimeMillis();
        Duration durationSinceLastInteraction = Duration.ofMillis(now - lastInteraction);
        if (durationSinceLastInteraction.toDays() <= ONE_DAY) {
            return null;
        } else if (durationSinceLastInteraction.toDays() < DAYS_IN_A_WEEK) {
            return context.getString(R.string.days_timestamp,
                    durationSinceLastInteraction.toDays());
        } else if (durationSinceLastInteraction.toDays() == DAYS_IN_A_WEEK) {
            return context.getString(R.string.one_week_timestamp);
        } else if (durationSinceLastInteraction.toDays() < DAYS_IN_A_WEEK * 2) {
            return context.getString(R.string.over_one_week_timestamp);
        } else if (durationSinceLastInteraction.toDays() == DAYS_IN_A_WEEK * 2) {
            return context.getString(R.string.two_weeks_timestamp);
        } else {
            // Over 2 weeks ago
            return context.getString(R.string.over_two_weeks_timestamp);
        }
    }

    /**
     * Estimates the height (in dp) which the text will have given the text size and the available
     * width. Returns Integer.MAX_VALUE if the estimation couldn't be obtained, as this is intended
     * to be used an estimate of the maximum.
     */
    private int estimateTextHeight(
            CharSequence text,
            @DimenRes int textSizeResId,
            int availableWidthDp) {
        StaticLayout staticLayout = buildStaticLayout(text, textSizeResId, availableWidthDp);
        if (staticLayout == null) {
            // Return max value (rather than e.g. -1) so the value can be used with <= bound checks.
            return Integer.MAX_VALUE;
        }
        return pxToDp(staticLayout.getHeight());
    }

    /**
     * Builds a StaticLayout for the text given the text size and available width. This can be used
     * to obtain information about how TextView will lay out the text. Returns null if any error
     * occurred creating a TextView.
     */
    @Nullable
    private StaticLayout buildStaticLayout(
            CharSequence text,
            @DimenRes int textSizeResId,
            int availableWidthDp) {
        try {
            TextView textView = new TextView(mContext);
            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX,
                    mContext.getResources().getDimension(textSizeResId));
            textView.setTextAppearance(android.R.style.TextAppearance_DeviceDefault);
            TextPaint paint = textView.getPaint();
            return StaticLayout.Builder.obtain(
                    text, 0, text.length(), paint, dpToPx(availableWidthDp))
                    // Simple break strategy avoids hyphenation unless there's a single word longer
                    // than the line width. We use this break strategy so that we consider text to
                    // "fit" only if it fits in a nice way (i.e. without hyphenation in the middle
                    // of words).
                    .setBreakStrategy(LineBreaker.BREAK_STRATEGY_SIMPLE)
                    .build();
        } catch (Exception e) {
            Log.e(TAG, "Could not create static layout: " + e);
            return null;
        }
    }

    private int dpToPx(float dp) {
        return (int) (dp * mDensity);
    }

    private int pxToDp(@Px float px) {
        return (int) (px / mDensity);
    }

    private static final class RemoteViewsAndSizes {
        final RemoteViews mRemoteViews;
        final int mAvatarSize;

        RemoteViewsAndSizes(RemoteViews remoteViews, int avatarSize) {
            mRemoteViews = remoteViews;
            mAvatarSize = avatarSize;
        }
    }
}
