diff options
27 files changed, 1608 insertions, 403 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index f5780ab0eec2..bd1339fe12a8 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -5555,15 +5555,19 @@ package android.app { field public static final int DEFAULT_LIGHTS = 4; // 0x4 field public static final int DEFAULT_SOUND = 1; // 0x1 field public static final int DEFAULT_VIBRATE = 2; // 0x2 + field public static final String EXTRA_ANSWER_INTENT = "android.answerIntent"; field public static final String EXTRA_AUDIO_CONTENTS_URI = "android.audioContents"; field public static final String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri"; field public static final String EXTRA_BIG_TEXT = "android.bigText"; + field public static final String EXTRA_CALL_PERSON = "android.callPerson"; field public static final String EXTRA_CHANNEL_GROUP_ID = "android.intent.extra.CHANNEL_GROUP_ID"; field public static final String EXTRA_CHANNEL_ID = "android.intent.extra.CHANNEL_ID"; field public static final String EXTRA_CHRONOMETER_COUNT_DOWN = "android.chronometerCountDown"; field public static final String EXTRA_COLORIZED = "android.colorized"; field public static final String EXTRA_COMPACT_ACTIONS = "android.compactActions"; field public static final String EXTRA_CONVERSATION_TITLE = "android.conversationTitle"; + field public static final String EXTRA_DECLINE_INTENT = "android.declineIntent"; + field public static final String EXTRA_HANG_UP_INTENT = "android.hangUpIntent"; field public static final String EXTRA_HISTORIC_MESSAGES = "android.messages.historic"; field public static final String EXTRA_INFO_TEXT = "android.infoText"; field public static final String EXTRA_IS_GROUP_CONVERSATION = "android.isGroupConversation"; @@ -5595,6 +5599,8 @@ package android.app { field public static final String EXTRA_TEXT_LINES = "android.textLines"; field public static final String EXTRA_TITLE = "android.title"; field public static final String EXTRA_TITLE_BIG = "android.title.big"; + field public static final String EXTRA_VERIFICATION_ICON = "android.verificationIcon"; + field public static final String EXTRA_VERIFICATION_TEXT = "android.verificationText"; field public static final int FLAG_AUTO_CANCEL = 16; // 0x10 field public static final int FLAG_BUBBLE = 4096; // 0x1000 field public static final int FLAG_FOREGROUND_SERVICE = 64; // 0x40 @@ -5843,6 +5849,14 @@ package android.app { method @NonNull public android.app.Notification.Builder setWhen(long); } + public static class Notification.CallStyle extends android.app.Notification.Style { + method @NonNull public static android.app.Notification.CallStyle forIncomingCall(@NonNull android.app.Person, @NonNull android.app.PendingIntent, @NonNull android.app.PendingIntent); + method @NonNull public static android.app.Notification.CallStyle forOngoingCall(@NonNull android.app.Person, @NonNull android.app.PendingIntent); + method @NonNull public static android.app.Notification.CallStyle forScreeningCall(@NonNull android.app.Person, @NonNull android.app.PendingIntent, @NonNull android.app.PendingIntent); + method @NonNull public android.app.Notification.CallStyle setVerificationIcon(@Nullable android.graphics.drawable.Icon); + method @NonNull public android.app.Notification.CallStyle setVerificationText(@Nullable CharSequence); + } + public static final class Notification.CarExtender implements android.app.Notification.Extender { ctor public Notification.CarExtender(); ctor public Notification.CarExtender(android.app.Notification); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 49f508d83f91..c242fd466c41 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -22,7 +22,10 @@ import static android.graphics.drawable.Icon.TYPE_URI_ADAPTIVE_BITMAP; import static com.android.internal.util.ContrastColorUtil.satisfiesTextContrast; +import static java.util.Objects.requireNonNull; + import android.annotation.ColorInt; +import android.annotation.ColorRes; import android.annotation.DimenRes; import android.annotation.Dimension; import android.annotation.DrawableRes; @@ -33,6 +36,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.annotation.StringRes; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; @@ -403,6 +407,7 @@ public class Notification implements Parcelable STANDARD_LAYOUTS.add(R.layout.notification_template_material_conversation); STANDARD_LAYOUTS.add(R.layout.notification_template_material_media); STANDARD_LAYOUTS.add(R.layout.notification_template_material_big_media); + STANDARD_LAYOUTS.add(R.layout.notification_template_material_call); STANDARD_LAYOUTS.add(R.layout.notification_template_header); } @@ -649,7 +654,7 @@ public class Notification implements Parcelable private static final List<Class<? extends Style>> PLATFORM_STYLE_CLASSES = Arrays.asList( BigTextStyle.class, BigPictureStyle.class, InboxStyle.class, MediaStyle.class, DecoratedCustomViewStyle.class, DecoratedMediaCustomViewStyle.class, - MessagingStyle.class); + MessagingStyle.class, CallStyle.class); /** @hide */ @IntDef(flag = true, prefix = { "FLAG_" }, value = {FLAG_SHOW_LIGHTS, FLAG_ONGOING_EVENT, @@ -1318,6 +1323,53 @@ public class Notification implements Parcelable public static final String EXTRA_IS_GROUP_CONVERSATION = "android.isGroupConversation"; /** + * {@link #extras} key: the type of call represented by the + * {@link android.app.Notification.CallStyle} notification. This extra is an int. + * @hide + */ + public static final String EXTRA_CALL_TYPE = "android.callType"; + + /** + * {@link #extras} key: the person to be displayed as calling for the + * {@link android.app.Notification.CallStyle} notification. This extra is a {@link Person}. + */ + public static final String EXTRA_CALL_PERSON = "android.callPerson"; + + /** + * {@link #extras} key: the icon to be displayed as a verification status of the caller on a + * {@link android.app.Notification.CallStyle} notification. This extra is an {@link Icon}. + */ + public static final String EXTRA_VERIFICATION_ICON = "android.verificationIcon"; + + /** + * {@link #extras} key: the text to be displayed as a verification status of the caller on a + * {@link android.app.Notification.CallStyle} notification. This extra is a + * {@link CharSequence}. + */ + public static final String EXTRA_VERIFICATION_TEXT = "android.verificationText"; + + /** + * {@link #extras} key: the intent to be sent when the users answers a + * {@link android.app.Notification.CallStyle} notification. This extra is a + * {@link PendingIntent}. + */ + public static final String EXTRA_ANSWER_INTENT = "android.answerIntent"; + + /** + * {@link #extras} key: the intent to be sent when the users declines a + * {@link android.app.Notification.CallStyle} notification. This extra is a + * {@link PendingIntent}. + */ + public static final String EXTRA_DECLINE_INTENT = "android.declineIntent"; + + /** + * {@link #extras} key: the intent to be sent when the users hangs up a + * {@link android.app.Notification.CallStyle} notification. This extra is a + * {@link PendingIntent}. + */ + public static final String EXTRA_HANG_UP_INTENT = "android.hangUpIntent"; + + /** * {@link #extras} key: whether the notification should be colorized as * supplied to {@link Builder#setColorized(boolean)}. */ @@ -5876,11 +5928,11 @@ public class Notification implements Parcelable return summary; } - private RemoteViews generateActionButton(Action action, boolean emphazisedMode, + private RemoteViews generateActionButton(Action action, boolean emphasizedMode, StandardTemplateParams p) { final boolean tombstone = (action.actionIntent == null); RemoteViews button = new BuilderRemoteViews(mContext.getApplicationInfo(), - emphazisedMode ? getEmphasizedActionLayoutResource() + emphasizedMode ? getEmphasizedActionLayoutResource() : tombstone ? getActionTombstoneLayoutResource() : getActionLayoutResource()); if (!tombstone) { @@ -5890,43 +5942,42 @@ public class Notification implements Parcelable if (action.mRemoteInputs != null) { button.setRemoteInputs(R.id.action0, action.mRemoteInputs); } - if (emphazisedMode) { + if (emphasizedMode) { // change the background bgColor CharSequence title = action.title; - ColorStateList[] outResultColor = null; + ColorStateList[] outResultColor = new ColorStateList[1]; int background = resolveBackgroundColor(p); if (isLegacy()) { title = ContrastColorUtil.clearColorSpans(title); } else { - outResultColor = new ColorStateList[1]; title = ensureColorSpanContrast(title, background, outResultColor); } button.setTextViewText(R.id.action0, processTextSpans(title)); - setTextViewColorPrimary(button, R.id.action0, p); - int rippleColor; - boolean hasColorOverride = outResultColor != null && outResultColor[0] != null; + int textColor = getPrimaryTextColor(p); + boolean hasColorOverride = outResultColor[0] != null; if (hasColorOverride) { // There's a span spanning the full text, let's take it and use it as the // background color background = outResultColor[0].getDefaultColor(); - int textColor = ContrastColorUtil.resolvePrimaryColor(mContext, + textColor = ContrastColorUtil.resolvePrimaryColor(mContext, background, mInNightMode); - button.setTextColor(R.id.action0, textColor); - rippleColor = textColor; } else if (getRawColor(p) != COLOR_DEFAULT && !isColorized(p) && mTintActionButtons && !mInNightMode) { - rippleColor = resolveContrastColor(p); - button.setTextColor(R.id.action0, rippleColor); - } else { - rippleColor = getPrimaryTextColor(p); + textColor = resolveContrastColor(p); } + button.setTextColor(R.id.action0, textColor); // We only want about 20% alpha for the ripple - rippleColor = (rippleColor & 0x00ffffff) | 0x33000000; + final int rippleColor = (textColor & 0x00ffffff) | 0x33000000; button.setColorStateList(R.id.action0, "setRippleColor", ColorStateList.valueOf(rippleColor)); button.setColorStateList(R.id.action0, "setButtonBackground", ColorStateList.valueOf(background)); button.setBoolean(R.id.action0, "setHasStroke", !hasColorOverride); + if (p.mAllowActionIcons) { + button.setImageViewIcon(R.id.action0, action.getIcon()); + boolean priority = action.getExtras().getBoolean(CallStyle.KEY_ACTION_PRIORITY); + button.setBoolean(R.id.action0, "setWrapModePriority", priority); + } } else { button.setTextViewText(R.id.action0, processTextSpans( processLegacyText(action.title))); @@ -5936,8 +5987,12 @@ public class Notification implements Parcelable button.setTextColor(R.id.action0, resolveContrastColor(p)); } } - button.setIntTag(R.id.action0, R.id.notification_action_index_tag, - mActions.indexOf(action)); + // CallStyle notifications add action buttons which don't actually exist in mActions, + // so we have to omit the index in that case. + int actionIndex = mActions.indexOf(action); + if (actionIndex != -1) { + button.setIntTag(R.id.action0, R.id.notification_action_index_tag, actionIndex); + } return button; } @@ -6371,6 +6426,10 @@ public class Notification implements Parcelable return R.layout.notification_template_material_conversation; } + private int getCallLayoutResource() { + return R.layout.notification_template_material_call; + } + private int getActionLayoutResource() { return R.layout.notification_material_action; } @@ -8039,6 +8098,10 @@ public class Notification implements Parcelable : mBuilder.getMessagingLayoutResource(), p, bindResult); + if (isConversationLayout) { + mBuilder.setTextViewColorPrimary(contentView, R.id.conversation_text, p); + mBuilder.setTextViewColorSecondary(contentView, R.id.app_name_divider, p); + } addExtras(mBuilder.mN.extras); if (!isConversationLayout) { @@ -8925,6 +8988,441 @@ public class Notification implements Parcelable } } + + + /** + * Helper class for generating large-format notifications that include a large image attachment. + * + * Here's how you'd set the <code>CallStyle</code> on a notification: + * <pre class="prettyprint"> + * Notification notif = new Notification.Builder(mContext) + * .setSmallIcon(R.drawable.new_post) + * .setStyle(Notification.CallStyle.forIncomingCall(caller, declineIntent, answerIntent)) + * .build(); + * </pre> + */ + public static class CallStyle extends Style { + private static final int CALL_TYPE_INCOMING = 1; + private static final int CALL_TYPE_ONGOING = 2; + private static final int CALL_TYPE_SCREENING = 3; + + /** + * This is a key used privately on the action.extras to give spacing priority + * to the required call actions + */ + private static final String KEY_ACTION_PRIORITY = "key_action_priority"; + + private int mCallType; + private Person mPerson; + private PendingIntent mAnswerIntent; + private PendingIntent mDeclineIntent; + private PendingIntent mHangUpIntent; + private Icon mVerificationIcon; + private CharSequence mVerificationText; + + CallStyle() { + } + + /** + * Create a CallStyle for an incoming call. + * This notification will have a decline and an answer action, will allow a single + * custom {@link Builder#addAction(Action) action}, and will have a default + * {@link Builder#setContentText(CharSequence) content text} for an incoming call. + * + * @param person The person displayed as the caller. + * The person also needs to have a non-empty name associated with it. + * @param declineIntent The intent to be sent when the user taps the decline action + * @param answerIntent The intent to be sent when the user taps the answer action + */ + @NonNull + public static CallStyle forIncomingCall(@NonNull Person person, + @NonNull PendingIntent declineIntent, @NonNull PendingIntent answerIntent) { + return new CallStyle(CALL_TYPE_INCOMING, person, + null /* hangUpIntent */, + requireNonNull(declineIntent, "declineIntent is required"), + requireNonNull(answerIntent, "answerIntent is required") + ); + } + + /** + * Create a CallStyle for an ongoing call. + * This notification will have a hang up action, will allow up to two + * custom {@link Builder#addAction(Action) actions}, and will have a default + * {@link Builder#setContentText(CharSequence) content text} for an ongoing call. + * + * @param person The person displayed as being on the other end of the call. + * The person also needs to have a non-empty name associated with it. + * @param hangUpIntent The intent to be sent when the user taps the hang up action + */ + @NonNull + public static CallStyle forOngoingCall(@NonNull Person person, + @NonNull PendingIntent hangUpIntent) { + return new CallStyle(CALL_TYPE_ONGOING, person, + requireNonNull(hangUpIntent, "hangUpIntent is required"), + null /* declineIntent */, + null /* answerIntent */ + ); + } + + /** + * Create a CallStyle for a call that is being screened. + * This notification will have a hang up and an answer action, will allow a single + * custom {@link Builder#addAction(Action) action}, and will have a default + * {@link Builder#setContentText(CharSequence) content text} for a call that is being + * screened. + * + * @param person The person displayed as the caller. + * The person also needs to have a non-empty name associated with it. + * @param hangUpIntent The intent to be sent when the user taps the hang up action + * @param answerIntent The intent to be sent when the user taps the answer action + */ + @NonNull + public static CallStyle forScreeningCall(@NonNull Person person, + @NonNull PendingIntent hangUpIntent, @NonNull PendingIntent answerIntent) { + return new CallStyle(CALL_TYPE_SCREENING, person, + requireNonNull(hangUpIntent, "hangUpIntent is required"), + null /* declineIntent */, + requireNonNull(answerIntent, "answerIntent is required") + ); + } + + /** + * @param person The person displayed for the incoming call. + * The user also needs to have a non-empty name associated with it. + * @param hangUpIntent The intent to be sent when the user taps the hang up action + * @param declineIntent The intent to be sent when the user taps the decline action + * @param answerIntent The intent to be sent when the user taps the answer action + */ + private CallStyle(int callType, @NonNull Person person, + @Nullable PendingIntent hangUpIntent, @Nullable PendingIntent declineIntent, + @Nullable PendingIntent answerIntent) { + if (person == null || TextUtils.isEmpty(person.getName())) { + throw new IllegalArgumentException("person must have a non-empty a name"); + } + mCallType = callType; + mPerson = person; + mAnswerIntent = answerIntent; + mDeclineIntent = declineIntent; + mHangUpIntent = hangUpIntent; + } + + /** + * Optional icon to be displayed with {@link #setVerificationText(CharSequence) text} + * as a verification status of the caller. + */ + @NonNull + public CallStyle setVerificationIcon(@Nullable Icon verificationIcon) { + mVerificationIcon = verificationIcon; + return this; + } + + /** + * Optional text to be displayed with an {@link #setVerificationIcon(Icon) icon} + * as a verification status of the caller. + */ + @NonNull + public CallStyle setVerificationText(@Nullable CharSequence verificationText) { + mVerificationText = safeCharSequence(verificationText); + return this; + } + + /** + * @hide + */ + public boolean displayCustomViewInline() { + // This is a lie; True is returned to make sure that the custom view is not used + // instead of the template, but it will not actually be included. + return true; + } + + /** + * @hide + */ + @Override + public void purgeResources() { + super.purgeResources(); + if (mVerificationIcon != null) { + mVerificationIcon.convertToAshmem(); + } + } + + /** + * @hide + */ + @Override + public void reduceImageSizes(Context context) { + super.reduceImageSizes(context); + if (mVerificationIcon != null) { + int rightIconSize = context.getResources().getDimensionPixelSize( + ActivityManager.isLowRamDeviceStatic() + ? R.dimen.notification_right_icon_size_low_ram + : R.dimen.notification_right_icon_size); + mVerificationIcon.scaleDownIfNecessary(rightIconSize, rightIconSize); + } + } + + /** + * @hide + */ + @Override + public RemoteViews makeContentView(boolean increasedHeight) { + return makeCallLayout(); + } + + /** + * @hide + */ + @Override + public RemoteViews makeHeadsUpContentView(boolean increasedHeight) { + return makeCallLayout(); + } + + /** + * @hide + */ + public RemoteViews makeBigContentView() { + return makeCallLayout(); + } + + @NonNull + private Action makeNegativeAction() { + if (mDeclineIntent == null) { + return makeAction(R.drawable.ic_call_decline, + R.string.call_notification_hang_up_action, + R.color.call_notification_decline_color, mHangUpIntent); + } else { + return makeAction(R.drawable.ic_call_decline, + R.string.call_notification_decline_action, + R.color.call_notification_decline_color, mDeclineIntent); + } + } + + @Nullable + private Action makeAnswerAction() { + return mAnswerIntent == null ? null : makeAction(R.drawable.ic_call_answer, + R.string.call_notification_answer_action, + R.color.call_notification_answer_color, mAnswerIntent); + } + + @NonNull + private Action makeAction(@DrawableRes int icon, @StringRes int title, + @ColorRes int colorRes, PendingIntent intent) { + Action action = new Action.Builder(Icon.createWithResource("", icon), + new SpannableStringBuilder().append(mBuilder.mContext.getString(title), + new ForegroundColorSpan(mBuilder.mContext.getColor(colorRes)), + SpannableStringBuilder.SPAN_INCLUSIVE_INCLUSIVE), + intent).build(); + action.getExtras().putBoolean(KEY_ACTION_PRIORITY, true); + return action; + } + + private ArrayList<Action> makeActionsList() { + final Action negativeAction = makeNegativeAction(); + final Action answerAction = makeAnswerAction(); + + ArrayList<Action> actions = new ArrayList<>(MAX_ACTION_BUTTONS); + final Action lastAction; + if (answerAction == null) { + // If there's no answer action, put the hang up / decline action at the end + lastAction = negativeAction; + } else { + // Otherwise put the answer action at the end, and put the decline action at start. + actions.add(negativeAction); + lastAction = answerAction; + } + // For consistency with the standard actions bar, contextual actions are ignored. + for (Action action : Builder.filterOutContextualActions(mBuilder.mActions)) { + if (actions.size() >= MAX_ACTION_BUTTONS - 1) { + break; + } + actions.add(action); + } + actions.add(lastAction); + return actions; + } + + private RemoteViews makeCallLayout() { + Bundle extras = mBuilder.mN.extras; + CharSequence text = mBuilder.processLegacyText(extras.getCharSequence(EXTRA_TEXT)); + if (text == null) { + text = getDefaultText(); + } + + // Bind standard template + StandardTemplateParams p = mBuilder.mParams.reset() + .viewType(StandardTemplateParams.VIEW_TYPE_BIG) + .allowActionIcons(true) + .hideLargeIcon(true) + .text(text) + .summaryText(mBuilder.processLegacyText(mVerificationText)); + // TODO(b/179178086): hide the snooze button + RemoteViews contentView = mBuilder.applyStandardTemplate( + mBuilder.getCallLayoutResource(), p, null /* result */); + + // Bind actions. + mBuilder.resetStandardTemplateWithActions(contentView); + bindCallActions(contentView, p); + + // Bind some extra conversation-specific header fields. + mBuilder.setTextViewColorPrimary(contentView, R.id.conversation_text, p); + mBuilder.setTextViewColorSecondary(contentView, R.id.app_name_divider, p); + contentView.setViewVisibility(R.id.app_name_divider, View.VISIBLE); + bindCallerVerification(contentView, p); + + // Bind some custom CallLayout properties + contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor", + mBuilder.isColorized(p) + ? mBuilder.getPrimaryTextColor(p) + : mBuilder.resolveContrastColor(p)); + contentView.setInt(R.id.status_bar_latest_event_content, + "setNotificationBackgroundColor", mBuilder.resolveBackgroundColor(p)); + contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon", + mBuilder.mN.mLargeIcon); + contentView.setBundle(R.id.status_bar_latest_event_content, "setData", + mBuilder.mN.extras); + + return contentView; + } + + private void bindCallActions(RemoteViews view, StandardTemplateParams p) { + view.setViewVisibility(R.id.actions_container, View.VISIBLE); + view.setViewVisibility(R.id.actions, View.VISIBLE); + view.setViewLayoutMarginDimen(R.id.notification_action_list_margin_target, + RemoteViews.MARGIN_BOTTOM, 0); + + // Clear view padding to allow buttons to start on the left edge. + // This must be done before 'setEmphasizedMode' which sets top/bottom margins. + view.setViewPadding(R.id.actions, 0, 0, 0, 0); + // Add an optional indent that will make buttons start at the correct column when + // there is enough space to do so (and fall back to the left edge if not). + view.setInt(R.id.actions, "setCollapsibleIndentDimen", + R.dimen.call_notification_collapsible_indent); + + // Emphasize so that buttons have borders or colored backgrounds + boolean emphasizedMode = true; + view.setBoolean(R.id.actions, "setEmphasizedMode", emphasizedMode); + // Use "wrap_content" (unlike normal emphasized mode) and allow prioritizing the + // required actions (Answer, Decline, and Hang Up). + view.setBoolean(R.id.actions, "setPrioritizedWrapMode", true); + + // Create the buttons for the generated actions list. + int i = 0; + for (Action action : makeActionsList()) { + final RemoteViews button = mBuilder.generateActionButton(action, emphasizedMode, p); + if (i > 0) { + // Clear start margin from non-first buttons to reduce the gap between buttons. + // (8dp remaining gap is from all buttons' standard 4dp inset). + button.setViewLayoutMarginDimen(R.id.action0, RemoteViews.MARGIN_START, 0); + } + view.addView(R.id.actions, button); + ++i; + } + } + + private void bindCallerVerification(RemoteViews contentView, StandardTemplateParams p) { + if (mVerificationIcon != null) { + contentView.setImageViewIcon(R.id.verification_icon, mVerificationIcon); + contentView.setDrawableTint(R.id.verification_icon, false /* targetBackground */, + mBuilder.getSecondaryTextColor(p), PorterDuff.Mode.SRC_ATOP); + contentView.setViewVisibility(R.id.verification_icon, View.VISIBLE); + } else { + contentView.setViewVisibility(R.id.verification_icon, View.GONE); + } + if (!TextUtils.isEmpty(mVerificationText)) { + contentView.setTextViewText(R.id.verification_text, mVerificationText); + mBuilder.setTextViewColorSecondary(contentView, R.id.verification_text, p); + contentView.setViewVisibility(R.id.verification_text, View.VISIBLE); + } else { + contentView.setViewVisibility(R.id.verification_text, View.GONE); + } + } + + @Nullable + private String getDefaultText() { + switch (mCallType) { + case CALL_TYPE_INCOMING: + return mBuilder.mContext.getString(R.string.call_notification_incoming_text); + case CALL_TYPE_ONGOING: + return mBuilder.mContext.getString(R.string.call_notification_ongoing_text); + case CALL_TYPE_SCREENING: + return mBuilder.mContext.getString(R.string.call_notification_screening_text); + } + return null; + } + + /** + * @hide + */ + public void addExtras(Bundle extras) { + super.addExtras(extras); + extras.putInt(EXTRA_CALL_TYPE, mCallType); + extras.putParcelable(EXTRA_CALL_PERSON, mPerson); + if (mVerificationIcon != null) { + extras.putParcelable(EXTRA_VERIFICATION_ICON, mVerificationIcon); + } + if (mVerificationText != null) { + extras.putCharSequence(EXTRA_VERIFICATION_TEXT, mVerificationText); + } + if (mAnswerIntent != null) { + extras.putParcelable(EXTRA_ANSWER_INTENT, mAnswerIntent); + } + if (mDeclineIntent != null) { + extras.putParcelable(EXTRA_DECLINE_INTENT, mDeclineIntent); + } + if (mHangUpIntent != null) { + extras.putParcelable(EXTRA_HANG_UP_INTENT, mHangUpIntent); + } + fixTitleAndTextExtras(extras); + } + + private void fixTitleAndTextExtras(Bundle extras) { + CharSequence sender = mPerson != null ? mPerson.getName() : null; + if (sender != null) { + extras.putCharSequence(EXTRA_TITLE, sender); + } + if (extras.getCharSequence(EXTRA_TEXT) == null) { + extras.putCharSequence(EXTRA_TEXT, getDefaultText()); + } + } + + /** + * @hide + */ + @Override + protected void restoreFromExtras(Bundle extras) { + super.restoreFromExtras(extras); + mCallType = extras.getInt(EXTRA_CALL_TYPE); + mPerson = extras.getParcelable(EXTRA_CALL_PERSON); + mVerificationIcon = extras.getParcelable(EXTRA_VERIFICATION_ICON); + mVerificationText = extras.getCharSequence(EXTRA_VERIFICATION_TEXT); + mAnswerIntent = extras.getParcelable(EXTRA_ANSWER_INTENT); + mDeclineIntent = extras.getParcelable(EXTRA_DECLINE_INTENT); + mHangUpIntent = extras.getParcelable(EXTRA_HANG_UP_INTENT); + } + + /** + * @hide + */ + @Override + public boolean hasSummaryInHeader() { + return false; + } + + /** + * @hide + */ + @Override + public boolean areNotificationsVisiblyDifferent(Style other) { + if (other == null || getClass() != other.getClass()) { + return true; + } + CallStyle otherS = (CallStyle) other; + return !Objects.equals(mCallType, otherS.mCallType) + || !Objects.equals(mPerson, otherS.mPerson) + || !Objects.equals(mVerificationText, otherS.mVerificationText); + } + } + /** * Notification style for custom views that are decorated by the system * @@ -11376,6 +11874,7 @@ public class Notification implements Parcelable boolean mHideActions; boolean mHideProgress; boolean mPromotePicture; + boolean mAllowActionIcons; CharSequence title; CharSequence text; CharSequence headerTextSecondary; @@ -11392,6 +11891,7 @@ public class Notification implements Parcelable mHideActions = false; mHideProgress = false; mPromotePicture = false; + mAllowActionIcons = false; title = null; text = null; summaryText = null; @@ -11431,6 +11931,11 @@ public class Notification implements Parcelable return this; } + final StandardTemplateParams allowActionIcons(boolean allowActionIcons) { + this.mAllowActionIcons = allowActionIcons; + return this; + } + final StandardTemplateParams promotePicture(boolean promotePicture) { this.mPromotePicture = promotePicture; return this; diff --git a/core/java/com/android/internal/widget/CallLayout.java b/core/java/com/android/internal/widget/CallLayout.java new file mode 100644 index 000000000000..6cc5a4aacda5 --- /dev/null +++ b/core/java/com/android/internal/widget/CallLayout.java @@ -0,0 +1,137 @@ +/* + * 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.internal.widget; + +import android.annotation.AttrRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.StyleRes; +import android.app.Notification; +import android.app.Person; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.util.AttributeSet; +import android.view.RemotableViewMethod; +import android.widget.FrameLayout; +import android.widget.RemoteViews; +import android.widget.TextView; + +import com.android.internal.R; + +/** + * A custom-built layout for the Notification.CallStyle. + */ +@RemoteViews.RemoteView +public class CallLayout extends FrameLayout { + private final PeopleHelper mPeopleHelper = new PeopleHelper(); + + private int mLayoutColor; + private Icon mLargeIcon; + private Person mUser; + + private CachingIconView mConversationIconView; + private CachingIconView mIcon; + private CachingIconView mConversationIconBadgeBg; + private TextView mConversationText; + + public CallLayout(@NonNull Context context) { + super(context); + } + + public CallLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public CallLayout(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CallLayout(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mPeopleHelper.init(getContext()); + mConversationText = findViewById(R.id.conversation_text); + mConversationIconView = findViewById(R.id.conversation_icon); + mIcon = findViewById(R.id.icon); + mConversationIconBadgeBg = findViewById(R.id.conversation_icon_badge_bg); + + // When the small icon is gone, hide the rest of the badge + mIcon.setOnForceHiddenChangedListener((forceHidden) -> { + mPeopleHelper.animateViewForceHidden(mConversationIconBadgeBg, forceHidden); + }); + } + + private void updateCallLayout() { + CharSequence callerName = ""; + String symbol = ""; + Icon icon = null; + if (mUser != null) { + icon = mUser.getIcon(); + callerName = mUser.getName(); + symbol = mPeopleHelper.findNamePrefix(callerName, ""); + } + if (icon == null) { + icon = mLargeIcon; + } + if (icon == null) { + icon = mPeopleHelper.createAvatarSymbol(callerName, symbol, mLayoutColor); + } + // TODO(b/179178086): crop/clip the icon to a circle? + mConversationIconView.setImageIcon(icon); + mConversationText.setText(callerName); + } + + @RemotableViewMethod + public void setLayoutColor(int color) { + mLayoutColor = color; + } + + /** + * @param color the color of the notification background + */ + @RemotableViewMethod + public void setNotificationBackgroundColor(int color) { + mConversationIconBadgeBg.setImageTintList(ColorStateList.valueOf(color)); + } + + @RemotableViewMethod + public void setLargeIcon(Icon largeIcon) { + mLargeIcon = largeIcon; + } + + /** + * Set the notification extras so that this layout has access + */ + @RemotableViewMethod + public void setData(Bundle extras) { + setUser(extras.getParcelable(Notification.EXTRA_CALL_PERSON)); + updateCallLayout(); + } + + private void setUser(Person user) { + mUser = user; + } + +} diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index 40e671ffd27c..1b1e0bfb3a58 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -18,8 +18,6 @@ package com.android.internal.widget; import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_EXTERNAL; import static com.android.internal.widget.MessagingGroup.IMAGE_DISPLAY_LOCATION_INLINE; -import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_IN; -import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_OUT; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -34,10 +32,7 @@ import android.app.Person; import android.app.RemoteInputHistoryItem; import android.content.Context; import android.content.res.ColorStateList; -import android.graphics.Bitmap; -import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.GradientDrawable; @@ -68,14 +63,12 @@ import android.widget.TextView; import com.android.internal.R; import com.android.internal.graphics.ColorUtils; -import com.android.internal.util.ContrastColorUtil; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.function.Consumer; -import java.util.regex.Pattern; /** * A custom-built layout for the Notification.MessagingStyle allows dynamic addition and removal @@ -85,16 +78,6 @@ import java.util.regex.Pattern; public class ConversationLayout extends FrameLayout implements ImageMessageConsumer, IMessagingLayout { - private static final float COLOR_SHIFT_AMOUNT = 60; - /** - * Pattern for filter some ignorable characters. - * p{Z} for any kind of whitespace or invisible separator. - * p{C} for any kind of punctuation character. - */ - private static final Pattern IGNORABLE_CHAR_PATTERN - = Pattern.compile("[\\p{C}\\p{Z}]"); - private static final Pattern SPECIAL_CHAR_PATTERN - = Pattern.compile ("[!@#$%&*()_+=|<>?{}\\[\\]~-]"); private static final Consumer<MessagingMessage> REMOVE_MESSAGE = MessagingMessage::removeMessage; public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f); @@ -106,6 +89,7 @@ public class ConversationLayout extends FrameLayout public static final int IMPORTANCE_ANIM_GROW_DURATION = 250; public static final int IMPORTANCE_ANIM_SHRINK_DURATION = 200; public static final int IMPORTANCE_ANIM_SHRINK_DELAY = 25; + private final PeopleHelper mPeopleHelper = new PeopleHelper(); private List<MessagingMessage> mMessages = new ArrayList<>(); private List<MessagingMessage> mHistoricMessages = new ArrayList<>(); private MessagingLinearLayout mMessagingLinearLayout; @@ -114,9 +98,6 @@ public class ConversationLayout extends FrameLayout private int mLayoutColor; private int mSenderTextColor; private int mMessageTextColor; - private int mAvatarSize; - private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - private Paint mTextPaint = new Paint(); private Icon mAvatarReplacement; private boolean mIsOneToOne; private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>(); @@ -196,6 +177,7 @@ public class ConversationLayout extends FrameLayout @Override protected void onFinishInflate() { super.onFinishInflate(); + mPeopleHelper.init(getContext()); mMessagingLinearLayout = findViewById(R.id.notification_messaging); mActions = findViewById(R.id.actions); mImageMessageContainer = findViewById(R.id.conversation_image_message_container); @@ -205,9 +187,6 @@ public class ConversationLayout extends FrameLayout int size = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels); mMessagingClipRect = new Rect(0, 0, size, size); setMessagingClippingDisabled(false); - mAvatarSize = getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size); - mTextPaint.setTextAlign(Paint.Align.CENTER); - mTextPaint.setAntiAlias(true); mConversationIconView = findViewById(R.id.conversation_icon); mConversationIconContainer = findViewById(R.id.conversation_icon_container); mIcon = findViewById(R.id.icon); @@ -250,15 +229,15 @@ public class ConversationLayout extends FrameLayout }); // When the small icon is gone, hide the rest of the badge mIcon.setOnForceHiddenChangedListener((forceHidden) -> { - animateViewForceHidden(mConversationIconBadgeBg, forceHidden); - animateViewForceHidden(mImportanceRingView, forceHidden); + mPeopleHelper.animateViewForceHidden(mConversationIconBadgeBg, forceHidden); + mPeopleHelper.animateViewForceHidden(mImportanceRingView, forceHidden); }); // When the conversation icon is gone, hide the whole badge mConversationIconView.setOnForceHiddenChangedListener((forceHidden) -> { - animateViewForceHidden(mConversationIconBadgeBg, forceHidden); - animateViewForceHidden(mImportanceRingView, forceHidden); - animateViewForceHidden(mIcon, forceHidden); + mPeopleHelper.animateViewForceHidden(mConversationIconBadgeBg, forceHidden); + mPeopleHelper.animateViewForceHidden(mImportanceRingView, forceHidden); + mPeopleHelper.animateViewForceHidden(mIcon, forceHidden); }); mConversationText = findViewById(R.id.conversation_text); mExpandButtonContainer = findViewById(R.id.expand_button_container); @@ -317,28 +296,6 @@ public class ConversationLayout extends FrameLayout R.dimen.notification_header_separating_margin); } - private void animateViewForceHidden(CachingIconView view, boolean forceHidden) { - boolean nowForceHidden = view.willBeForceHidden() || view.isForceHidden(); - if (forceHidden == nowForceHidden) { - // We are either already forceHidden or will be - return; - } - view.animate().cancel(); - view.setWillBeForceHidden(forceHidden); - view.animate() - .scaleX(forceHidden ? 0.5f : 1.0f) - .scaleY(forceHidden ? 0.5f : 1.0f) - .alpha(forceHidden ? 0.0f : 1.0f) - .setInterpolator(forceHidden ? ALPHA_OUT : ALPHA_IN) - .setDuration(160); - if (view.getVisibility() != VISIBLE) { - view.setForceHidden(forceHidden); - } else { - view.animate().withEndAction(() -> view.setForceHidden(forceHidden)); - } - view.animate().start(); - } - @RemotableViewMethod public void setAvatarReplacement(Icon icon) { mAvatarReplacement = icon; @@ -561,7 +518,8 @@ public class ConversationLayout extends FrameLayout if (mConversationIcon == null) { Icon avatarIcon = messagingGroup.getAvatarIcon(); if (avatarIcon == null) { - avatarIcon = createAvatarSymbol(conversationText, "", mLayoutColor); + avatarIcon = mPeopleHelper.createAvatarSymbol(conversationText, "", + mLayoutColor); } mConversationIcon = avatarIcon; } @@ -674,11 +632,11 @@ public class ConversationLayout extends FrameLayout } } if (lastIcon == null) { - lastIcon = createAvatarSymbol(" ", "", mLayoutColor); + lastIcon = mPeopleHelper.createAvatarSymbol(" ", "", mLayoutColor); } bottomView.setImageIcon(lastIcon); if (secondLastIcon == null) { - secondLastIcon = createAvatarSymbol("", "", mLayoutColor); + secondLastIcon = mPeopleHelper.createAvatarSymbol("", "", mLayoutColor); } topView.setImageIcon(secondLastIcon); } @@ -838,8 +796,10 @@ public class ConversationLayout extends FrameLayout } private void updateTitleAndNamesDisplay() { + // Map of unique names to their prefix ArrayMap<CharSequence, String> uniqueNames = new ArrayMap<>(); - ArrayMap<Character, CharSequence> uniqueCharacters = new ArrayMap<>(); + // Map of single-character string prefix to the only name which uses it, or null if multiple + ArrayMap<String, CharSequence> uniqueCharacters = new ArrayMap<>(); for (int i = 0; i < mGroups.size(); i++) { MessagingGroup group = mGroups.get(i); CharSequence senderName = group.getSenderName(); @@ -847,22 +807,22 @@ public class ConversationLayout extends FrameLayout continue; } if (!uniqueNames.containsKey(senderName)) { - // Only use visible characters to get uniqueNames - String pureSenderName = IGNORABLE_CHAR_PATTERN - .matcher(senderName).replaceAll("" /* replacement */); - char c = pureSenderName.charAt(0); - if (uniqueCharacters.containsKey(c)) { + String charPrefix = mPeopleHelper.findNamePrefix(senderName, null); + if (charPrefix == null) { + continue; + } + if (uniqueCharacters.containsKey(charPrefix)) { // this character was already used, lets make it more unique. We first need to // resolve the existing character if it exists - CharSequence existingName = uniqueCharacters.get(c); + CharSequence existingName = uniqueCharacters.get(charPrefix); if (existingName != null) { - uniqueNames.put(existingName, findNameSplit((String) existingName)); - uniqueCharacters.put(c, null); + uniqueNames.put(existingName, mPeopleHelper.findNameSplit(existingName)); + uniqueCharacters.put(charPrefix, null); } - uniqueNames.put(senderName, findNameSplit((String) senderName)); + uniqueNames.put(senderName, mPeopleHelper.findNameSplit(senderName)); } else { - uniqueNames.put(senderName, Character.toString(c)); - uniqueCharacters.put(c, pureSenderName); + uniqueNames.put(senderName, charPrefix); + uniqueCharacters.put(charPrefix, senderName); } } } @@ -898,8 +858,8 @@ public class ConversationLayout extends FrameLayout } else { Icon cachedIcon = cachedAvatars.get(senderName); if (cachedIcon == null) { - cachedIcon = createAvatarSymbol(senderName, uniqueNames.get(senderName), - mLayoutColor); + cachedIcon = mPeopleHelper.createAvatarSymbol(senderName, + uniqueNames.get(senderName), mLayoutColor); cachedAvatars.put(senderName, cachedIcon); } group.setCreatedAvatar(cachedIcon, senderName, uniqueNames.get(senderName), @@ -908,49 +868,6 @@ public class ConversationLayout extends FrameLayout } } - private Icon createAvatarSymbol(CharSequence senderName, String symbol, int layoutColor) { - if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) || - SPECIAL_CHAR_PATTERN.matcher(symbol).find()) { - Icon avatarIcon = Icon.createWithResource(getContext(), - R.drawable.messaging_user); - avatarIcon.setTint(findColor(senderName, layoutColor)); - return avatarIcon; - } else { - Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - float radius = mAvatarSize / 2.0f; - int color = findColor(senderName, layoutColor); - mPaint.setColor(color); - canvas.drawCircle(radius, radius, radius, mPaint); - boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f; - mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE); - mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f); - int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)); - canvas.drawText(symbol, radius, yPos, mTextPaint); - return Icon.createWithBitmap(bitmap); - } - } - - private int findColor(CharSequence senderName, int layoutColor) { - double luminance = ContrastColorUtil.calculateLuminance(layoutColor); - float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f; - - // we need to offset the range if the luminance is too close to the borders - shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0); - shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0); - return ContrastColorUtil.getShiftedColor(layoutColor, - (int) (shift * COLOR_SHIFT_AMOUNT)); - } - - private String findNameSplit(String existingName) { - String[] split = existingName.split(" "); - if (split.length > 1) { - return Character.toString(split[0].charAt(0)) - + Character.toString(split[1].charAt(0)); - } - return existingName.substring(0, 1); - } - @RemotableViewMethod public void setLayoutColor(int color) { mLayoutColor = color; diff --git a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java index 5213746e5a12..058a9218def4 100644 --- a/core/java/com/android/internal/widget/EmphasizedNotificationButton.java +++ b/core/java/com/android/internal/widget/EmphasizedNotificationButton.java @@ -16,17 +16,24 @@ package com.android.internal.widget; +import android.annotation.Nullable; import android.content.Context; import android.content.res.ColorStateList; +import android.graphics.BlendMode; +import android.graphics.drawable.Drawable; import android.graphics.drawable.DrawableWrapper; import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.InsetDrawable; +import android.graphics.drawable.Icon; import android.graphics.drawable.RippleDrawable; import android.util.AttributeSet; import android.view.RemotableViewMethod; +import android.view.ViewGroup; import android.widget.Button; +import android.widget.LinearLayout; import android.widget.RemoteViews; +import com.android.internal.R; + /** * A button implementation for the emphasized notification style. * @@ -37,6 +44,7 @@ public class EmphasizedNotificationButton extends Button { private final RippleDrawable mRipple; private final int mStrokeWidth; private final int mStrokeColor; + private boolean mPriority; public EmphasizedNotificationButton(Context context) { this(context, null); @@ -80,4 +88,57 @@ public class EmphasizedNotificationButton extends Button { inner.setStroke(hasStroke ? mStrokeWidth : 0, mStrokeColor); invalidate(); } + + /** + * Sets an image icon which will have its size constrained and will be set to the same color as + * the text. Must be called after {@link #setTextColor(int)} for the latter to work. + */ + @RemotableViewMethod(asyncImpl = "setImageIconAsync") + public void setImageIcon(@Nullable Icon icon) { + final Drawable drawable = icon == null ? null : icon.loadDrawable(mContext); + setImageDrawable(drawable); + } + + /** + * @hide + */ + @RemotableViewMethod + public Runnable setImageIconAsync(@Nullable Icon icon) { + final Drawable drawable = icon == null ? null : icon.loadDrawable(mContext); + return () -> setImageDrawable(drawable); + } + + private void setImageDrawable(Drawable drawable) { + if (drawable != null) { + drawable.mutate(); + drawable.setTintList(getTextColors()); + drawable.setTintBlendMode(BlendMode.SRC_IN); + int iconSize = mContext.getResources().getDimensionPixelSize( + R.dimen.notification_actions_icon_drawable_size); + drawable.setBounds(0, 0, iconSize, iconSize); + } + setCompoundDrawablesRelative(drawable, null, null, null); + } + + /** + * Changes the LayoutParams.width to WRAP_CONTENT, with the argument representing if this view + * is a priority over its peers (which affects weight). + */ + @RemotableViewMethod + public void setWrapModePriority(boolean priority) { + mPriority = priority; + ViewGroup.LayoutParams layoutParams = getLayoutParams(); + layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; + if (layoutParams instanceof LinearLayout.LayoutParams) { + ((LinearLayout.LayoutParams) layoutParams).weight = 0; + } + setLayoutParams(layoutParams); + } + + /** + * Sizing this button is a priority compared with its peers. + */ + public boolean isPriority() { + return mPriority; + } } diff --git a/core/java/com/android/internal/widget/NotificationActionListLayout.java b/core/java/com/android/internal/widget/NotificationActionListLayout.java index c7ea781b2793..8e6497b204c7 100644 --- a/core/java/com/android/internal/widget/NotificationActionListLayout.java +++ b/core/java/com/android/internal/widget/NotificationActionListLayout.java @@ -16,6 +16,7 @@ package com.android.internal.widget; +import android.annotation.DimenRes; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.RippleDrawable; @@ -41,13 +42,16 @@ public class NotificationActionListLayout extends LinearLayout { private final int mGravity; private int mTotalWidth = 0; + private int mExtraStartPadding = 0; private ArrayList<Pair<Integer, TextView>> mMeasureOrderTextViews = new ArrayList<>(); private ArrayList<View> mMeasureOrderOther = new ArrayList<>(); private boolean mEmphasizedMode; + private boolean mPrioritizedWrapMode; private int mDefaultPaddingBottom; private int mDefaultPaddingTop; private int mEmphasizedHeight; private int mRegularHeight; + @DimenRes private int mCollapsibleIndentDimen; public NotificationActionListLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); @@ -68,7 +72,7 @@ public class NotificationActionListLayout extends LinearLayout { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - if (mEmphasizedMode) { + if (mEmphasizedMode && !mPrioritizedWrapMode) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } @@ -151,7 +155,15 @@ public class NotificationActionListLayout extends LinearLayout { measuredChildren++; } - mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft; + int collapsibleIndent = mCollapsibleIndentDimen == 0 ? 0 + : getResources().getDimensionPixelOffset(mCollapsibleIndentDimen); + if (innerWidth - usedWidth > collapsibleIndent) { + mExtraStartPadding = collapsibleIndent; + } else { + mExtraStartPadding = 0; + } + + mTotalWidth = usedWidth + mPaddingRight + mPaddingLeft + mExtraStartPadding; setMeasuredDimension(resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec), resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } @@ -163,7 +175,11 @@ public class NotificationActionListLayout extends LinearLayout { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View c = getChildAt(i); - if (c instanceof TextView && ((TextView) c).getText().length() > 0) { + if (c instanceof EmphasizedNotificationButton + && ((EmphasizedNotificationButton) c).isPriority()) { + // add with 0 length to ensure that this view is measured before others. + mMeasureOrderTextViews.add(Pair.create(0, (TextView) c)); + } else if (c instanceof TextView && ((TextView) c).getText().length() > 0) { mMeasureOrderTextViews.add(Pair.create(((TextView) c).getText().length(), (TextView)c)); } else { @@ -197,7 +213,7 @@ public class NotificationActionListLayout extends LinearLayout { @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - if (mEmphasizedMode) { + if (mEmphasizedMode && !mPrioritizedWrapMode) { super.onLayout(changed, left, top, right, bottom); return; } @@ -214,6 +230,9 @@ public class NotificationActionListLayout extends LinearLayout { int absoluteGravity = Gravity.getAbsoluteGravity(Gravity.START, getLayoutDirection()); if (absoluteGravity == Gravity.RIGHT) { childLeft += right - left - mTotalWidth; + } else { + // Put the extra start padding (if any) on the left when LTR + childLeft += mExtraStartPadding; } } @@ -274,6 +293,26 @@ public class NotificationActionListLayout extends LinearLayout { } /** + * When used with emphasizedMode, changes the button sizing behavior to prioritize certain + * buttons (which are system generated) to not scrunch, and leave the remaining space for + * custom actions. + */ + @RemotableViewMethod + public void setPrioritizedWrapMode(boolean prioritizedWrapMode) { + mPrioritizedWrapMode = prioritizedWrapMode; + } + + /** + * When buttons are in wrap mode, this is a padding that will be applied at the start of the + * layout of the actions, but only when those actions would fit with the entire padding + * visible. Otherwise, this padding will be omitted entirely. + */ + @RemotableViewMethod + public void setCollapsibleIndentDimen(@DimenRes int collapsibleIndentDimen) { + mCollapsibleIndentDimen = collapsibleIndentDimen; + } + + /** * Set whether the list is in a mode where some actions are emphasized. This will trigger an * equal measuring where all actions are full height and change a few parameters like * the padding. diff --git a/core/java/com/android/internal/widget/PeopleHelper.java b/core/java/com/android/internal/widget/PeopleHelper.java new file mode 100644 index 000000000000..77f4c8f6bede --- /dev/null +++ b/core/java/com/android/internal/widget/PeopleHelper.java @@ -0,0 +1,179 @@ +/* + * 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.internal.widget; + +import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_IN; +import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_OUT; + +import android.annotation.ColorInt; +import android.annotation.NonNull; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.drawable.Icon; +import android.text.TextUtils; +import android.view.View; + +import com.android.internal.R; +import com.android.internal.graphics.ColorUtils; +import com.android.internal.util.ContrastColorUtil; + +import java.util.regex.Pattern; + +/** + * This class provides some methods used by both the {@link ConversationLayout} and + * {@link CallLayout} which both use the visual design originally created for conversations in R. + */ +public class PeopleHelper { + + private static final float COLOR_SHIFT_AMOUNT = 60; + /** + * Pattern for filter some ignorable characters. + * p{Z} for any kind of whitespace or invisible separator. + * p{C} for any kind of punctuation character. + */ + private static final Pattern IGNORABLE_CHAR_PATTERN = Pattern.compile("[\\p{C}\\p{Z}]"); + private static final Pattern SPECIAL_CHAR_PATTERN = + Pattern.compile("[!@#$%&*()_+=|<>?{}\\[\\]~-]"); + + private Context mContext; + private int mAvatarSize; + private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private Paint mTextPaint = new Paint(); + + /** + * Call this when the view is inflated to provide a context and initialize the helper + */ + public void init(Context context) { + mContext = context; + mAvatarSize = context.getResources().getDimensionPixelSize(R.dimen.messaging_avatar_size); + mTextPaint.setTextAlign(Paint.Align.CENTER); + mTextPaint.setAntiAlias(true); + } + + /** + * A utility for animating CachingIconViews away when hidden. + */ + public void animateViewForceHidden(CachingIconView view, boolean forceHidden) { + boolean nowForceHidden = view.willBeForceHidden() || view.isForceHidden(); + if (forceHidden == nowForceHidden) { + // We are either already forceHidden or will be + return; + } + view.animate().cancel(); + view.setWillBeForceHidden(forceHidden); + view.animate() + .scaleX(forceHidden ? 0.5f : 1.0f) + .scaleY(forceHidden ? 0.5f : 1.0f) + .alpha(forceHidden ? 0.0f : 1.0f) + .setInterpolator(forceHidden ? ALPHA_OUT : ALPHA_IN) + .setDuration(160); + if (view.getVisibility() != View.VISIBLE) { + view.setForceHidden(forceHidden); + } else { + view.animate().withEndAction(() -> view.setForceHidden(forceHidden)); + } + view.animate().start(); + } + + /** + * This creates an avatar symbol for the given person or group + * + * @param name the name of the person or group + * @param symbol a pre-chosen symbol for the person or group. See + * {@link #findNamePrefix(CharSequence, String)} or + * {@link #findNameSplit(CharSequence)} + * @param layoutColor the background color of the layout + */ + @NonNull + public Icon createAvatarSymbol(@NonNull CharSequence name, @NonNull String symbol, + @ColorInt int layoutColor) { + if (symbol.isEmpty() || TextUtils.isDigitsOnly(symbol) + || SPECIAL_CHAR_PATTERN.matcher(symbol).find()) { + Icon avatarIcon = Icon.createWithResource(mContext, R.drawable.messaging_user); + avatarIcon.setTint(findColor(name, layoutColor)); + return avatarIcon; + } else { + Bitmap bitmap = Bitmap.createBitmap(mAvatarSize, mAvatarSize, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + float radius = mAvatarSize / 2.0f; + int color = findColor(name, layoutColor); + mPaint.setColor(color); + canvas.drawCircle(radius, radius, radius, mPaint); + boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f; + mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE); + mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f); + int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)); + canvas.drawText(symbol, radius, yPos, mTextPaint); + return Icon.createWithBitmap(bitmap); + } + } + + private int findColor(@NonNull CharSequence senderName, int layoutColor) { + double luminance = ContrastColorUtil.calculateLuminance(layoutColor); + float shift = Math.abs(senderName.hashCode()) % 5 / 4.0f - 0.5f; + + // we need to offset the range if the luminance is too close to the borders + shift += Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - luminance, 0); + shift -= Math.max(COLOR_SHIFT_AMOUNT / 2.0f / 100 - (1.0f - luminance), 0); + return ContrastColorUtil.getShiftedColor(layoutColor, + (int) (shift * COLOR_SHIFT_AMOUNT)); + } + + /** + * Get the name with whitespace and punctuation characters removed + */ + private String getPureName(@NonNull CharSequence name) { + return IGNORABLE_CHAR_PATTERN.matcher(name).replaceAll("" /* replacement */); + } + + /** + * Gets a single character string prefix name for the person or group + * + * @param name the name of the person or group + * @param fallback the string to return if the name has no usable characters + */ + public String findNamePrefix(@NonNull CharSequence name, String fallback) { + String pureName = getPureName(name); + if (pureName.isEmpty()) { + return fallback; + } + try { + return new String(Character.toChars(pureName.codePointAt(0))); + } catch (RuntimeException ignore) { + return fallback; + } + } + + /** + * Find a 1 or 2 character prefix name for the person or group + */ + public String findNameSplit(@NonNull CharSequence name) { + String nameString = name instanceof String ? ((String) name) : name.toString(); + String[] split = nameString.trim().split("[ ]+"); + if (split.length > 1) { + String first = findNamePrefix(split[0], null); + String second = findNamePrefix(split[1], null); + if (first != null && second != null) { + return first + second; + } + } + return findNamePrefix(name, ""); + } +} diff --git a/core/res/res/drawable/btn_notification_emphasized.xml b/core/res/res/drawable/btn_notification_emphasized.xml index 1a574fe39e6e..ad680549aaa1 100644 --- a/core/res/res/drawable/btn_notification_emphasized.xml +++ b/core/res/res/drawable/btn_notification_emphasized.xml @@ -23,7 +23,7 @@ <ripple android:color="?attr/colorControlHighlight"> <item> <shape android:shape="rectangle"> - <corners android:radius="?attr/buttonCornerRadius" /> + <corners android:radius="@dimen/notification_action_button_radius" /> <padding android:left="@dimen/button_padding_horizontal_material" android:top="@dimen/button_padding_vertical_material" android:right="@dimen/button_padding_horizontal_material" diff --git a/core/res/res/drawable/ic_call_answer.xml b/core/res/res/drawable/ic_call_answer.xml new file mode 100644 index 000000000000..77c0ad1a585f --- /dev/null +++ b/core/res/res/drawable/ic_call_answer.xml @@ -0,0 +1,34 @@ +<!-- + 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="20" + android:viewportHeight="20" + android:tint="?android:attr/colorControlNormal" + android:autoMirrored="true"> + <path + android:fillColor="#FF000000" + android:pathData="M6.0168,3.3333L6.5751,5.575L4.6668,7.4916C3.8751,5.7166 3.6001,4.1916 + 3.4834,3.3333H6.0168ZM14.4251,13.375L16.6668,13.9416V16.5166C15.8084,16.4 14.2668,16.125 + 12.4834,15.325L14.4251,13.375ZM6.3418,1.6666H2.5668C2.0918,1.6666 1.7001,2.0666 + 1.7334,2.5416C2.4834,12.875 11.7668,18.275 17.5168,18.275C17.9668,18.275 18.3334,17.9 + 18.3334,17.4416V13.6166C18.3334,13.0416 17.9418,12.5416 + 17.3834,12.4083L14.5918,11.7083C14.2251,11.6166 13.7584,11.6833 + 13.4084,12.0333L10.9251,14.5166C8.6751,13.1833 6.7918,11.3 + 5.4668,9.0416L7.9168,6.5916C8.2251,6.2833 8.3501,5.8333 + 8.2418,5.4083L7.5584,2.6166C7.4168,2.0583 6.9168,1.6666 6.3418,1.6666Z"/> +</vector>
\ No newline at end of file diff --git a/core/res/res/drawable/ic_call_decline.xml b/core/res/res/drawable/ic_call_decline.xml new file mode 100644 index 000000000000..a5ee8f4e6e72 --- /dev/null +++ b/core/res/res/drawable/ic_call_decline.xml @@ -0,0 +1,36 @@ +<!-- + 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="20" + android:viewportHeight="20" + android:tint="?android:attr/colorControlNormal" + android:autoMirrored="true"> + <path + android:fillColor="#FF000000" + android:pathData="M5.2834,9.3083V10.7833L4.1084,11.4916L3.1334,10.5166C3.8084,10.0416 + 4.5251,9.6333 5.2834,9.3083ZM14.75,9.325C15.4917,9.65 16.2084,10.05 + 16.875,10.525L15.925,11.475L14.75,10.7666V9.325ZM9.975,6.6666C6.8584,6.6666 3.725,7.7333 + 1.1751,9.9416C0.8084,10.2583 0.9667,10.7166 1.1501,10.9L3.2917,13.0416C3.4917,13.2333 + 3.7417,13.3333 4.0001,13.3333C4.175,13.3333 4.35,13.2833 + 4.5084,13.1916L6.4667,12.0166C6.725,11.8583 6.95,11.5583 6.95,11.1666V8.3833C7.95,8.125 + 8.975,8 10.0084,8C11.0417,8 12.075,8.1333 13.0834,8.3916V11.1416C13.0834,11.4916 + 13.2667,11.8166 13.5667,11.9916L15.525,13.1666C15.6834,13.2583 15.8584,13.3083 + 16.0334,13.3083C16.2917,13.3083 16.5417,13.2083 + 16.7334,13.0166L18.85,10.9C19.1167,10.6333 19.1167,10.1916 18.825,9.9416C16.3334,7.7833 + 13.1667,6.6666 9.975,6.6666Z"/> +</vector> diff --git a/core/res/res/layout/notification_material_action_emphasized.xml b/core/res/res/layout/notification_material_action_emphasized.xml index a6b7b380eaa4..cd1f1ab88c96 100644 --- a/core/res/res/layout/notification_material_action_emphasized.xml +++ b/core/res/res/layout/notification_material_action_emphasized.xml @@ -22,6 +22,7 @@ android:layout_height="match_parent" android:layout_marginStart="12dp" android:layout_weight="1" + android:drawablePadding="6dp" android:gravity="center" android:textColor="@color/notification_default_color" android:singleLine="true" diff --git a/core/res/res/layout/notification_template_conversation_header.xml b/core/res/res/layout/notification_template_conversation_header.xml new file mode 100644 index 000000000000..b018676e68f0 --- /dev/null +++ b/core/res/res/layout/notification_template_conversation_header.xml @@ -0,0 +1,166 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/conversation_header" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingTop="16dp" + > + + <TextView + android:id="@+id/conversation_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/notification_conversation_header_separating_margin" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" + android:textSize="16sp" + android:singleLine="true" + android:layout_weight="1" + /> + + <TextView + android:id="@+id/app_name_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" + android:layout_marginEnd="@dimen/notification_conversation_header_separating_margin" + android:text="@string/notification_header_divider_symbol" + android:layout_gravity="center" + android:paddingTop="1sp" + android:singleLine="true" + android:visibility="gone" + /> + + <!-- App Name --> + <com.android.internal.widget.ObservableTextView + android:id="@+id/app_name_text" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" + android:layout_marginEnd="@dimen/notification_conversation_header_separating_margin" + android:paddingTop="1sp" + android:singleLine="true" + android:visibility="gone" + /> + + <TextView + android:id="@+id/time_divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" + android:layout_marginEnd="@dimen/notification_conversation_header_separating_margin" + android:text="@string/notification_header_divider_symbol" + android:layout_gravity="center" + android:paddingTop="1sp" + android:singleLine="true" + android:visibility="gone" + /> + + <DateTimeView + android:id="@+id/time" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" + android:paddingTop="1sp" + android:showRelative="true" + android:singleLine="true" + android:visibility="gone" + /> + + <ViewStub + android:id="@+id/chronometer" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" + android:layout="@layout/notification_template_part_chronometer" + android:visibility="gone" + /> + + <ImageView + android:id="@+id/verification_icon" + android:layout_width="@dimen/notification_badge_size" + android:layout_height="@dimen/notification_badge_size" + android:layout_gravity="center" + android:layout_marginStart="4dp" + android:contentDescription="@string/notification_alerted_content_description" + android:paddingTop="2dp" + android:scaleType="fitCenter" + android:src="@drawable/ic_notifications_alerted" + android:visibility="gone" + /> + + <TextView + android:id="@+id/verification_text" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" + android:paddingTop="1sp" + android:showRelative="true" + android:singleLine="true" + android:visibility="gone" + /> + + <ImageButton + android:id="@+id/feedback" + android:layout_width="@dimen/notification_feedback_size" + android:layout_height="@dimen/notification_feedback_size" + android:layout_gravity="center" + android:layout_marginStart="@dimen/notification_header_separating_margin" + android:background="?android:selectableItemBackgroundBorderless" + android:contentDescription="@string/notification_feedback_indicator" + android:paddingTop="2dp" + android:scaleType="fitCenter" + android:src="@drawable/ic_feedback_indicator" + android:visibility="gone" + /> + + <ImageView + android:id="@+id/profile_badge" + android:layout_width="@dimen/notification_badge_size" + android:layout_height="@dimen/notification_badge_size" + android:layout_gravity="center" + android:layout_marginStart="4dp" + android:paddingTop="2dp" + android:scaleType="fitCenter" + android:visibility="gone" + android:contentDescription="@string/notification_work_profile_content_description" + /> + + <ImageView + android:id="@+id/alerted_icon" + android:layout_width="@dimen/notification_alerted_size" + android:layout_height="@dimen/notification_alerted_size" + android:layout_gravity="center" + android:layout_marginStart="4dp" + android:contentDescription="@string/notification_alerted_content_description" + android:paddingTop="2dp" + android:scaleType="fitCenter" + android:src="@drawable/ic_notifications_alerted" + android:visibility="gone" + /> +</LinearLayout> diff --git a/core/res/res/layout/notification_template_conversation_icon_container.xml b/core/res/res/layout/notification_template_conversation_icon_container.xml new file mode 100644 index 000000000000..e9ec7ce77deb --- /dev/null +++ b/core/res/res/layout/notification_template_conversation_icon_container.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/conversation_icon_container" + android:layout_width="@dimen/conversation_content_start" + android:layout_height="wrap_content" + android:gravity="start|top" + android:clipChildren="false" + android:clipToPadding="false" + android:paddingTop="12dp" + android:paddingBottom="12dp" + android:importantForAccessibility="no" + > + + <FrameLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false" + android:layout_gravity="top|center_horizontal" + > + + <!-- Big icon: 52x52, 12dp padding left + top, 16dp padding right --> + <com.android.internal.widget.CachingIconView + android:id="@+id/conversation_icon" + android:layout_width="@dimen/conversation_avatar_size" + android:layout_height="@dimen/conversation_avatar_size" + android:scaleType="centerCrop" + android:importantForAccessibility="no" + /> + + <ViewStub + android:layout="@layout/conversation_face_pile_layout" + android:layout_width="@dimen/conversation_avatar_size" + android:layout_height="@dimen/conversation_avatar_size" + android:id="@+id/conversation_face_pile" + /> + + <FrameLayout + android:id="@+id/conversation_icon_badge" + android:layout_width="@dimen/conversation_icon_size_badged" + android:layout_height="@dimen/conversation_icon_size_badged" + android:layout_marginLeft="@dimen/conversation_badge_side_margin" + android:layout_marginTop="@dimen/conversation_badge_side_margin" + android:clipChildren="false" + android:clipToPadding="false" + > + + <com.android.internal.widget.CachingIconView + android:id="@+id/conversation_icon_badge_bg" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:src="@drawable/conversation_badge_background" + android:forceHasOverlappingRendering="false" + android:scaleType="center" + /> + + <com.android.internal.widget.CachingIconView + android:id="@+id/icon" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="4dp" + android:layout_gravity="center" + android:forceHasOverlappingRendering="false" + /> + + <com.android.internal.widget.CachingIconView + android:id="@+id/conversation_icon_badge_ring" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:src="@drawable/conversation_badge_ring" + android:visibility="gone" + android:forceHasOverlappingRendering="false" + android:clipToPadding="false" + android:scaleType="center" + /> + </FrameLayout> + </FrameLayout> +</FrameLayout> diff --git a/core/res/res/layout/notification_template_material_call.xml b/core/res/res/layout/notification_template_material_call.xml new file mode 100644 index 000000000000..471d874c59f5 --- /dev/null +++ b/core/res/res/layout/notification_template_material_call.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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 + --> +<com.android.internal.widget.CallLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/status_bar_latest_event_content" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:clipChildren="false" + android:tag="call" + android:theme="@style/Theme.DeviceDefault.Notification" + > + + <!-- CallLayout shares visual appearance with ConversationLayout, so shares layouts --> + <include layout="@layout/notification_template_conversation_icon_container" /> + + <LinearLayout + android:id="@+id/notification_action_list_margin_target" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/notification_action_list_height" + android:orientation="vertical" + > + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_gravity="top" + android:orientation="horizontal" + > + + <LinearLayout + android:id="@+id/notification_main_column" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginStart="@dimen/conversation_content_start" + android:layout_marginEnd="@dimen/notification_content_margin_end" + android:orientation="vertical" + android:minHeight="68dp" + > + + <include + layout="@layout/notification_template_conversation_header" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + /> + + <include layout="@layout/notification_template_text" /> + + <include + android:layout_width="match_parent" + android:layout_height="@dimen/notification_progress_bar_height" + android:layout_marginTop="@dimen/notification_progress_margin_top" + layout="@layout/notification_template_progress" + /> + </LinearLayout> + + <!-- TODO(b/179178086): remove padding from main column when this is visible --> + <com.android.internal.widget.NotificationExpandButton + android:id="@+id/expand_button" + android:layout_width="@dimen/notification_header_expand_icon_size" + android:layout_height="@dimen/notification_header_expand_icon_size" + android:layout_gravity="top|end" + android:contentDescription="@string/expand_button_content_description_collapsed" + android:paddingTop="@dimen/notification_expand_button_padding_top" + android:scaleType="center" + android:visibility="gone" + /> + + </LinearLayout> + + <include layout="@layout/notification_material_action_list" /> + + </LinearLayout> + +</com.android.internal.widget.CallLayout> diff --git a/core/res/res/layout/notification_template_material_conversation.xml b/core/res/res/layout/notification_template_material_conversation.xml index f9364d565f3b..f3aa54066c92 100644 --- a/core/res/res/layout/notification_template_material_conversation.xml +++ b/core/res/res/layout/notification_template_material_conversation.xml @@ -24,82 +24,7 @@ android:theme="@style/Theme.DeviceDefault.Notification" > - <FrameLayout - android:id="@+id/conversation_icon_container" - android:layout_width="@dimen/conversation_content_start" - android:layout_height="wrap_content" - android:gravity="start|top" - android:clipChildren="false" - android:clipToPadding="false" - android:paddingTop="12dp" - android:paddingBottom="12dp" - android:importantForAccessibility="no" - > - - <FrameLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:clipChildren="false" - android:clipToPadding="false" - android:layout_gravity="top|center_horizontal" - > - - <!-- Big icon: 52x52, 12dp padding left + top, 16dp padding right --> - <com.android.internal.widget.CachingIconView - android:id="@+id/conversation_icon" - android:layout_width="@dimen/conversation_avatar_size" - android:layout_height="@dimen/conversation_avatar_size" - android:scaleType="centerCrop" - android:importantForAccessibility="no" - /> - - <ViewStub - android:layout="@layout/conversation_face_pile_layout" - android:layout_width="@dimen/conversation_avatar_size" - android:layout_height="@dimen/conversation_avatar_size" - android:id="@+id/conversation_face_pile" - /> - - <FrameLayout - android:id="@+id/conversation_icon_badge" - android:layout_width="@dimen/conversation_icon_size_badged" - android:layout_height="@dimen/conversation_icon_size_badged" - android:layout_marginLeft="@dimen/conversation_badge_side_margin" - android:layout_marginTop="@dimen/conversation_badge_side_margin" - android:clipChildren="false" - android:clipToPadding="false" - > - <com.android.internal.widget.CachingIconView - android:id="@+id/conversation_icon_badge_bg" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_gravity="center" - android:src="@drawable/conversation_badge_background" - android:forceHasOverlappingRendering="false" - android:scaleType="center" - /> - <com.android.internal.widget.CachingIconView - android:id="@+id/icon" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_margin="4dp" - android:layout_gravity="center" - android:forceHasOverlappingRendering="false" - /> - <com.android.internal.widget.CachingIconView - android:id="@+id/conversation_icon_badge_ring" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:src="@drawable/conversation_badge_ring" - android:visibility="gone" - android:forceHasOverlappingRendering="false" - android:clipToPadding="false" - android:scaleType="center" - /> - </FrameLayout> - </FrameLayout> - </FrameLayout> + <include layout="@layout/notification_template_conversation_icon_container" /> <!-- Wraps entire "expandable" notification --> <com.android.internal.widget.RemeasuringLinearLayout @@ -132,161 +57,14 @@ <!-- Use layout_marginStart instead of paddingStart to work around strange measurement behavior on lower display densities. --> - <LinearLayout - android:id="@+id/conversation_header" + <include + layout="@layout/notification_template_conversation_header" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="horizontal" - android:paddingTop="16dp" android:layout_marginBottom="2dp" android:layout_marginStart="@dimen/conversation_content_start" - > - <TextView - android:id="@+id/conversation_text" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/notification_conversation_header_separating_margin" - android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Title" - android:textSize="16sp" - android:singleLine="true" - android:layout_weight="1" - /> - - <TextView - android:id="@+id/app_name_divider" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="?attr/notificationHeaderTextAppearance" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:layout_marginEnd="@dimen/notification_conversation_header_separating_margin" - android:text="@string/notification_header_divider_symbol" - android:layout_gravity="center" - android:paddingTop="1sp" - android:singleLine="true" - android:visibility="gone" /> - <!-- App Name --> - <com.android.internal.widget.ObservableTextView - android:id="@+id/app_name_text" - android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:layout_marginEnd="@dimen/notification_conversation_header_separating_margin" - android:paddingTop="1sp" - android:singleLine="true" - android:visibility="gone" - /> - - <TextView - android:id="@+id/time_divider" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:textAppearance="?attr/notificationHeaderTextAppearance" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:layout_marginEnd="@dimen/notification_conversation_header_separating_margin" - android:text="@string/notification_header_divider_symbol" - android:layout_gravity="center" - android:paddingTop="1sp" - android:singleLine="true" - android:visibility="gone" - /> - - <DateTimeView - android:id="@+id/time" - android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Time" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:layout_marginStart="@dimen/notification_conversation_header_separating_margin" - android:paddingTop="1sp" - android:showRelative="true" - android:singleLine="true" - android:visibility="gone" - /> - - <ImageButton - android:id="@+id/feedback" - android:layout_width="@dimen/notification_feedback_size" - android:layout_height="@dimen/notification_feedback_size" - android:layout_gravity="center" - android:layout_marginStart="@dimen/notification_header_separating_margin" - android:background="?android:selectableItemBackgroundBorderless" - android:contentDescription="@string/notification_feedback_indicator" - android:paddingTop="2dp" - android:scaleType="fitCenter" - android:src="@drawable/ic_feedback_indicator" - android:visibility="gone" - /> - - <ImageView - android:id="@+id/profile_badge" - android:layout_width="@dimen/notification_badge_size" - android:layout_height="@dimen/notification_badge_size" - android:layout_gravity="center" - android:layout_marginStart="4dp" - android:paddingTop="2dp" - android:scaleType="fitCenter" - android:visibility="gone" - android:contentDescription="@string/notification_work_profile_content_description" - /> - - <ImageView - android:id="@+id/alerted_icon" - android:layout_width="@dimen/notification_alerted_size" - android:layout_height="@dimen/notification_alerted_size" - android:layout_gravity="center" - android:layout_marginStart="4dp" - android:contentDescription="@string/notification_alerted_content_description" - android:paddingTop="2dp" - android:scaleType="fitCenter" - android:src="@drawable/ic_notifications_alerted" - android:visibility="gone" - /> - - <LinearLayout - android:id="@+id/app_ops" - android:layout_height="wrap_content" - android:layout_width="wrap_content" - android:paddingTop="3dp" - android:layout_marginStart="2dp" - android:background="?android:selectableItemBackgroundBorderless" - android:orientation="horizontal" > - <ImageView - android:layout_marginStart="4dp" - android:id="@+id/camera" - android:layout_width="?attr/notificationHeaderIconSize" - android:layout_height="?attr/notificationHeaderIconSize" - android:src="@drawable/ic_camera" - android:visibility="gone" - android:focusable="false" - android:contentDescription="@string/notification_appops_camera_active" - /> - <ImageView - android:id="@+id/mic" - android:layout_width="?attr/notificationHeaderIconSize" - android:layout_height="?attr/notificationHeaderIconSize" - android:src="@drawable/ic_mic" - android:layout_marginStart="4dp" - android:visibility="gone" - android:focusable="false" - android:contentDescription="@string/notification_appops_microphone_active" - /> - <ImageView - android:id="@+id/overlay" - android:layout_width="?attr/notificationHeaderIconSize" - android:layout_height="?attr/notificationHeaderIconSize" - android:src="@drawable/ic_alert_window_layer" - android:layout_marginStart="4dp" - android:visibility="gone" - android:focusable="false" - android:contentDescription="@string/notification_appops_overlay_active" - /> - </LinearLayout> - </LinearLayout> - <!-- Messages --> <com.android.internal.widget.MessagingLinearLayout android:id="@+id/notification_messaging" diff --git a/core/res/res/layout/notification_top_line_views.xml b/core/res/res/layout/notification_top_line_views.xml index 7cda03f8fc4f..7656dd50b2d4 100644 --- a/core/res/res/layout/notification_top_line_views.xml +++ b/core/res/res/layout/notification_top_line_views.xml @@ -26,7 +26,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="@dimen/notification_header_separating_margin" android:singleLine="true" - android:textAppearance="?attr/notificationHeaderTextAppearance" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" android:visibility="?attr/notificationHeaderAppNameVisibility" /> @@ -34,7 +34,7 @@ android:id="@+id/header_text_secondary_divider" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="?attr/notificationHeaderTextAppearance" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" android:layout_marginStart="@dimen/notification_header_separating_margin" android:layout_marginEnd="@dimen/notification_header_separating_margin" android:text="@string/notification_header_divider_symbol" @@ -45,7 +45,7 @@ android:id="@+id/header_text_secondary" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="?attr/notificationHeaderTextAppearance" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" android:layout_marginStart="@dimen/notification_header_separating_margin" android:layout_marginEnd="@dimen/notification_header_separating_margin" android:visibility="gone" @@ -56,7 +56,7 @@ android:id="@+id/header_text_divider" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="?attr/notificationHeaderTextAppearance" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" android:layout_marginStart="@dimen/notification_header_separating_margin" android:layout_marginEnd="@dimen/notification_header_separating_margin" android:text="@string/notification_header_divider_symbol" @@ -67,7 +67,7 @@ android:id="@+id/header_text" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="?attr/notificationHeaderTextAppearance" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" android:layout_marginStart="@dimen/notification_header_separating_margin" android:layout_marginEnd="@dimen/notification_header_separating_margin" android:visibility="gone" @@ -78,7 +78,7 @@ android:id="@+id/time_divider" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="?attr/notificationHeaderTextAppearance" + android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info" android:layout_marginStart="@dimen/notification_header_separating_margin" android:layout_marginEnd="@dimen/notification_header_separating_margin" android:text="@string/notification_header_divider_symbol" diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index 20a5d379cbcc..5546621b6ee8 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -153,6 +153,11 @@ <color name="notification_action_list_background_color">@null</color> + <!-- The color of the Decline and Hang Up actions on a CallStyle notification --> + <color name="call_notification_decline_color">#d93025</color> + <!-- The color of the Answer action on a CallStyle notification --> + <color name="call_notification_answer_color">#1e8e3e</color> + <!-- Keyguard colors --> <color name="keyguard_avatar_frame_color">#ffffffff</color> <color name="keyguard_avatar_frame_shadow_color">#80000000</color> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index cb16af5a16b0..3b6e41ffa92b 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -236,9 +236,20 @@ value is calculated in ConversationLayout#updateActionListPadding() --> <dimen name="notification_actions_padding_start">36dp</dimen> + <!-- The start padding to optionally use (e.g. if there's extra space) for CallStyle + notification actions. + this = conversation_content_start (80dp) - button inset (4dp) - action padding (12dp) --> + <dimen name="call_notification_collapsible_indent">64dp</dimen> + <!-- The size of icons for visual actions in the notification_material_action_list --> <dimen name="notification_actions_icon_size">48dp</dimen> + <!-- The size of icons for visual actions in the notification_material_action_list --> + <dimen name="notification_actions_icon_drawable_size">20dp</dimen> + + <!-- The corner radius if the emphasized action buttons in a notification --> + <dimen name="notification_action_button_radius">8dp</dimen> + <!-- Size of the stroke with for the emphasized notification button style --> <dimen name="emphasized_button_stroke_width">1dp</dimen> diff --git a/core/res/res/values/ids.xml b/core/res/res/values/ids.xml index a4c7293e48b6..ab4e0f3b608e 100644 --- a/core/res/res/values/ids.xml +++ b/core/res/res/values/ids.xml @@ -102,6 +102,10 @@ <item type="id" name="selection_end_handle" /> <item type="id" name="insertion_handle" /> <item type="id" name="floating_toolbar_menu_item_image_button" /> + <item type="id" name="camera" /> + <item type="id" name="mic" /> + <item type="id" name="overlay" /> + <item type="id" name="app_ops" /> <!-- Accessibility action identifier for {@link android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction#ACTION_SHOW_ON_SCREEN}. --> <item type="id" name="accessibilityActionShowOnScreen" /> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 9b5f67091a2a..af5e406979ad 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -5039,6 +5039,18 @@ <!-- Tempalate for Notification.MessagingStyle to join a conversation name with the name of the sender of a message, to make a notification title [CHAR LIMIT=NONE] --> <string name="notification_messaging_title_template"><xliff:g id="conversation_title" example="Tasty Treat Team">%1$s</xliff:g>: <xliff:g id="sender_name" example="Adrian Baker">%2$s</xliff:g></string> + <!-- Action text to be displayed for the "answer" action of an incoming call [CHAR LIMIT=13] --> + <string name="call_notification_answer_action">Answer</string> + <!-- Action text to be displayed for the "decline" action of an incoming call [CHAR LIMIT=13] --> + <string name="call_notification_decline_action">Decline</string> + <!-- Action text to be displayed for the "hang up" action of an ongoing call [CHAR LIMIT=13] --> + <string name="call_notification_hang_up_action">Hang Up</string> + <!-- Default notification text to be displayed in incoming call notifications [CHAR LIMIT=40] --> + <string name="call_notification_incoming_text">Incoming call</string> + <!-- Default notification text to be displayed in ongoing call notifications [CHAR LIMIT=40] --> + <string name="call_notification_ongoing_text">Ongoing call</string> + <!-- Default notification text to be displayed in screening call notifications [CHAR LIMIT=40] --> + <string name="call_notification_screening_text">Screening an incoming call</string> <!-- Label describing the number of selected items [CHAR LIMIT=48] --> <plurals name="selected_count"> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 41375a7dd419..4109d4c9f6f9 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3089,8 +3089,26 @@ <!-- TV Remote Service package --> <java-symbol type="string" name="config_tvRemoteServicePackage" /> + + <!-- Notifications: MessagingStyle --> <java-symbol type="string" name="notification_messaging_title_template" /> + <!-- Notifications: CallStyle --> + <java-symbol type="layout" name="notification_template_material_call" /> + <java-symbol type="string" name="call_notification_answer_action" /> + <java-symbol type="string" name="call_notification_decline_action" /> + <java-symbol type="string" name="call_notification_hang_up_action" /> + <java-symbol type="string" name="call_notification_incoming_text" /> + <java-symbol type="string" name="call_notification_ongoing_text" /> + <java-symbol type="string" name="call_notification_screening_text" /> + <java-symbol type="color" name="call_notification_decline_color"/> + <java-symbol type="color" name="call_notification_answer_color"/> + <java-symbol type="dimen" name="call_notification_collapsible_indent"/> + <java-symbol type="drawable" name="ic_call_answer" /> + <java-symbol type="drawable" name="ic_call_decline" /> + <java-symbol type="id" name="verification_icon" /> + <java-symbol type="id" name="verification_text" /> + <!-- Notification handler / dashboard package --> <java-symbol type="string" name="config_notificationHandlerPackage" /> @@ -3416,6 +3434,7 @@ <java-symbol type="dimen" name="notification_media_image_max_width"/> <java-symbol type="dimen" name="notification_media_image_max_height"/> <java-symbol type="dimen" name="notification_right_icon_size"/> + <java-symbol type="dimen" name="notification_actions_icon_drawable_size"/> <java-symbol type="dimen" name="notification_custom_view_max_image_height"/> <java-symbol type="dimen" name="notification_custom_view_max_image_width"/> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index a4ff18fedbf3..12b8ccfd09d6 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -163,10 +163,14 @@ <!-- heads up elevation that is added if the view is pinned --> <dimen name="heads_up_pinned_elevation">16dp</dimen> - <!-- Height of a messaging notifications with actions at least. Not that this is an upper bound + <!-- Height of a messaging notifications with actions at least. Note that this is an upper bound and the notification won't use this much, but is measured with wrap_content --> <dimen name="notification_messaging_actions_min_height">196dp</dimen> + <!-- Height of a call notification. Note that this is an upper bound + and the notification won't use this much, but is measured with wrap_content --> + <dimen name="call_notification_full_height">172dp</dimen> + <!-- a threshold in dp per second that is considered fast scrolling --> <dimen name="scroll_fast_threshold">1500dp</dimen> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index a03fc136da61..845d321416ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -71,6 +71,7 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.ContrastColorUtil; import com.android.internal.widget.CachingIconView; +import com.android.internal.widget.CallLayout; import com.android.internal.widget.MessagingLayout; import com.android.systemui.Dependency; import com.android.systemui.Interpolators; @@ -165,6 +166,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private int mMaxSmallHeightLarge; private int mMaxSmallHeightMedia; private int mMaxExpandedHeight; + private int mMaxCallHeight; private int mIncreasedPaddingBetweenElements; private int mNotificationLaunchHeight; private boolean mMustStayOnScreen; @@ -645,8 +647,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } private void updateLimitsForView(NotificationContentView layout) { - boolean customView = layout.getContractedChild() != null - && layout.getContractedChild().getId() + View contractedView = layout.getContractedChild(); + boolean customView = contractedView != null + && contractedView.getId() != com.android.internal.R.id.status_bar_latest_event_content; boolean beforeN = mEntry.targetSdk < Build.VERSION_CODES.N; boolean beforeP = mEntry.targetSdk < Build.VERSION_CODES.P; @@ -661,7 +664,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView View expandedView = layout.getExpandedChild(); boolean isMediaLayout = expandedView != null && expandedView.findViewById(com.android.internal.R.id.media_actions) != null; - boolean isMessagingLayout = layout.getContractedChild() instanceof MessagingLayout; + boolean isMessagingLayout = contractedView instanceof MessagingLayout; + boolean isCallLayout = contractedView instanceof CallLayout; boolean showCompactMediaSeekbar = mMediaManager.getShowCompactMediaSeekbar(); if (customView && beforeS && !mIsSummaryWithChildren) { @@ -684,6 +688,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView // make sure we don't crop them terribly. We actually need to revisit this and give // them a headerless design, then remove this hack. smallHeight = mMaxSmallHeightLarge; + } else if (isCallLayout) { + smallHeight = mMaxCallHeight; } else if (mUseIncreasedCollapsedHeight && layout == mPrivateLayout) { smallHeight = mMaxSmallHeightLarge; } else { @@ -1645,6 +1651,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView R.dimen.notification_min_height_media); mMaxExpandedHeight = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_max_height); + mMaxCallHeight = NotificationUtils.getFontScaledHeight(mContext, + R.dimen.call_notification_full_height); mMaxHeadsUpHeightBeforeN = NotificationUtils.getFontScaledHeight(mContext, R.dimen.notification_max_heads_up_height_legacy); mMaxHeadsUpHeightBeforeP = NotificationUtils.getFontScaledHeight(mContext, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCallTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCallTemplateViewWrapper.kt new file mode 100644 index 000000000000..4541ebf4c4f2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCallTemplateViewWrapper.kt @@ -0,0 +1,85 @@ +/* + * 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.statusbar.notification.row.wrapper + +import android.content.Context +import android.view.View +import com.android.internal.widget.CachingIconView +import com.android.internal.widget.CallLayout +import com.android.systemui.R +import com.android.systemui.statusbar.notification.NotificationUtils +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow + +/** + * Wraps a notification containing a call template + */ +class NotificationCallTemplateViewWrapper constructor( + ctx: Context, + view: View, + row: ExpandableNotificationRow +) : NotificationTemplateViewWrapper(ctx, view, row) { + + private val minHeightWithActions: Int = + NotificationUtils.getFontScaledHeight(ctx, R.dimen.call_notification_full_height) + private val callLayout: CallLayout = view as CallLayout + + private lateinit var conversationIconView: CachingIconView + private lateinit var conversationBadgeBg: View + private lateinit var expandBtn: View + private lateinit var appName: View + private lateinit var conversationTitleView: View + + private fun resolveViews() { + with(callLayout) { + conversationIconView = requireViewById(com.android.internal.R.id.conversation_icon) + conversationBadgeBg = + requireViewById(com.android.internal.R.id.conversation_icon_badge_bg) + expandBtn = requireViewById(com.android.internal.R.id.expand_button) + appName = requireViewById(com.android.internal.R.id.app_name_text) + conversationTitleView = requireViewById(com.android.internal.R.id.conversation_text) + } + } + + override fun onContentUpdated(row: ExpandableNotificationRow) { + // Reinspect the notification. Before the super call, because the super call also updates + // the transformation types and we need to have our values set by then. + resolveViews() + super.onContentUpdated(row) + } + + override fun updateTransformedTypes() { + // This also clears the existing types + super.updateTransformedTypes() + addTransformedViews( + appName, + conversationTitleView + ) + addViewsTransformingToSimilar( + conversationIconView, + conversationBadgeBg, + expandBtn + ) + } + + override fun disallowSingleClick(x: Float, y: Float): Boolean { + val isOnExpandButton = expandBtn.visibility == View.VISIBLE && + isOnView(expandBtn, x, y) + return isOnExpandButton || super.disallowSingleClick(x, y) + } + + override fun getMinLayoutHeight(): Int = minHeightWithActions +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt index c49f6cbda8ac..905bccfa6cdf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.row.wrapper import android.content.Context import android.view.View -import android.view.View.GONE import android.view.ViewGroup import com.android.internal.widget.CachingIconView import com.android.internal.widget.ConversationLayout @@ -48,8 +47,8 @@ class NotificationConversationTemplateViewWrapper constructor( private lateinit var conversationIconView: CachingIconView private lateinit var conversationBadgeBg: View - private lateinit var expandButton: View - private lateinit var expandButtonContainer: View + private lateinit var expandBtn: View + private lateinit var expandBtnContainer: View private lateinit var imageMessageContainer: ViewGroup private lateinit var messagingLinearLayout: MessagingLinearLayout private lateinit var conversationTitleView: View @@ -66,9 +65,8 @@ class NotificationConversationTemplateViewWrapper constructor( conversationIconView = requireViewById(com.android.internal.R.id.conversation_icon) conversationBadgeBg = requireViewById(com.android.internal.R.id.conversation_icon_badge_bg) - expandButton = requireViewById(com.android.internal.R.id.expand_button) - expandButtonContainer = - requireViewById(com.android.internal.R.id.expand_button_container) + expandBtn = requireViewById(com.android.internal.R.id.expand_button) + expandBtnContainer = requireViewById(com.android.internal.R.id.expand_button_container) importanceRing = requireViewById(com.android.internal.R.id.conversation_icon_badge_ring) appName = requireViewById(com.android.internal.R.id.app_name_text) conversationTitleView = requireViewById(com.android.internal.R.id.conversation_text) @@ -126,7 +124,7 @@ class NotificationConversationTemplateViewWrapper constructor( addViewsTransformingToSimilar( conversationIconView, conversationBadgeBg, - expandButton, + expandBtn, importanceRing, facePileTop, facePileBottom, @@ -134,11 +132,9 @@ class NotificationConversationTemplateViewWrapper constructor( ) } - override fun getExpandButton() = super.getExpandButton() - override fun setShelfIconVisible(visible: Boolean) { if (conversationLayout.isImportantConversation) { - if (conversationIconView.visibility != GONE) { + if (conversationIconView.visibility != View.GONE) { conversationIconView.isForceHidden = visible // We don't want the small icon to be hidden by the extended wrapper, as force // hiding the conversationIcon will already do that via its listener. @@ -152,7 +148,7 @@ class NotificationConversationTemplateViewWrapper constructor( override fun getShelfTransformationTarget(): View? = if (conversationLayout.isImportantConversation) - if (conversationIconView.visibility != GONE) + if (conversationIconView.visibility != View.GONE) conversationIconView else // A notification with a fallback icon was set to important. Currently @@ -169,8 +165,8 @@ class NotificationConversationTemplateViewWrapper constructor( conversationLayout.updateExpandability(expandable, onClickListener) override fun disallowSingleClick(x: Float, y: Float): Boolean { - val isOnExpandButton = expandButtonContainer.visibility == View.VISIBLE && - isOnView(expandButtonContainer, x, y) + val isOnExpandButton = expandBtnContainer.visibility == View.VISIBLE && + isOnView(expandBtnContainer, x, y) return isOnExpandButton || super.disallowSingleClick(x, y) } @@ -179,10 +175,4 @@ class NotificationConversationTemplateViewWrapper constructor( minHeightWithActions else super.getMinLayoutHeight() - - private fun addTransformedViews(vararg vs: View?) = - vs.forEach { view -> view?.let(mTransformationHelper::addTransformedView) } - - private fun addViewsTransformingToSimilar(vararg vs: View?) = - vs.forEach { view -> view?.let(mTransformationHelper::addViewTransformingToSimilar) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index 97201f5c9a34..34bc5370e8f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -36,7 +36,6 @@ import android.widget.TextView; import com.android.internal.widget.CachingIconView; import com.android.internal.widget.NotificationExpandButton; -import com.android.settingslib.Utils; import com.android.systemui.Interpolators; import com.android.systemui.statusbar.TransformableView; import com.android.systemui.statusbar.ViewTransformationHelper; @@ -60,6 +59,7 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { private CachingIconView mIcon; private NotificationExpandButton mExpandButton; private View mAltExpandTarget; + private View mIconContainer; protected NotificationHeaderView mNotificationHeader; protected NotificationTopLineView mNotificationTopLine; private TextView mHeaderText; @@ -112,6 +112,7 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { mAppNameText = mView.findViewById(com.android.internal.R.id.app_name_text); mExpandButton = mView.findViewById(com.android.internal.R.id.expand_button); mAltExpandTarget = mView.findViewById(com.android.internal.R.id.alternate_expand_target); + mIconContainer = mView.findViewById(com.android.internal.R.id.conversation_icon_container); mLeftIcon = mView.findViewById(com.android.internal.R.id.left_icon); mRightIcon = mView.findViewById(com.android.internal.R.id.right_icon); mWorkProfileImage = mView.findViewById(com.android.internal.R.id.profile_badge); @@ -203,11 +204,8 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { public void clearConversationSkin() { if (mAppNameText != null) { final ColorStateList colors = mAppNameText.getTextColors(); - final int textAppearance = Utils.getThemeAttr( - mAppNameText.getContext(), - com.android.internal.R.attr.notificationHeaderTextAppearance, + mAppNameText.setTextAppearance( com.android.internal.R.style.TextAppearance_DeviceDefault_Notification_Info); - mAppNameText.setTextAppearance(textAppearance); mAppNameText.setTextColor(colors); MarginLayoutParams layoutParams = (MarginLayoutParams) mAppNameText.getLayoutParams(); final int marginStart = mAppNameText.getResources().getDimensionPixelSize( @@ -265,19 +263,11 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_ICON, mIcon); mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_EXPANDER, mExpandButton); - if (mWorkProfileImage != null) { - mTransformationHelper.addViewTransformingToSimilar(mWorkProfileImage); - } if (mIsLowPriority && mHeaderText != null) { mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE, mHeaderText); } - if (mAudiblyAlertedIcon != null) { - mTransformationHelper.addViewTransformingToSimilar(mAudiblyAlertedIcon); - } - if (mFeedbackIcon != null) { - mTransformationHelper.addViewTransformingToSimilar(mFeedbackIcon); - } + addViewsTransformingToSimilar(mWorkProfileImage, mAudiblyAlertedIcon, mFeedbackIcon); } @Override @@ -287,6 +277,9 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { if (mAltExpandTarget != null) { mAltExpandTarget.setOnClickListener(expandable ? onClickListener : null); } + if (mIconContainer != null) { + mIconContainer.setOnClickListener(expandable ? onClickListener : null); + } if (mNotificationHeader != null) { mNotificationHeader.setOnClickListener(expandable ? onClickListener : null); } @@ -371,4 +364,20 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper { super.setVisible(visible); mTransformationHelper.setVisible(visible); } + + protected void addTransformedViews(View... views) { + for (View view : views) { + if (view != null) { + mTransformationHelper.addTransformedView(view); + } + } + } + + protected void addViewsTransformingToSimilar(View... views) { + for (View view : views) { + if (view != null) { + mTransformationHelper.addViewTransformingToSimilar(view); + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java index b3d1a94beaa9..5fff8c83048f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java @@ -74,6 +74,8 @@ public abstract class NotificationViewWrapper implements TransformableView { return new NotificationMessagingTemplateViewWrapper(ctx, v, row); } else if ("conversation".equals(v.getTag())) { return new NotificationConversationTemplateViewWrapper(ctx, v, row); + } else if ("call".equals(v.getTag())) { + return new NotificationCallTemplateViewWrapper(ctx, v, row); } Class<? extends Notification.Style> style = row.getEntry().getSbn().getNotification().getNotificationStyle(); |